diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..bc6a78ca --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,23 @@ +version: "2" +checks: + method-count: + config: + threshold: 50 +exclude_patterns: + - "parsers/examples/" + - "debian/" + - "deps/" + - "scripts/" + - "wordlists/" + - "backup/" + - "docker/" + - "log/" + - "tmp/" + - "images/" + - "**/__init__.py" + - "**/*.conf" + - "**/*.sh" + - "**/*.qss" + - "**/*.txt" + - "**/*.yml" + - "**/*.md" \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..85d014d1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +select=E501,F811,F823,F831,F841,E502,E703,E704,E713,E741,E742,E743,W291,W601,W602 +exclude=.git,.idea,tmp,backup,log,images,venv +max-line-length: 120 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..a8c4c4dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report about: Create a bug report title: '' +labels: '' +assignees: '' + +--- + +**Legion version**: {Enter Legion version you are running} + +**Which install method are you using**: {Choose either 'Docker' or 'Traditional install' method} + +**DESCRIPTION** + +Enter a clear description. E.g., It's dark in the room. + +**PRECONDITIONS** +Describe any preconditions that might be required. Be specific. Include versions, execution method, operating system, +and anything else that might be needed to replicate the conditions. E.g., Be in room 5a on 123 plop lane, Plopsville, +NY. It's a soulless modern retrofit within a once beautiful and grand building. It's hidden by a poorly painted dark +green door six rooms down from the elevator with a cartoon drawn on it over on the west side. + +**STEPS TO REPRODUCE** +Describe the sequence of events needed to reproduce the problem. E.g., + +1. Enter room 5a. +2. Flip light switch. +3. Realize it's still dark. + +**EXPECTED RESULT** +Enter a clear description of what you expected under normal conditions. E.g., A lit room. + +**ACTUAL RESULT** +Enter a clear description of the outcome deemed to be a problem. E.g., A dark room. + +**ADDITIONAL INFORMATION** +Enter any additional relevant information including screenshots. E.g., The room has a distinct smell. The switch used is +on the left side of the door. diff --git a/.gitignore b/.gitignore index 894a44cc..3e265ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,30 @@ venv.bak/ # mypy .mypy_cache/ + +# temps +tmp/ + +# init +.initialized + +# extension scripts +scripts/ms08-067_check.py +scripts/ndr.py +scripts/rdp-sec-check.pl +scripts/smbenum.sh +scripts/snmpbrute.py +scripts/installDeps.sh +scripts/smtp-user-enum.pl +scripts/snmpcheck.rb + +scripts/CloudFail + +docker/runLocal.sh +docker/cleanupUntagged.sh +docker/cleanupExited.sh + +log/*.log +fixname.sh +ghostdriver.log +huge_nmap.xml diff --git a/.justcloned b/.justcloned new file mode 100644 index 00000000..e69de29b diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 00000000..5d109203 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,149 @@ +LEGION 0.4.3 + +* Revise NMAP import process +* Fix import progress calculations +* Doubleckick to copy hostname (Linux only) +* Script to generate huge bogus NMAP XML imports for testing. + +LEGION 0.4.2 + +* Tweak the screenshooter to use eyewitness as suggested by daniruiz +* Add a Wsl check before running unixPath2Win +* Include Revision by daniruiz to tempPath creation routine +* Revise to monospaced font to improve readability as suggested by daniruiz +* Revise dependancies to resolve missing PhantomJs import +* Set log level to Info +* Eliminate some temporary code, debug lines, and other cleanup +* Revise screenshooter to use schema://ip:port when url is a single node +* Fix typo in startLegion.sh + +LEGION 0.4.1 + +* Add checkXserver.sh to help users troubleshoot connections to X +* Fix a few missing dependencies + +LEGION 0.4.0 + +* Refactored to Python 3.8+ +* Refactored to PyQt 6 +* Database calls migrated to sessions (dramatically improves performance, reliability and huge memory reductions) +* Refactored Logging +* General cleanup +* Screenshot tool revised to use PhantomJs webdriver (other webdrivers coming soon) +* Simplify startup scripts, dependancy installations scripts, etc +* Eliminate support for distributions other than Kali 2022 and later or Ubuntu 20.04 or later +* Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) + +LEGION 0.3.9 + +* Start time message box ensuring run as root +* Start time message box to help users resolve NMAP v7.92 segfaults +* Bug fixes for edge cases +* Screenshot tool revisions +* Default config revisions +* Open maximized +* Don't open with top of window off screen +* Stage module revisions (nothing hard coded anymore, adds option to specify any NSE script for any stage) +* Ensure pyExploiutDb is updated at all times + +LEGION 0.3.8 + +* Bug fixes +* Preparation to move to postgresql backend + +LEGION 0.3.7 + +* Bug fixes for several edge cases +* Screenshot fixes +* Service version data overwrite bug fixed +* Stale service version data bug fixed +* Refactor of docker base image + +LEGION 0.3.6 + +* Significant code refactoring + +LEGION 0.3.5 + +* Bug Fixes +* Copy from tables using double click +* CVE -> ExploitDB redesign using pyExploitDb and bugfixes + +LEGION 0.3.4 + +* Depnendancy polish +* Minor UI and schedule changes + +LEGION 0.3.3 + +* Fix hydra 8.7+ issues +* Fix minor UI update issues + +LEGION 0.3.2 + +* Stage 3 is now vulners scan +* Former stages 3, 4 and 5 are respectively 4, 5 and 6 now +* Config editor dialog + +LEGION 0.3.1 + +* UI polish everywhere (element sizing, scaling, icons, tooltips, etc.) +* Code cleanup +* UI performance improvements +* More port actions + +LEGION 0.3.0 + +* UI polish everywhere (element sizing, scaling, icons, etc.) +* Code cleanup +* Highly configurable host addition dialog +* Stability improvements +* Docker image of preconfigured application published to DockerHub + +LEGION 0.2.4 + +* Open SPRT and Legion files +* Resolve file open/save access issue +* Consolidate lower panel + +LEGION 0.2.3 + +* Bug fixes for edge cases +* Elapsed and estimated remaining time for processes +* Host 1-M CVE UI element added +* Service 1-M CVE UI element added +* CVE object model revised +* Improved UI performance + +LEGION 0.2.2 + +* Bug fixes for edge cases +* In UI Logging panel +* Thread safe logging +* Add AzureCveQuery Plugin +* Setup to use AsyncIO +* Dep installer fixes +* Addition of .justcloned to re-initalize on cloning / pull to update + +LEGION 0.2.1 + +* Fixed DB relationships +* Removed X requirement for screen captures +* Revised HTTP/HTTPS detection +* Fixed Host panel columns +* Fixed process lanuches for discovered services +* Fixed context menus so they show applicable actions for context +* Fixed note unique keys + +LEGION 0.2.0 + +* Port to PyQt6 +* Handle process output encoding issues +* Added dependancy installer + +LEGION 0.1.1 + +* Support for WSL (Windows Subsystem for Linux) +* Removed Elixir +* Converted to Python3.5+ +* Process handeling ensures dead processes are shown as crashed or finished diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b071be60 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing + +Are you interested in contributing to Legion? We love contributors! We ask that you follow the guidelines below when +making contributions to the project. We ask that you also follow our contributor [**Code of Conduct**](#code-of-conduct) +. We look forward to working with you - get started in one of the sections below. + +#### Did you find a bug? + +* Awesome. First, ensure that your bug isn't a duplicate by checking + the [Issues](https://github.com/Hackman238/legion/issues). **If the issue already exists**, go ahead and comment on + the issue with your bug report information, following the guidelines below as if you were reporting a new issue - just + don't make a new issue. +* **If you can't find the issue**, its time to [open a new issue](https://github.com/Hackman238/legion/issues/new). Fill + in the issue with a clear title and description. Where possible, include steps to reproduce the bug, code samples, and + screenshots. Tag the issue if it fits into one of our existing categories. + +#### Did you patch a bug? + +* Excellent - your first steps are to open a new [Pull Request](https://github.com/Hackman238/legion/pulls) with your + patch. Be prepared to answer questions or receive feedback from the team. +* Ensure the PR has a description that clearly details the problem and your solution. Be sure to include issue numbers + if possible. +* Follow our coding practices and standards for whatever files you're working on. + +#### Do you want to add a feature? + +* We love new features! Please [make an issue](https://github.com/Hackman238/legion/issues/new) for discussion, with a + clear title and description of the proposed feature. Add the Proposal tag, and use the comments to follow up with the + team. +* Wait for a response and approval to your proposal before working on your patch and submitting a PR. This way, if we + find any problems with the proposal, and discuss remediation before work gets wasted. +* Once your feature is complete, all you need to do is [submit a PR](https://github.com/Hackman238/legion/pulls). + +#### Do you have a general question? + +* If you have any questions not related to legion's code, features, or bugs use legion@shanewilliamscott.com + +#### Code of Conduct + +We like to keep the rules simple. + +* Behavior meant to malign, disrespect, denigrate, harass, or attack any person will not be tolerated. +* Use respectful language and foster a community of collaboration. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..e6b312a8 --- /dev/null +++ b/README.md @@ -0,0 +1,302 @@ +## NOTICE + +This is the new home of "Legion". A major release will follow very soon! + +## + +![alt tag](https://github.com/Hackman238/legion/blob/master/images/LegionBanner.png) + +## ✨ About + +Legion, a fork of SECFORCE's Sparta, is an open source, easy-to-use, super-extensible, and semi-automated network +penetration testing framework that aids in discovery, reconnaissance, and exploitation of information systems. + +## Fix NMAP 7.92 Sefgaults under Kali + +Install NMAP 7.93 using the following: +```shell +sudo apt install snapd -y +sudo systemctl enable --now snapd.apparmor +sudo systemctl start snapd +sudo snap install nmap +sudo mv /usr/bin/nmap /usr/bin/nmap-7.92 +sudo ln -s /snap/bin/nmap /usr/bin/nmap +``` + +Then verify the version is 7.93 with: +`nmap -v` + +Update the apparmor profile: +`vi /var/lib/snapd/apparmor/profiles/snap.nmap.nmap` + +Goto line 300, create new line and add in: +``` +owner @{HOME}/.local/share/legion/tmp/** rw, +/etc/ssl/kali.cnf r, +``` + +Reboot + +## 🍿 Features + +* Automatic recon and scanning with NMAP, whataweb, nikto, Vulners, Hydra, SMBenum, dirbuster, sslyzer, webslayer and + more (with almost 100 auto-scheduled scripts). +* Easy to use graphical interface with rich context menus and panels that allow pentesters to quickly find and exploit + attack vectors on hosts. +* Modular functionality allows users to easily customize Legion and automatically call their own scripts/tools. +* Multiple custom scan configurations ideal for testing different environments of various size and complexity. +* Highly customizable stage scanning for ninja-like IPS evasion. +* Automatic detection of CPEs (Common Platform Enumeration) and CVEs (Common Vulnerabilities and Exposures). +* Ties CVEs to Exploits as detailed in Exploit-Database. +* Realtime auto-saving of project results and tasks. + +### Notable changes from Sparta + +* Refactored from Python 2.7 to Python 3.8+ and the elimination of deprecated and unmaintained libraries. +* Upgraded to PyQT6, increased responsiveness, less buggy, more intuitive GUI that includes features like: + * Task completion estimates + * 1-Click scan lists of ips, hostnames and CIDR subnets + * Ability to purge results, rescan hosts and delete hosts + * Granular NMAP scanning options +* Support for hostname resolution and scanning of vhosts/sni hosts. +* Revise process queuing and execution routines for increased app reliability and performance. +* Simplification of installation with dependency resolution and installation routines. +* Realtime project auto-saving so in the event some goes wrong, you will not lose any progress! +* Docker container deployment option. +* Supported by a highly active development team. + +## 🌉 Supported Distributions + +### Docker runIt script support + +RunIt script (`docker/runIt.sh`) supports: + +- Ubuntu 20.04+ +- Kali 2022+ + +It is possible to run the docker image on any Linux distribution, however, different distributions have different hoops +to jump through to get a docker app to be able to connect to the X server. Everyone is welcome to try to figure those +hoops out and create a PR for runIt. + +### Traditional installation support + +We can only promise correct operation on **Ubuntu 20.04** using the traditional installation at this time. While it should +work on ParrotOS, Kali, and others, until we have Legion packaged and placed into the repos for each of these distros, +it is musical chairs in regard to platform updates changing and breaking dependencies. Native a native package exists and is +included by default on Kali. + +## 💻 Installation + +Two installation methods available: + +- [Docker method](#traditional-installation-method) +- [Traditional installation method](#traditional-installation-method) + +It is **preferable** to use the Docker method over a traditional installation. This is because of all the dependency +requirements and the complications that occur in environments which differ from a clean, non-default installation. + +> NOTE: Docker versions of Legion are *unlikely* to work when run as root or under a root X! + +### Docker method + +Docker method includes support for various environments, choose the one that works for you. + +- [Linux with local X11](#linux-with-local-x11) +- [Linux with remote X11](#linux-with-remote-x11) +- [Windows under WSL](#windows-under-wsl-using-xming-and-docker-desktop) +- [⚠️ Windows without WSL](#windows-using-xming-and-docker-desktop-without-wsl) +- [⚠️ OSX using XQuartz](#osx-using-xquartz) + +#### Linux with local X11 + +Assumes **Docker** and **X11** are installed and set up (including running Docker commands as a non-root user). + +It is critical to follow all the instructions for running as a non-root user. Skipping any of them will result in +complications getting Docker to communicate with the X server. + +See detailed instructions to set up Docker [here](#configuring-docker) and enable running containers as non-root users +and granting Docker group SSH rights [here](#setup-docker-to-allow-non-root-users). + +Within Terminal: + +```shell +git clone https://github.com/Hackman238/legion.git +cd legion/docker +chmod +x runIt.sh +./runIt.sh +``` + +#### Linux with remote X11 + +Assumes **Docker** and **X11** are installed and set up. + +Replace `X.X.X.X` with the IP address of the remote running X11. + +Within Terminal: + +```shell +git clone https://github.com/Hackman238/legion.git +cd legion/docker +chmod +x runIt.sh +./runIt.sh X.X.X.X +``` + +#### Windows under WSL using Xming and Docker Desktop + +Assumes: + +- Xming is installed in Windows. +- Docker Desktop is installed in Windows +- Docker Desktop is running in Linux containers mode +- Docker Desktop is connected to WSL. + +See detailed Docker instructions [here](#setup-hyper-v-docker-desktop-xming-and-wsl) + +Replace `X.X.X.X` with the IP address with which Xming has registered itself. Right click Xming in system tray -> View +log and see IP next to "XdmcpRegisterConnection: newAddress" + +Within Terminal: + +```shell +git clone https://github.com/Hackman238/legion.git +cd legion/docker +sudo chmod +x runIt.sh +sudo ./runIt.sh X.X.X.X +``` + +#### Windows using Xming and Docker Desktop without WSL + +Why? Don't do this. :) + +#### OSX using XQuartz + +Not yet in `runIt.sh` script. Possible to set up using `socat`. +See [instructions here](https://kartoza.com/en/blog/how-to-run-a-linux-gui-application-on-osx-using-docker/) + +#### Configuring Docker + +#### Setting up Docker on Linux + +To install Docker components typically needed and add set up the environment for Docker, under a term, run: + +```shell +sudo apt-get update +sudo apt-get install -y docker.io python3-pip -y +sudo groupadd docker +pip install --user docker-compose +``` + +#### Setup Docker to allow non-root users + +To enable non-root users to run Docker commands, under a term, run: + +```shell +sudo usermod -aG docker $USER +sudo chmod 666 /var/run/docker.sock +sudo xhost +local:docker +``` + +#### Setup Hyper-V, Docker Desktop, Xming and WSL + +The order is important for port reservation reasons. If you have WSL, HyperV, or Docker Desktop installed then please +uninstall those features before proceeding. + +- Cortana / Search -> cmd -> Right click -> Run as Administrator +- To reserve the Docker port, under CMD, run: + ```shell + netsh int ipv4 add excludedportrange protocol=tcp startport=2375 numberofports=1 + ``` + - This will likely fail if you have Hyper-V already enabled or Docker Desktop installed +- To install Hyper-V, under CMD, run: + ```shell + dism.exe /Online /Enable-Feature:Microsoft-Hyper-V /All + ``` +- Reboot +- Cortana / Search -> cmd -> Right click -> Run as Administrator +- To install WSL, under CMD, run: + ```shell + dism.exe /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux + ``` +- Reboot +- Download from (Free account required) +- Run installer +- Optionally input your Docker Hub login +- Right click Docker Desktop in system tray -> Switch to Linux containers + - If it says Switch to Windows containers then skip this step, it's already using Linux containers +- Right click Docker Desktop in system tray -> Settings +- General -> Expose on localhost without TLS +- Download +- Run installer and select multi window mode +- Open Microsoft Store +- Install Kali, Ubuntu or one of the other WSL Linux Distributions +- Open the distribution, let it bootstrap and fill in the user creation details +- To install Docker components typically needed and add set up the environment for Docker redirection, under the WSL + window, run: + ```shell + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + sudo apt-get update + sudo apt-get install -y docker-ce python-pip -y + sudo apt autoremove + sudo usermod -aG docker $USER + pip install --user docker-compose + echo "export DOCKER_HOST=tcp://localhost:2375" >> ~/.bashrc && source ~/.bashrc + ``` +- Test Docker is reachable with: + ```shell + docker images + ``` + +### Traditional installation method + +> Please use the Docker image where possible! It's becoming very difficult to support all the various platforms and +> their own quirks. + +Assumes Ubuntu, Kali or Parrot Linux is being used with **Python 3.6** installed. + +Within Terminal: + +```shell +git clone https://github.com/Hackman238/legion.git +cd legion +sudo chmod +x startLegion.sh +sudo ./startLegion.sh +``` + +## 🏗 Development + +### Executing test cases + +To run all test cases, execute the following in root directory: + +```shell +python -m unittest +``` + +### Modifying Configuration + +The configuration of selected ports and associated terminal actions can be easily modified by editing the legion.conf file. +> [StagedNmapSettings] defines what ports will be scanned in sequential order as well as any NSE scripts that will be called. +> +> [SchedulerSettings] defines what actions will occur automatically based upon port scan results. + +```shell +sudoedit /root/.local/share/legion/legion.conf +``` + +## ⚖️ License + +Legion is licensed under the GNU General Public License v3.0. Take a look at the +[LICENSE](https://github.com/Hackman238/legion/blob/master/LICENSE) for more information. + +## ⭐️ Attribution + +* Refactored Python 3.6+ codebase, added feature set and ongoing development of Legion is credited + to [Hackman238] & [sscottgvit] (Shane Scott) +* The initial Sparta Python 2.7 codebase and application design is credited SECFORCE. +* Several additional PortActions, PortTerminalActions and SchedulerSettings are credited to batmancrew. +* The nmap XML output parsing engine was largely based on code by yunshu, modified by ketchup and modified SECFORCE. +* ms08-067_check script used by `smbenum.sh` is credited to Bernardo Damele A.G. +* Legion relies heavily on nmap, hydra, python, PyQt, SQLAlchemy and many other tools and technologies, so we would like + to thank all of the people involved in the creation of those. +* Special thanks to Dmitriy Dubson [ddubson] for his continued contributions to the project! diff --git a/app/ApplicationInfo.py b/app/ApplicationInfo.py new file mode 100644 index 00000000..f9254306 --- /dev/null +++ b/app/ApplicationInfo.py @@ -0,0 +1,44 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +applicationInfo = { + "name": "LEGION", + "version": "0.4.3", + "build": '1729699482', + "author": "Shane Scott", + "copyright": "2024", + "links": ["http://github.com/Hackman238/legion/issues"], + "emails": [], + "update": '10/23/2024', + "license": "GPL v3", + "desc": "Legion is a fork of SECFORCE's Sparta, Legion is an open source, easy-to-use, \n" + + "super-extensible and semi-automated network penetration testing tool that aids in " + + "discovery, \nreconnaissance and exploitation of information systems.", + "smallIcon": "./images/icons/Legion-N_128x128.svg", + "bigIcon": "./images/icons/Legion-N_128x128.svg" +} + + +def getVersion(): + return f"{applicationInfo['version']}-{applicationInfo['build']}" + + +def getConsoleLogo(): + fileObj = open('./app/legionLogo.txt', 'r') + allData = fileObj.read() + return allData diff --git a/app/ModelHelpers.py b/app/ModelHelpers.py new file mode 100644 index 00000000..1b741707 --- /dev/null +++ b/app/ModelHelpers.py @@ -0,0 +1,34 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2019 Hackman238 + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from PyQt6 import QtCore + + +def resolveHeaders(role, orientation, section, headers): + if role == QtCore.Qt.ItemDataRole.DisplayRole and orientation == QtCore.Qt.Orientation.Horizontal: + if section < len(headers): + return headers[section] + else: + return "not implemented in view model" + + +def itemInteractive() -> QtCore.Qt.ItemFlag: + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable + + +def itemSelectable() -> QtCore.Qt.ItemFlag: + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable diff --git a/app/Project.py b/app/Project.py new file mode 100644 index 00000000..857829f1 --- /dev/null +++ b/app/Project.py @@ -0,0 +1,44 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from typing import NamedTuple + +from app.auxiliary import Wordlist +from db.RepositoryContainer import RepositoryContainer +from db.SqliteDbAdapter import Database + +projectTypes = ["legion", "sparta"] + + +class ProjectProperties(NamedTuple): + projectName: str + workingDirectory: str + projectType: str + isTemporary: bool + outputFolder: str + runningFolder: str + usernamesWordList: Wordlist + passwordWordList: Wordlist + storeWordListsOnExit: bool + + +class Project: + def __init__(self, projectProperties: ProjectProperties, repositoryContainer: RepositoryContainer, + database: Database): + self.properties: ProjectProperties = projectProperties + self.repositoryContainer: RepositoryContainer = repositoryContainer + self.database = database diff --git a/app/ProjectManager.py b/app/ProjectManager.py new file mode 100644 index 00000000..3f75dbe8 --- /dev/null +++ b/app/ProjectManager.py @@ -0,0 +1,155 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import ntpath +import os +import sys +from typing import Tuple + +from app.Project import Project, ProjectProperties +from app.tools.ToolCoordinator import fileExists +from app.auxiliary import Wordlist, getTempFolder +from app.shell.Shell import Shell +from app.tools.nmap.NmapPaths import getNmapRunningFolder +from db.RepositoryFactory import RepositoryFactory +from db.SqliteDbAdapter import Database + +tempDirectory = getTempFolder() + + +class ProjectManager: + def __init__(self, shell: Shell, repositoryFactory: RepositoryFactory, logger): + self.shell = shell + self.repositoryFactory = repositoryFactory + self.logger = logger + + def createNewProject(self, projectType: str, isTemp: bool) -> Project: + database = self.__createDatabase() + workingDirectory = self.shell.get_current_working_directory() + + # to store tool output of finished processes + outputFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-tool-output", + directory=tempDirectory) + + # to store tool output of running processes + runningFolder = self.shell.create_temporary_directory(prefix="legion-", suffix="-running", directory=tempDirectory) + + self.shell.create_directory_recursively(f"{outputFolder}/screenshots") # to store screenshots + self.shell.create_directory_recursively(getNmapRunningFolder(runningFolder)) # to store nmap output + self.shell.create_directory_recursively(f"{runningFolder}/hydra") # to store hydra output + self.shell.create_directory_recursively(f"{runningFolder}/dnsmap") # to store dnsmap output + + (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) + repositoryContainer = self.repositoryFactory.buildRepositories(database) + + projectName = database.name + projectProperties = ProjectProperties( + projectName, workingDirectory, projectType, isTemp, outputFolder, runningFolder, usernameWordList, + passwordWordList, storeWordListsOnExit=True + ) + return Project(projectProperties, repositoryContainer, database) + + def openExistingProject(self, projectName: str, projectType: str = "legion") -> Project: + self.logger.info(f"Opening existing project: {projectName}...") + database = self.__createDatabase(projectName) + workingDirectory = f"{ntpath.dirname(projectName)}/" + outputFolder, _ = self.__determineOutputFolder(projectName, projectType) + runningFolder = self.shell.create_temporary_directory(suffix="-running", prefix=projectType + '-', + directory=tempDirectory) + (usernameWordList, passwordWordList) = self.__createUsernameAndPasswordWordLists(outputFolder) + projectProperties = ProjectProperties( + projectName=projectName, workingDirectory=workingDirectory, projectType=projectType, isTemporary=False, + outputFolder=outputFolder, runningFolder=runningFolder, usernamesWordList=usernameWordList, + passwordWordList=passwordWordList, storeWordListsOnExit=True + ) + repositoryContainer = self.repositoryFactory.buildRepositories(database) + return Project(projectProperties, repositoryContainer, database) + + def closeProject(self, project: Project) -> None: + self.logger.info(f"Closing project {project.properties.projectName}...") + # if current project is not temporary & delete wordlists if necessary + projectProperties = project.properties + try: + if not projectProperties.isTemporary: + if not projectProperties.storeWordListsOnExit: + self.logger.info('Removing wordlist files.') + self.shell.remove_file(projectProperties.usernamesWordList.filename) + self.shell.remove_file(projectProperties.passwordWordList.filename) + else: + self.logger.info('Removing temporary files and folders...') + self.shell.remove_file(projectProperties.projectName) + self.shell.remove_directory(projectProperties.outputFolder) + + self.logger.info('Removing running folder at close...') + self.shell.remove_directory(projectProperties.runningFolder) + except: + self.logger.info('Something went wrong removing temporary files and folders..') + self.logger.info("Unexpected error: {0}".format(sys.exc_info()[0])) + + # this function copies the current project files and folder to a new location + # if the replace flag is set to 1, it overwrites the destination file and folder + def saveProjectAs(self, project: Project, fileName: str, replace=0, projectType="legion") -> Project: + self.logger.info(f"Saving project {project.properties.projectName}...") + toolOutputFolder, normalizedFileName = self.__determineOutputFolder(fileName, projectType) + + # check if filename already exists (skip the check if we want to replace the file) + if replace == 0 and fileExists(self.shell, normalizedFileName): + return + + self.shell.copy(source=project.properties.projectName, destination=normalizedFileName) + os.system('cp -r "' + project.properties.outputFolder + '/." "' + toolOutputFolder + '"') + + if project.properties.isTemporary: + self.shell.remove_file(project.properties.projectName) + self.shell.remove_directory(project.properties.outputFolder) + + self.logger.info(f"Project saved as {normalizedFileName}.") + return self.openExistingProject(normalizedFileName, projectType) + + def __createDatabase(self, projectName: str = None) -> Database: + if projectName: + return Database(projectName) + + databaseFile = self.shell.create_named_temporary_file(suffix=".legion", prefix="legion-", directory=tempDirectory, + delete_on_close=False) # to store the db file + return Database(databaseFile.name) + + @staticmethod + def setStoreWordListsOnExit(project: Project, storeWordListsOnExit: bool) -> None: + projectProperties = ProjectProperties( + projectName=project.properties.projectName, workingDirectory=project.properties.workingDirectory, + projectType=project.properties.projectType, isTemporary=project.properties.isTemporary, + outputFolder=project.properties.outputFolder, runningFolder=project.properties.runningFolder, + usernamesWordList=project.properties.usernamesWordList, + passwordWordList=project.properties.passwordWordList, storeWordListsOnExit=storeWordListsOnExit + ) + project.properties = projectProperties + + @staticmethod + def __determineOutputFolder(projectName: str, projectType: str) -> Tuple[str, str]: + nameOffset = len(projectType) + 1 + if not projectName.endswith(projectType): + # use the same name as the file for the folder (without the extension) + return f"{projectName}-tool-output", f"{projectName}.{projectType}" + else: + return f"{projectName[:-nameOffset]}-tool-output", projectName + + @staticmethod + def __createUsernameAndPasswordWordLists(outputFolder: str) -> Tuple[Wordlist, Wordlist]: + usernamesWordlist = Wordlist(f"{outputFolder}/legion-usernames.txt") # to store found usernames + passwordWordlist = Wordlist(f"{outputFolder}/legion-passwords.txt") # to store found passwords + return usernamesWordlist, passwordWordlist diff --git a/app/Screenshooter.py b/app/Screenshooter.py new file mode 100644 index 00000000..7cce0a74 --- /dev/null +++ b/app/Screenshooter.py @@ -0,0 +1,111 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import os + +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +from PyQt6 import QtCore + +from app.logging.legionLog import getAppLogger +from app.http.isHttps import isHttps +from app.timing import getTimestamp +from app.auxiliary import isKali + +logger = getAppLogger() + +class Screenshooter(QtCore.QThread): + done = QtCore.pyqtSignal(str, str, str, name="done") # signal sent after each individual screenshot is taken + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self, timeout): + QtCore.QThread.__init__(self, parent=None) + self.queue = [] + self.processing = False + self.timeout = timeout # screenshooter timeout (ms) + + def tsLog(self, msg): + self.log.emit(str(msg)) + logger.info(msg) + + def addToQueue(self, ip, port, url): + self.queue.append([ip, port, url]) + + # this function should be called when the project is saved/saved as as the tool-output folder changes + def updateOutputFolder(self, screenshotsFolder): + self.outputfolder = screenshotsFolder + + def run(self): + while self.processing == True: + self.sleep(1) # effectively a semaphore + + self.processing = True + + for i in range(0, len(self.queue)): + try: + queueItem = self.queue.pop(0) + ip = queueItem[0] + port = queueItem[1] + url = queueItem[2] + outputfile = getTimestamp() + '-screenshot-' + url.replace(':', '-') + '.png' + self.save(url, ip, port, outputfile) + + except Exception as e: + self.tsLog('Unable to take the screenshot. Error follows.') + self.tsLog(e) + continue + + self.processing = False + + if not len(self.queue) == 0: # if meanwhile queue were added to the queue, start over unless we are in pause mode + self.run() + + def save(self, url, ip, port, outputfile): + # Handle single node URI case by pivot to IP + if len(str(url).split('.')) == 1: + url = '{0}:{1}'.format(str(ip), str(port)) + + if isHttps(ip, port): + url = 'https://{0}'.format(url) + else: + url = 'http://{0}'.format(url) + + self.tsLog('Taking Screenshot of: {0}'.format(str(url))) + + # Use eyewitness under Kali. Use webdriver is not Kali. Once eyewitness is more boradly available, the conter case can be eliminated. + if isKali(): + import tempfile + import subprocess + + tmpOutputfolder = tempfile.mkdtemp(dir=self.outputfolder) + command = ('xvfb-run --server-args="-screen 0:0, 1024x768x24" /usr/bin/eyewitness --single "{url}/"' + ' --no-prompt -d "{outputfolder}"') \ + .format(url=url, outputfolder=tmpOutputfolder) + p = subprocess.Popen(command, shell=True) + p.wait() # wait for command to finish + fileName = os.listdir(tmpOutputfolder + '/screens/')[0] + outputfile = tmpOutputfolder.removeprefix(self.outputfolder) + '/screens/' + fileName + else: + from selenium import webdriver + + driver = webdriver.PhantomJS(executable_path='/usr/bin/phantomjs') + driver.set_window_size(1280, 1024) + driver.get(url) + driver.save_screenshot('{0}/{1}'.format(self.outputfolder, outputfile)) + driver.quit() + self.tsLog('Saving screenshot as: {0}'.format(str(outputfile))) + self.done.emit(ip, port, outputfile) # send a signal to add the 'process' to the DB diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/actions/AbstractObservable.py b/app/actions/AbstractObservable.py new file mode 100644 index 00000000..eaa5b7d1 --- /dev/null +++ b/app/actions/AbstractObservable.py @@ -0,0 +1,31 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC +from typing import List + +from app.actions.AbstractObserver import AbstractObserver + + +class AbstractObservable(ABC): + _observers: List[AbstractObserver] = [] + + def attach(self, observer): + self._observers.append(observer) + + def detach(self, observer): + self._observers.remove(observer) diff --git a/app/actions/AbstractObserver.py b/app/actions/AbstractObserver.py new file mode 100644 index 00000000..2904306e --- /dev/null +++ b/app/actions/AbstractObserver.py @@ -0,0 +1,22 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC + + +class AbstractObserver(ABC): + pass diff --git a/app/actions/updateProgress/AbstractUpdateProgressObservable.py b/app/actions/updateProgress/AbstractUpdateProgressObservable.py new file mode 100644 index 00000000..0bf48461 --- /dev/null +++ b/app/actions/updateProgress/AbstractUpdateProgressObservable.py @@ -0,0 +1,34 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import abstractmethod + +from app.actions.AbstractObservable import AbstractObservable + + +class AbstractUpdateProgressObservable(AbstractObservable): + @abstractmethod + def updateProgress(self, progress): + pass + + @abstractmethod + def start(self): + pass + + @abstractmethod + def finished(self): + pass diff --git a/app/actions/updateProgress/AbstractUpdateProgressObserver.py b/app/actions/updateProgress/AbstractUpdateProgressObserver.py new file mode 100644 index 00000000..5d50bdf7 --- /dev/null +++ b/app/actions/updateProgress/AbstractUpdateProgressObserver.py @@ -0,0 +1,34 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import abstractmethod + +from app.actions.AbstractObserver import AbstractObserver + + +class AbstractUpdateProgressObserver(AbstractObserver): + @abstractmethod + def onProgressUpdate(self, progress) -> None: + pass + + @abstractmethod + def onStart(self) -> None: + pass + + @abstractmethod + def onFinished(self) -> None: + pass diff --git a/app/actions/updateProgress/UpdateProgressObservable.py b/app/actions/updateProgress/UpdateProgressObservable.py new file mode 100644 index 00000000..1ed96f73 --- /dev/null +++ b/app/actions/updateProgress/UpdateProgressObservable.py @@ -0,0 +1,32 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.actions.updateProgress.AbstractUpdateProgressObservable import AbstractUpdateProgressObservable + + +class UpdateProgressObservable(AbstractUpdateProgressObservable): + def finished(self): + for observer in self._observers: + observer.onFinished() + + def start(self): + for observer in self._observers: + observer.onStart() + + def updateProgress(self, progress, title): + for observer in self._observers: + observer.onProgressUpdate(progress, title) diff --git a/app/auxiliary.py b/app/auxiliary.py new file mode 100644 index 00000000..b5cab4df --- /dev/null +++ b/app/auxiliary.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import os, sys, socket, locale, webbrowser, \ + re, platform # for webrequests, screenshot timeouts, timestamps, browser stuff and regex +from PyQt6 import QtCore, QtWidgets +from PyQt6.QtCore import * # for QProcess +from six import u as unicode + +from app.http.isHttps import isHttps +from app.logging.legionLog import getAppLogger +from app.timing import timing + +from PyQt6.QtWidgets import QAbstractItemView +import subprocess + +log = getAppLogger() + +# Convert Windows path to Posix +def winPath2Unix(windowsPath): + windowsPath = windowsPath.replace("\\", "/") + windowsPath = windowsPath.replace("C:", "/mnt/c") + return windowsPath + +# Convert Posix path to Windows +def unixPath2Win(posixPath): + posixPath = posixPath.replace("/", "\\") + posixPath = posixPath.replace("\\mnt\\c", "C:") + return posixPath + +# Check if running in WSL +def isWsl(): + release = str(platform.uname().release).lower() + return "microsoft" in release + +# Check if running in Kali +def isKali(): + release = str(platform.uname().release).lower() + return "kali" in release + +# Get the AppData Temp directory path if WSL +def getAppdataTemp(): + try: + username = os.environ["WSL_USER_NAME"] + except KeyError: + raise Exception("WSL detected but environment variable 'WSL_USER_NAME' is unset. Please run 'export WSL_USER_NAME=' followed by your username as it appears in c:\\Users\\") + + appDataTemp = "C:\\Users\\{0}\\AppData\\Local\\Temp".format(username) + appDataTempUnix = winPath2Unix(appDataTemp) + + if os.path.exists(appDataTempUnix): + return appDataTemp + else: + raise Exception("The AppData Temp directory path {0} does not exist.".format(appDataTemp)) + return path + +# Get the temp folder based on os. Create if missing from *nix +def getTempFolder(): + if isWsl(): + tempPathWin = "{0}\\legion\\tmp".format(getAppdataTemp()) + tempPath = winPath2Unix(tempPathWin) + if not os.path.isdir(os.path.expanduser(tempPath)): + os.makedirs(tempPath) + log.info("WSL is detected. The AppData Temp directory path is {0} ({1})".format(tempPath, tempPathWin)) + else: + tempPath = os.path.expanduser("~/.local/share/legion/tmp") + if not os.path.isdir(tempPath): + os.makedirs(tempPath) + log.info("Non-WSL The AppData Temp directory path is {0}".format(tempPath)) + return tempPath + +def getPid(qprocess): + pid = qprocess.processId() + return pid + +def formatCommandQProcess(inputCommand): + parts = inputCommand.split() + program = parts[0] + arguments = parts[1:] + return program, arguments + +# bubble sort algorithm that sorts an array (in place) based on the values in another array +# the values in the array must be comparable and in the corresponding positions +# used to sort objects by one of their attributes. +@timing +def sortArrayWithArray(array, arrayToSort): + for i in range(0, len(array) - 1): + swap_test = False + for j in range(0, len(array) - i - 1): + if array[j] > array[j + 1]: + array[j], array[j + 1] = array[j + 1], array[j] # swap + arrayToSort[j], arrayToSort[j + 1] = arrayToSort[j + 1], arrayToSort[j] + swap_test = True + if swap_test == False: + break + + +# converts an IP address to an integer (for the sort function) +def IP2Int(ip): + try: + res = 0 + ip = ip.split("/")[0] # bug fix: remove slash if it's a range + o = list(map(int, ip.split('.'))) + res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] + except: + log.error("Input IP {0} is not valid. Passing for now.".format(str(ip))) + pass + return res + + +# used by the settings dialog when a user cancels and the GUI needs to be reset +def clearLayout(layout): + if layout != None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget != None: + widget.deleteLater() + else: + clearLayout(item.layout()) + + +# this function sets a table view's properties +@timing +def setTableProperties(table, headersLen, hiddenColumnIndexes=[]): + table.verticalHeader().setVisible(False) # hide the row headers + table.setShowGrid(False) # hide the table grid + table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # select entire row instead of single cell + table.setSortingEnabled(True) # enable column sorting + table.horizontalHeader().setStretchLastSection(True) # header behaviour + table.horizontalHeader().setSortIndicatorShown(False) # hide sort arrow from column header + table.setWordWrap(False) # row behaviour + table.resizeRowsToContents() + + for i in range(0, headersLen): # reset all the hidden columns + table.setColumnHidden(i, False) + + for i in hiddenColumnIndexes: # hide some columns + table.hideColumn(i) + + table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # create the right-click context menu + + +def checkHydraResults(output): + usernames = [] + passwords = [] + string = '\[[0-9]+\]\[[a-z-]+\].+' # when a password is found, the line contains [port#][plugin-name] + results = re.findall(string, output, re.I) + if results: + for line in results: + login = re.search('(login:[\s]*)([^\s]+)', line) + if login: + log.info('Found username: ' + login.group(2)) + usernames.append(login.group(2)) + password = re.search('(password:[\s]*)([^\s]+)', line) + if password: + # print 'Found password: ' + password.group(2) + + passwords.append(password.group(2)) + return True, usernames, passwords # returns the lists of found usernames and passwords + return False, [], [] + + +# this class is used for example to store found usernames/passwords +class Wordlist(): + def __init__(self, filename): # needs full path + self.filename = filename + self.wordlist = [] + with open(filename, 'a+') as f: # open for appending + reading + self.wordlist = f.readlines() + log.info('Wordlist was created/opened: ' + str(filename)) + + def setFilename(self, filename): + self.filename = filename + + # adds a word to the wordlist (without duplicates) + def add(self, word): + with open(self.filename, 'a') as f: + if not word + '\n' in self.wordlist: + log.info('Adding ' + word + ' to the wordlist..') + self.wordlist.append(word + '\n') + f.write(word + '\n') + + +# Custom QProcess class +class MyQProcess(QProcess): + sigHydra = QtCore.pyqtSignal(QObject, list, list, name="hydra") # signal to indicate Hydra found stuff + + def __init__(self, name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox): + QProcess.__init__(self) + self.id = -1 + self.name = name + self.tabTitle = tabTitle + self.hostIp = hostIp + self.port = port + self.protocol = protocol + self.command = command + self.startTime = startTime + self.outputfile = outputfile + self.display = textbox # has its own display widget to be able to display its output in the GUI + self.elapsed = -1 + + @pyqtSlot() # this slot allows the process to append its output to the display widget + def readStdOutput(self): + output = str(self.readAllStandardOutput()) + self.display.appendPlainText(unicode(output).strip()) + + # check if any usernames/passwords were found (if so emit a signal so that the gui can tell the user about it) + if self.name == 'hydra': + found, userlist, passlist = checkHydraResults(output) + if found: # send the brutewidget object along with lists of found usernames/passwords + self.sigHydra.emit(self.display.parentWidget(), userlist, passlist) + + stderror = str(self.readAllStandardError()) + + if len(stderror) > 0: + self.display.appendPlainText(unicode(stderror).strip()) # append standard error too + + +# browser opener class with queue and semaphores +class BrowserOpener(QtCore.QThread): + done = QtCore.pyqtSignal(name="done") # signals that we are done opening urls in browser + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.urls = [] + self.processing = False + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def addToQueue(self, url): + self.urls.append(url) + + def run(self): + while self.processing == True: + self.sleep(1) # effectively a semaphore + + self.processing = True + for i in range(0, len(self.urls)): + try: + url = self.urls.pop(0) + self.tsLog('Opening url in browser: ' + url) + if isHttps(url.split(':')[0], url.split(':')[1]): + webbrowser.open_new_tab('https://' + url) + else: + webbrowser.open_new_tab('http://' + url) + if i == 0: + # fixes bug in Kali. have to sleep on first url so the next ones don't open a new browser + # instead of adding a new tab + self.sleep(3) + else: + self.sleep(1) # fixes bug when several calls to urllib occur too fast (interrupted system call) + + except: + self.tsLog('Problem while opening url in browser. Moving on..') + continue + + self.processing = False + if not len(self.urls) == 0: # if meanwhile urls were added to the queue, start over + self.run() + else: + self.done.emit() + + +# This class handles what is to be shown in each panel +class Filters(): + def __init__(self): + # host filters + self.checked = True + self.up = True + self.down = False + # port/service filters + self.tcp = True + self.udp = True + self.portopen = True + self.portclosed = False + self.portfiltered = False + self.keywords = [] + + @timing + def apply(self, up, down, checked, portopen, portfiltered, portclosed, tcp, udp, keywords=[]): + self.checked = checked + self.up = up + self.down = down + self.tcp = tcp + self.udp = udp + self.portopen = portopen + self.portclosed = portclosed + self.portfiltered = portfiltered + self.keywords = keywords + + @timing + def setKeywords(self, keywords): + log.info(str(keywords)) + self.keywords = keywords + + @timing + def getFilters(self): + return [self.up, self.down, self.checked, self.portopen, self.portfiltered, self.portclosed, self.tcp, self.udp, + self.keywords] + + @timing + def display(self): + log.info('Filters are:') + log.info('Show checked hosts: ' + str(self.checked)) + log.info('Show up hosts: ' + str(self.up)) + log.info('Show down hosts: ' + str(self.down)) + log.info('Show tcp: ' + str(self.tcp)) + log.info('Show udp: ' + str(self.udp)) + log.info('Show open ports: ' + str(self.portopen)) + log.info('Show closed ports: ' + str(self.portclosed)) + log.info('Show filtered ports: ' + str(self.portfiltered)) + log.info('Keyword search:') + for w in self.keywords: + log.info(w) + + +### VALIDATION FUNCTIONS ### +# TODO: should probably be moved to a new file called test_validation.py + +def validateNmapInput(text): # validate nmap input entered in Add Hosts dialog + if re.search('[^a-zA-Z0-9\.\/\-\s]', text) != None: + return False + return True + + +def validateCommandFormat(text): # used by settings dialog to validate commands + if text != '' and text != ' ': + return True + return False + + +def validateNumeric(text): # only allows numbers + if text.isdigit(): + return True + return False + + +def validateString(text): # only allows alphanumeric characters, '_' and '-' + if text != '' and re.search("[^A-Za-z0-9_-]+", text) == None: + return True + return False + + +def validateStringWithSpace(text): # only allows alphanumeric characters, '_', '-' and space + if text != '' and re.search("[^A-Za-z0-9_() -]+", text) == None: + return True + return False + + +def validateNmapPorts(text): # only allows alphanumeric characters and the following: ./-'"*,:[any kind of space] + if re.search('[^a-zA-Z0-9\.\/\-\'\"\*\,\:\s]', text) != None: + return False + return True diff --git a/app/http/__init__.py b/app/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/http/isHttps.py b/app/http/isHttps.py new file mode 100644 index 00000000..3f4b5192 --- /dev/null +++ b/app/http/isHttps.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import ssl + + +def defaultUserAgent() -> str: + return "Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0" + + +def isHttps(ip, port) -> bool: + from urllib.error import URLError + try: + from urllib.request import Request, urlopen + headers = {"User-Agent": defaultUserAgent()} + req = Request(f"https://{ip}:{port}", headers=headers) + urlopen(req, timeout=5).read() + return True + except URLError as e: + reason = str(e.reason) + print("urlerror" + reason) + if 'Forbidden' in reason or 'certificate verify failed' in reason: + return True + return False + except ssl.CertificateError: + print("ssl") + return True diff --git a/app/importers/NmapImporter.py b/app/importers/NmapImporter.py new file mode 100644 index 00000000..0abf4c19 --- /dev/null +++ b/app/importers/NmapImporter.py @@ -0,0 +1,355 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import sys + +from PyQt6 import QtCore + +from app.actions.updateProgress import AbstractUpdateProgressObservable +from app.logging.legionLog import getAppLogger +from db.entities.host import hostObj +from db.entities.l1script import l1ScriptObj +from db.entities.nmapSession import nmapSessionObj +from db.entities.note import note +from db.entities.os import osObj +from db.entities.port import portObj +from db.entities.service import serviceObj +from db.repositories.HostRepository import HostRepository +from parsers.Parser import parseNmapReport, MalformedXmlDocumentException +from time import time + +appLog = getAppLogger() + +class NmapImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self, updateProgressObservable: AbstractUpdateProgressObservable, hostRepository: HostRepository): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + self.updateProgressObservable = updateProgressObservable + self.hostRepository = hostRepository + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def setDB(self, db): + self.db = db + + def setHostRepository(self, hostRepository: HostRepository): + self.hostRepository = hostRepository + + def setFilename(self, filename): + self.filename = filename + + def setOutput(self, output): + self.output = output + + # it is necessary to get the qprocess because we need to send it back to the scheduler when we're done importing + def run(self): + try: + self.updateProgressObservable.start() + session = self.db.session() + self.tsLog("Parsing nmap xml file: " + self.filename) + startTime = time() + + try: + nmapReport = parseNmapReport(self.filename) + except MalformedXmlDocumentException as e: + self.tsLog('Giving up on import due to previous errors.') + appLog.error(f"NMAP xml report is likely malformed: {e}") + self.updateProgressObservable.finished() + self.done.emit() + return + + self.tsLog('nmap xml report read successfully!') + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + s = nmapReport.getSession() # nmap session info + if s: + n = nmapSessionObj(self.filename, s.startTime, s.finish_time, s.nmapVersion, s.scanArgs, s.totalHosts, + s.upHosts, s.downHosts) + session.add(n) + session.commit() + + allHosts = nmapReport.getAllHosts() + hostCount = len(allHosts) + if hostCount == 0: # to fix a division by zero if we ran nmap on one host + hostCount = 1 + + createProgress = 0 + createOsNodesProgress = 0 + createPortsProgress = 0 + createDbScriptsProgress = 0 + updateObjectsRunScriptsProgress = 0 + + self.updateProgressObservable.updateProgress(int(createProgress), 'Adding hosts...') + + for h in allHosts: # create all the hosts that need to be created + db_host = self.hostRepository.getHostInformation(h.ip) + + if not db_host: # if host doesn't exist in DB, create it first + hid = hostObj(osMatch='', osAccuracy='', ip=h.ip, ipv4=h.ipv4, ipv6=h.ipv6, macaddr=h.macaddr, + status=h.status, hostname=h.hostname, vendor=h.vendor, uptime=h.uptime, + lastboot=h.lastboot, distance=h.distance, state=h.state, count=h.count) + self.tsLog("Adding db_host") + session.add(hid) + session.commit() + t_note = note(h.ip, 'Added by nmap') + session.add(t_note) + session.commit() + else: + self.tsLog("Found db_host already in db") + + createProgress = createProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(int(createProgress), 'Adding hosts...') + + self.updateProgressObservable.updateProgress(int(createOsNodesProgress), 'Creating Service, Port and OS children...') + + for h in allHosts: # create all OS, service and port objects that need to be created + self.tsLog("Processing h {ip}".format(ip=h.ip)) + + db_host = self.hostRepository.getHostInformation(h.ip) + if db_host: + self.tsLog("Found db_host during os/ports/service processing") + else: + self.tsLog("Did not find db_host during os/ports/service processing") + self.tsLog("A host that should have been found was not. Something is wrong. Save your session and report a bug.") + self.tsLog("Include your nmap file, sanitized if needed.") + continue + + os_nodes = h.getOs() # parse and store all the OS nodes + self.tsLog(" 'os_nodes' to process: {os_nodes}".format(os_nodes=str(len(os_nodes)))) + for os in os_nodes: + self.tsLog(" Processing os obj {os}".format(os=str(os.name))) + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name).filter_by( + family=os.family).filter_by(generation=os.generation).filter_by(osType=os.osType).filter_by( + vendor=os.vendor).first() + + if not db_os: + t_osObj = osObj(os.name, os.family, os.generation, os.osType, os.vendor, os.accuracy, + db_host.id) + session.add(t_osObj) + session.commit() + + createOsNodesProgress = createOsNodesProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(int(createOsNodesProgress), 'Creating Service, Port and OS children...') + + self.updateProgressObservable.updateProgress(int(createPortsProgress), 'Processing ports...') + + all_ports = h.all_ports() + portCount = len(all_ports) + self.tsLog(" 'ports' to process: {all_ports}".format(all_ports=str(len(all_ports)))) + for p in all_ports: # parse the ports + self.tsLog(" Processing port obj {port}".format(port=str(p.portId))) + s = p.getService() + + if not (s is None): # check if service already exists to avoid adding duplicates + self.tsLog(" Processing service result *********** name={0} prod={1} ver={2} extra={3} fing={4}" + .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ + .filter_by(name=s.name).filter_by(product=s.product).filter_by(version=s.version) \ + .filter_by(extrainfo=s.extrainfo).filter_by(fingerprint=s.fingerprint).first() + if not db_service: + self.tsLog(" Did not find service *********** name={0} prod={1} ver={2} extra={3} fing={4}" + .format(s.name, s.product, s.version, s.extrainfo, s.fingerprint)) + db_service = serviceObj(s.name, db_host.id, s.product, s.version, s.extrainfo, + s.fingerprint) + session.add(db_service) + session.commit() + else: # else, there is no service info to parse + db_service = None + # fetch the port + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId) \ + .filter_by(protocol=p.protocol).first() + + if not db_port: + self.tsLog(" Did not find port *********** portid={0} proto={1}".format(p.portId, p.protocol)) + if db_service: + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, db_service.id) + else: + db_port = portObj(p.portId, p.protocol, p.state, db_host.id, '') + session.add(db_port) + session.commit() + createPortsProgress = createPortsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(createPortsProgress, 'Processing ports...') + + self.updateProgressObservable.updateProgress(createDbScriptsProgress, 'Creating script objects...') + + for h in allHosts: # create all script objects that need to be created + db_host = self.hostRepository.getHostInformation(h.ip) + + for p in h.all_ports(): + for scr in p.getScripts(): + self.tsLog(" Processing script obj {scr}".format(scr=str(scr))) + db_port = session.query(portObj).filter_by(hostId=db_host.id) \ + .filter_by(portId=p.portId).filter_by(protocol=p.protocol).first() + # Todo + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ + .filter_by(portId=db_port.id).first() + # end todo + db_script = session.query(l1ScriptObj).filter_by(hostId=db_host.id) \ + .filter_by(portId=db_port.id).first() + + if not db_script: # if this script object doesn't exist, create it + t_l1ScriptObj = l1ScriptObj(scr.scriptId, scr.output, db_port.id, db_host.id) + self.tsLog(" Adding l1ScriptObj obj {script}".format(script=scr.scriptId)) + session.add(t_l1ScriptObj) + session.commit() + for hs in h.getHostScripts(): + db_script = session.query(l1ScriptObj).filter_by(scriptId=hs.scriptId) \ + .filter_by(hostId=db_host.id).first() + if not db_script: + t_l1ScriptObj = l1ScriptObj(hs.scriptId, hs.output, None, db_host.id) + session.add(t_l1ScriptObj) + session.commit() + + createDbScriptsProgress = createDbScriptsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(createDbScriptsProgress, 'Creating script objects...') + + self.updateProgressObservable.updateProgress(updateObjectsRunScriptsProgress, 'Update objects and run scripts...') + + for h in allHosts: # update everything + + db_host = self.hostRepository.getHostInformation(h.ip) + if not db_host: + self.tsLog(" A host that should have been found was not. Something is wrong. Save your session and report a bug.") + self.tsLog(" Include your nmap file, sanitized if needed.") + + if db_host.ipv4 == '' and not h.ipv4 == '': + db_host.ipv4 = h.ipv4 + if db_host.ipv6 == '' and not h.ipv6 == '': + db_host.ipv6 = h.ipv6 + if db_host.macaddr == '' and not h.macaddr == '': + db_host.macaddr = h.macaddr + if not h.status == '': + db_host.status = h.status + if db_host.hostname == '' and not h.hostname == '': + db_host.hostname = h.hostname + if db_host.vendor == '' and not h.vendor == '': + db_host.vendor = h.vendor + if db_host.uptime == '' and not h.uptime == '': + db_host.uptime = h.uptime + if db_host.lastboot == '' and not h.lastboot == '': + db_host.lastboot = h.lastboot + if db_host.distance == '' and not h.distance == '': + db_host.distance = h.distance + if db_host.state == '' and not h.state == '': + db_host.state = h.state + if db_host.count == '' and not h.count == '': + db_host.count = h.count + + session.add(db_host) + session.commit() + + tmp_name = '' + tmp_accuracy = '0' # TODO: check if better to convert to int for comparison + + os_nodes = h.getOs() + for os in os_nodes: + db_os = session.query(osObj).filter_by(hostId=db_host.id).filter_by(name=os.name) \ + .filter_by(family=os.family).filter_by(generation=os.generation) \ + .filter_by(osType=os.osType).filter_by(vendor=os.vendor).first() + + db_os.osAccuracy = os.accuracy # update the accuracy + + # get the most accurate OS match/accuracy to store it in the host table for easier access + if not os.name == '': + if os.accuracy > tmp_accuracy: + tmp_name = os.name + tmp_accuracy = os.accuracy + + if os_nodes: # if there was operating system info to parse + # update the current host with the most accurate OS match + if not tmp_name == '' and not tmp_accuracy == '0': + db_host.osMatch = tmp_name + db_host.osAccuracy = tmp_accuracy + + session.add(db_host) + session.commit() + + for scr in h.getHostScripts(): + self.tsLog("-----------------------Host SCR: {0}".format(scr.scriptId)) + db_host = self.hostRepository.getHostInformation(h.ip) + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) + session.commit() + + for scr in h.getScripts(): + self.tsLog("-----------------------SCR: {0}".format(scr.scriptId)) + db_host = self.hostRepository.getHostInformation(h.ip) + scrProcessorResults = scr.scriptSelector(db_host) + for scrProcessorResult in scrProcessorResults: + session.add(scrProcessorResult) + session.commit() + + for p in h.all_ports(): + s = p.getService() + if not (s is None): + db_service = session.query(serviceObj).filter_by(hostId=db_host.id) \ + .filter_by(name=s.name).filter_by(product=s.product) \ + .filter_by(version=s.version).filter_by(extrainfo=s.extrainfo) \ + .filter_by(fingerprint=s.fingerprint).first() + else: + db_service = None + # fetch the port + db_port = session.query(portObj).filter_by(hostId=db_host.id).filter_by(portId=p.portId) \ + .filter_by(protocol=p.protocol).first() + if db_port: + if db_port.state != p.state: + db_port.state = p.state + session.add(db_port) + session.commit() + # if there is some new service information, update it -- might be causing issue 164 + if not (db_service is None) and db_port.serviceId != db_service.id: + db_port.serviceId = db_service.id + session.add(db_port) + session.commit() + # store the script results (note that existing script outputs are also kept) + for scr in p.getScripts(): + db_script = session.query(l1ScriptObj).filter_by(scriptId=scr.scriptId) \ + .filter_by(portId=db_port.id).first() + + if not scr.output == '' and scr.output != None: + db_script.output = scr.output + + session.add(db_script) + session.commit() + + updateObjectsRunScriptsProgress = updateObjectsRunScriptsProgress + (100.0 / hostCount) + self.updateProgressObservable.updateProgress(updateObjectsRunScriptsProgress, 'Update objects and run scripts...') + + self.updateProgressObservable.updateProgress(100, 'Almost done...') + + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + self.tsLog(f"Finished in {str(time() - startTime)} seconds.") + self.updateProgressObservable.finished() + self.done.emit() + + # call the scheduler (if there is no terminal output it means we imported nmap) + self.schedule.emit(nmapReport, self.output == '') + + except Exception as e: + self.tsLog('Something went wrong when parsing the nmap file..') + self.tsLog("Unexpected error: {0}".format(sys.exc_info()[0])) + self.tsLog(e) + self.updateProgressObservable.finished() + self.done.emit() + raise diff --git a/app/importers/PythonImporter.py b/app/importers/PythonImporter.py new file mode 100644 index 00000000..fdca084a --- /dev/null +++ b/app/importers/PythonImporter.py @@ -0,0 +1,71 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from PyQt6 import QtCore + +from db.entities.host import hostObj +from scripts.python import pyShodan, macvendors +from time import time + + +class PythonImporter(QtCore.QThread): + tick = QtCore.pyqtSignal(int, name="changed") # New style signal + done = QtCore.pyqtSignal(name="done") # New style signal + schedule = QtCore.pyqtSignal(object, bool, name="schedule") # New style signal + log = QtCore.pyqtSignal(str, name="log") + + def __init__(self): + QtCore.QThread.__init__(self, parent=None) + self.output = '' + self.hostIp = '' + self.pythonScriptDispatch = {'pyShodan': pyShodan.PyShodanScript(), 'macvendors': macvendors.macvendorsScript()} + self.pythonScriptObj = None + + def tsLog(self, msg): + self.log.emit(str(msg)) + + def setDB(self, db): + self.db = db + + def setHostIp(self, hostIp): + self.hostIp = hostIp + + def setPythonScript(self, pythonScript): + self.pythonScriptObj = self.pythonScriptDispatch[pythonScript] + + def setOutput(self, output): + self.output = output + + def run(self): # it is necessary to get the qprocess to send it back to the scheduler when we're done + try: + session = self.db.session() + startTime = time() + self.db.dbsemaphore.acquire() # ensure that while this thread is running, no one else can write to the DB + #self.setPythonScript(self.pythonScript) + db_host = session.query(hostObj).filter_by(ip = self.hostIp).first() + self.pythonScriptObj.setDbHost(db_host) + self.pythonScriptObj.setSession(session) + self.pythonScriptObj.run() + session.commit() + self.db.dbsemaphore.release() # we are done with the DB + self.tsLog('Finished in ' + str(time() - startTime) + ' seconds.') + self.done.emit() + + except Exception as e: + self.tsLog(e) + raise + self.done.emit() diff --git a/app/importers/__init__.py b/app/importers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/legionLogo.txt b/app/legionLogo.txt new file mode 100755 index 00000000..49620872 --- /dev/null +++ b/app/legionLogo.txt @@ -0,0 +1,54 @@ +  +  Xt:.    +  ;@t    +   8 S:  +  S X@.   :%8888XXXXSS8@X%:.   +  :X@%  .%88XX888@888@@8888@@@8%:.   +  .8 @:  ;88@@88XSX%SX%XSXX%tSX888:.   +  .:8:Xt.  :X8@X88XX8S%XS%%%%%X%%%@XSt.   +  t.@@:8  .88X8@SSXXS%S%t%%%SS%tS%XS%;..    + .8.88:8   8@S@XXX%%%S%tS%%%%Xt%%%XX;..   + X.888;;  X@XXXSSSS%%tS%t%%S%t%%SX%;:.    + .8t8S8@8;.  ;8X@XXX%XtSt%tS%%%%S%SS@t:..   + .@%8X 8t.   888XSX%@%%%S%%%S%SS%%%@;:..   + :8S88;XSt.   .8@@X@SS%S%%%S%S@SX%X@Xt:.    + .X@;S% 88    ;88@@SX%S%S%@X%%ttt%%%;:.    + ;@8StS.@:.     %88X8X%S@S@St%S;%Xt%8t;..     +  ;X@@8@:.   .S888SS@S%%%SSS@S%88X8t:. .tt..  +  .t@8@..   .%888XX%X%%X88  t88888@8@8%88.  +   :%@8:     .;88@SX%%%X t888%tStt%SS8X %@%:  +  .;X8.   :88X@%XSX8t88%%t%XtXtt@;@ .SX   +   .;X%;.   .@888@%8@t;8S@S@;%@%@Stt@@@;8    +   :t%8X.     t8@@S@@8SXX;t%t%X;tt%t%t8tS%   + .%St%t.   .;888@888S88tS;t%;%%%tXSXSXt8.  ... ... ..   + .%tSt%:.    :@8@X8S@:X8@%St%%%%%%;%S@:@ t. .%@%X;t;8S8t:.  ;8%;@tt;8 .tXX t8.   t%S:; t888X;:@:S. ;8X ;t;S;@@:. tS . %88X::@tS:.  @X.%t;t@8.   .X@%8tSS8t.  +  .;StX::    t@8888t..8S;tSt%%%Xt%t%S@%@:. .. @. @: ..  .  . .S;   X8X: 8tS;S 8: .  ;%@@;.. 88 @8. . .8;S; S;  :88X.tS: ..8S S @:   .. S ...  + .:tS%:.    .t@88Xt. ;%S;tStX;;%X;X;X@t:.   .t .%.    .8. ;@.   .S ;. 88 :S:. .@%8. 8% SS. X@ .8%    . :S@.:8t .tX %. ;S.  :@ @.  +  :t%X;.     .:8888t  .;S;t%ttt%Xtt%%X8St:. .S.. 8t.     .8. %8    :t. X8 t8;.  .;:. .X8 :S. @8: tX.     8S ;:S. :%.: S 88.  :X:@   +  .%SS;.    :88XS. ...%X%tSXt%%StX;88t;.   .8% .t.   .8 ;8  @:@: :  S8     8S .SS. @8. @8;    t. 8   ;8;X:t8S:;:8 :8.@   +  .;tX:.   88;. .t@tS%%X%St%%%88t:.   .@% %t   :S :@@8@S.t8: 88 88:     .@8 :% .S; SS:   .8 .8;  :@:X.. X8 %S.  :X:@    +   .;;;:.   .;S8888SXS8XSXStt%St @tX@S.    @% %t   :: :@@888%X8 8 S@;    @S SX. S; tS.   :8. 8;  :8:@ .@ ;.t%@::8.X.  +  ;tt.  .X8X888@X888888StS@X8:8  :@;.  .St %t :8: t8. .@%t..%S ;S;. .8SSX%tt8t@: .@8 .S  8S. @8;    ;X ;X:  :@:X .: @ : ;8S@ @   +  .tt;:.  t8888888888@888888%@8tX@S8 8;t.   .t; %t  .  t@ .::. ;8. .@8:  %;t %;. 8S %t .:. t;.    8X :8  ;8;X :8:. 8t.@  +  .tt%.   .888@@@XS%S@88888@88@88t8:t%88X8.   .. tt :  ;.;X :8 8S..X:. 8X .SSt :%.  .8@ .X  :8 ..%8   %  S8.   :S:X .;88 .;8.   +   .;t:..  tX888%%tttS%%t;@%S8@88:88.SS:Xt8@ ;@. 8;   % :;.t. :S: X8t:..;8:;:t%;8;:  ;  ;.%t   X%. @8:  .::8;SS%@:.  .88S.S t:  ;S 8   t ; .S.  +   ;;t.   :X@8@SXS;%%%%St;%88888S;888SX:S@.X:  .X% X.8:tS:8@:X..88 ;@ tXSX  %.  :8.S @:;.8;8;tS. ..X. X8:  8%X:88:; ;%t.;:.: :8@.8t:  :;S@;8   +   :t:.  S888t8ttt%t%St%%St8@X8.X 888@@;tSS@ .S@St.  ..:. .t@8St.  .;;; S.    X@:: X@;: .  ;8@%.  ;X8@;. ::t@;   :XX:  :8@Xt..%8@. .. 8X8.  +  :;;.   S@X8@@;t%%%%St%X;;%tXX8%t 8@%;8S8;8X8:   ::::::.  .:....   :::::. .     ... . ... ... . . ..   ...    .   ..  +  :;:.  @88S Xtt%%%XtStttt%t%S%S8X8X8t.;%SS@88%.  ...      ...                           +  .;:.   .X8t@@%Xtt%%tt;ttt%%%%tSXt@8:t;@;888;@;8:                           +   .;;..   8S%@88@%%SS@;%t%%%%%%%%%tt%%888.:S8X:%88:.                     + .;;:.    8t8.8SStX%@%tSt%%%%t%X;%%%%%%S8888888%88S:.                      + :t;. ::88@%%%t%X%SX%S;%ttXt;t%SS%Xt8X888.S@@@t%...                    + .;;.. .XXSt8t%S;XtXS@%%%%X%%t;t%Sttt%Xt88; 8%X@8%%S%%t%;::.                   + .t;.. ;::8:8@8Xtt%XSX@@X8XSX%t%t%t;t%%%t%tX@tSX8tSS%8S%@tX%St;;.          +  .;;: :8@8@S8888tSSX8888@SS8%%%%%tt%%Xt%@tXt88S@:@8@StXtXSXt%XtSt;.           +  .;t.; @8X%t8888X@88888@88888t%St%%tttXt%@tX@%:t8@SSSStS%X%@t8%tSX;:.        +  .;;@.;88t@@X8@t888888888@XS@SXttttt%%tS@%XSS X%8 @@tSXS%StSSttXSSX%t;.     +  .;; @8@SXS88@S8X8888888@X8@88SX%%%%%%tt@8%S@S @8:@%Xt%SStSS%X%XtXtSX;:.      +  .;%@%8@t%X8@@8888888@88@XSS888X%t%%%St%@S@SX8S8888@XX@tSX%@tt%%S%Xt%St:.    +  .;%;88S%8@@8@X88888888XX8@X88X%Stt%t%%%X8X 8;: ::8SX@@8888%X%X%%%S%%%Xt;:    +  .;SX88tX@8S8X88@@88@@X@88S8888S%%S;%%St%t8%@X88@8@%XXSttX8@tX%XSStSXt%Xt:.     +   .;t8%%X88@8@X@888XXX@8X8X88%@X%XtttS;tX%@8@t%8@SS@8SXX%@%tt@8SStSXtSX%Xtt;.  +   .t%S@@88@;XX8X8XXX88888X88%XSSSX%t%t%%tSX@@X8t88 8SXt%S%X%%%t%S%%SXtt88SS;.   +   .;t8888@;.XSS88@8@S8@@S8888XXX%Stt%X%XXS8@8X8888:8Xt%SX%%S%%Xt%S%StX%%%StS:.   diff --git a/app/logging/legionLog.py b/app/logging/legionLog.py new file mode 100644 index 00000000..2932800a --- /dev/null +++ b/app/logging/legionLog.py @@ -0,0 +1,72 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import os +import logging +from logging import Logger + +cachedAppLogger = None +cachedStartupLogger = None +cachedDbLogger = None + +cache_path = os.path.expanduser("~/.cache/legion/log") +log_path = os.path.join(cache_path, 'legion.log') +if not os.path.isfile(log_path): + if not os.path.isdir(cache_path): + os.makedirs(cache_path) + +def getStartupLogger() -> Logger: + global cachedStartupLogger + logger = getOrCreateCachedLogger("legion-startup", + os.path.expanduser("~/.cache/legion/log/legion-startup.log"), True, cachedStartupLogger) + cachedStartupLogger = logger + return logger + + +def getAppLogger() -> Logger: + global cachedAppLogger + logger = getOrCreateCachedLogger("legion", + os.path.expanduser("~/.cache/legion/log/legion.log"), True, cachedAppLogger) + cachedAppLogger = logger + return logger + + +def getDbLogger() -> Logger: + global cachedDbLogger + logger = getOrCreateCachedLogger("legion-db", + os.path.expanduser("~/.cache/legion/log/legion-db.log"), False, cachedDbLogger) + cachedDbLogger = logger + return logger + + +def getOrCreateCachedLogger(logName: str, logPath: str, console: bool, cachedLogger): + if cachedLogger: + return cachedLogger + + import logging + from rich.logging import RichHandler + + logging.basicConfig( + level="INFO", + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)] + ) + + log = logging.getLogger("rich") + log.setLevel(logging.INFO) + return log diff --git a/app/logic.py b/app/logic.py new file mode 100644 index 00000000..3ea59c70 --- /dev/null +++ b/app/logic.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import ntpath +import shutil + +from app.Project import Project +from app.tools.ToolCoordinator import ToolCoordinator +from app.shell.Shell import Shell +from app.tools.nmap.NmapPaths import getNmapOutputFolder +from ui.ancillaryDialog import * + +log = getAppLogger() + +class Logic: + def __init__(self, shell: Shell, projectManager, toolCoordinator: ToolCoordinator): + self.projectManager = projectManager + self.activeProject: Project = None + self.toolCoordinator = toolCoordinator + self.shell = shell + + def createFolderForTool(self, tool): + if 'nmap' in tool: + tool = 'nmap' + path = self.activeProject.properties.runningFolder + '/' + re.sub("[^0-9a-zA-Z]", "", str(tool)) + if not os.path.exists(path): + os.makedirs(path) + + # this flag is matched to the conf file setting, so that we know if we need + # to delete the found usernames/passwords wordlists on exit + def setStoreWordlistsOnExit(self, flag=True): + self.storeWordlists = flag + + def copyNmapXMLToOutputFolder(self, file): + outputFolder = self.activeProject.properties.outputFolder + try: + path = getNmapOutputFolder(outputFolder) + ntpath.basename(str(file)) + if not os.path.exists(path): + os.makedirs(path) + + shutil.copy(str(file), path) # will overwrite if file already exists + except: + log.info('Something went wrong copying the imported XML to the project folder.') + log.info("Unexpected error: {0}".format(sys.exc_info()[0])) + + def createNewTemporaryProject(self) -> None: + self.activeProject = self.projectManager.createNewProject(projectType="legion", isTemp=True) + + def openExistingProject(self, filename, projectType="legion") -> None: + self.activeProject = self.projectManager.openExistingProject(projectName=filename, projectType=projectType) + + def saveProjectAs(self, filename, replace=0, projectType='legion') -> bool: + self.activeProject = self.projectManager.saveProjectAs(self.activeProject, filename, replace, projectType) + return True diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 00000000..c4a7083d --- /dev/null +++ b/app/settings.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import shutil + +from app.auxiliary import * # for timestamp + + +# this class reads and writes application settings +from app.timing import getTimestamp + +log = getAppLogger() + +class AppSettings(): + def __init__(self): + # check if settings file exists and creates it if it doesn't + if not os.path.exists(os.path.expanduser('~/.local/share/legion/legion.conf')): + if not os.path.isdir(os.path.expanduser("~/.local/share/legion")): + os.makedirs(os.path.expanduser("~/.local/share/legion")) + shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) + log.info('Loading settings file..') + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.Format.NativeFormat) + + def getGeneralSettings(self): + return self.getSettingsByGroup("GeneralSettings") + + def getBruteSettings(self): + return self.getSettingsByGroup("BruteSettings") + + def getStagedNmapSettings(self): + return self.getSettingsByGroup('StagedNmapSettings') + + def getToolSettings(self): + return self.getSettingsByGroup('ToolSettings') + + def getGUISettings(self): + return self.getSettingsByGroup('GUISettings') + + def getHostActions(self): + self.actions.beginGroup('HostActions') + hostactions = [] + sortArray = [] + keys = self.actions.childKeys() + for k in keys: + hostactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, hostactions) # sort by label so that it appears nicely in the context menu + return hostactions + + # this function fetches all the host actions from the settings file + def getPortActions(self): + self.actions.beginGroup('PortActions') + portactions = [] + sortArray = [] + keys = self.actions.childKeys() + for k in keys: + portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + return portactions + + # this function fetches all the port actions from the settings file + def getPortTerminalActions(self): + self.actions.beginGroup('PortTerminalActions') + portactions = [] + sortArray = [] + keys = self.actions.childKeys() + for k in keys: + portactions.append([self.actions.value(k)[0], str(k), self.actions.value(k)[1], self.actions.value(k)[2]]) + sortArray.append(self.actions.value(k)[0]) + self.actions.endGroup() + sortArrayWithArray(sortArray, portactions) # sort by label so that it appears nicely in the context menu + return portactions + + # this function fetches all the port actions that will be run as terminal commands from the settings file + def getSchedulerSettings(self): + settings = [] + self.actions.beginGroup('SchedulerSettings') + keys = self.actions.childKeys() + for k in keys: + settings.append([str(k), self.actions.value(k)[0], self.actions.value(k)[1]]) + self.actions.endGroup() + return settings + + def getSettingsByGroup(self, name: str) -> dict: + self.actions.beginGroup(name) + settings = dict() + keys = self.actions.childKeys() + for k in keys: + settings.update({str(k): str(self.actions.value(k))}) + self.actions.endGroup() + log.debug("getSettingsByGroup name:{0}, result:{1}".format(str(name), str(settings))) + return settings + + def backupAndSave(self, newSettings, saveBackup=True): + # Backup and save + if saveBackup: + log.info('Backing up old settings and saving new settings...') + os.rename(os.path.expanduser('~/.local/share/legion/legion.conf'), os.path.expanduser("~/.local/share/legion/backup/") + getTimestamp() + '-legion.conf') + else: + log.info('Saving config...') + + self.actions = QtCore.QSettings(os.path.expanduser('~/.local/share/legion/legion.conf'), QtCore.QSettings.Format.NativeFormat) + + self.actions.beginGroup('GeneralSettings') + self.actions.setValue('default-terminal', newSettings.general_default_terminal) + self.actions.setValue('tool-output-black-background', newSettings.general_tool_output_black_background) + self.actions.setValue('screenshooter-timeout', newSettings.general_screenshooter_timeout) + self.actions.setValue('web-services', newSettings.general_web_services) + self.actions.setValue('enable-scheduler', newSettings.general_enable_scheduler) + self.actions.setValue('enable-scheduler-on-import', newSettings.general_enable_scheduler_on_import) + self.actions.setValue('max-fast-processes', newSettings.general_max_fast_processes) + self.actions.setValue('max-slow-processes', newSettings.general_max_slow_processes) + self.actions.endGroup() + + self.actions.beginGroup('BruteSettings') + self.actions.setValue('store-cleartext-passwords-on-exit', newSettings.brute_store_cleartext_passwords_on_exit) + self.actions.setValue('username-wordlist-path', newSettings.brute_username_wordlist_path) + self.actions.setValue('password-wordlist-path', newSettings.brute_password_wordlist_path) + self.actions.setValue('default-username', newSettings.brute_default_username) + self.actions.setValue('default-password', newSettings.brute_default_password) + self.actions.setValue('services', newSettings.brute_services) + self.actions.setValue('no-username-services', newSettings.brute_no_username_services) + self.actions.setValue('no-password-services', newSettings.brute_no_password_services) + self.actions.endGroup() + + self.actions.beginGroup('ToolSettings') + self.actions.setValue('nmap-path', newSettings.tools_path_nmap) + self.actions.setValue('hydra-path', newSettings.tools_path_hydra) + self.actions.setValue('cutycapt-path', newSettings.tools_path_cutycapt) + self.actions.setValue('texteditor-path', newSettings.tools_path_texteditor) + self.actions.setValue('pyshodan-api-key', newSettings.tools_pyshodan_api_key) + self.actions.endGroup() + + self.actions.beginGroup('StagedNmapSettings') + self.actions.setValue('stage1-ports', newSettings.tools_nmap_stage1_ports) + self.actions.setValue('stage2-ports', newSettings.tools_nmap_stage2_ports) + self.actions.setValue('stage3-ports', newSettings.tools_nmap_stage3_ports) + self.actions.setValue('stage4-ports', newSettings.tools_nmap_stage4_ports) + self.actions.setValue('stage5-ports', newSettings.tools_nmap_stage5_ports) + self.actions.setValue('stage6-ports', newSettings.tools_nmap_stage6_ports) + self.actions.endGroup() + + self.actions.beginGroup('GUISettings') + self.actions.setValue('process-tab-column-widths', newSettings.gui_process_tab_column_widths) + self.actions.setValue('process-tab-detail', newSettings.gui_process_tab_detail) + self.actions.endGroup() + + self.actions.beginGroup('HostActions') + for a in newSettings.hostActions: + self.actions.setValue(a[1], [a[0], a[2]]) + self.actions.endGroup() + + self.actions.beginGroup('PortActions') + for a in newSettings.portActions: + self.actions.setValue(a[1], [a[0], a[2], a[3]]) + self.actions.endGroup() + + self.actions.beginGroup('PortTerminalActions') + for a in newSettings.portTerminalActions: + self.actions.setValue(a[1], [a[0], a[2], a[3]]) + self.actions.endGroup() + + self.actions.beginGroup('SchedulerSettings') + for tool in newSettings.automatedAttacks: + self.actions.setValue(tool[0], [tool[1], tool[2]]) + self.actions.endGroup() + + self.actions.sync() + + +# This class first sets all the default settings and +# then overwrites them with the settings found in the configuration file +class Settings(): + def __init__(self, appSettings=None): + + # general + self.general_default_terminal = "gnome-terminal" + self.general_tool_output_black_background = "False" + self.general_screenshooter_timeout = "15000" + self.general_web_services = "http,https,ssl,soap,http-proxy,http-alt,https-alt" + self.general_enable_scheduler = "True" + self.general_max_fast_processes = "10" + self.general_max_slow_processes = "10" + + # brute + self.brute_store_cleartext_passwords_on_exit = "True" + self.brute_username_wordlist_path = "/usr/share/wordlists/" + self.brute_password_wordlist_path = "/usr/share/wordlists/" + self.brute_default_username = "root" + self.brute_default_password = "password" + self.brute_services = "asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get," + \ + "https-head,https-get,http-get-form,http-post-form,https-get-form," + \ + "https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s," + \ + "ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s," + \ + "mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s," + \ + "postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5," + \ + "ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" + self.brute_no_username_services = "cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" + self.brute_no_password_services = "oracle-sid,rsh,smtp-enum" + + # tools + self.tools_nmap_stage1_ports = "T:80,443" + self.tools_nmap_stage2_ports = "T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" + self.tools_nmap_stage3_ports = "Vulners,CVE" + self.tools_nmap_stage4_ports = "T:23,21,22,110,111,2049,3389,8080,U:500,5060" + self.tools_nmap_stage5_ports = "T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048," + \ + "2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" + self.tools_nmap_stage6_ports = "T:30000-65535" + + self.tools_path_nmap = "/sbin/nmap" + self.tools_path_hydra = "/usr/bin/hydra" + self.tools_path_cutycapt = "/usr/bin/cutycapt" + self.tools_path_texteditor = "/usr/bin/xdg-open" + self.tools_pyshodan_api_key = "" + + # GUI settings + self.gui_process_tab_column_widths = "125,0,100,150,100,100,100,100,100,100,100,100,100,100,100,100,100" + self.gui_process_tab_detail = False + + self.hostActions = [] + self.portActions = [] + self.portTerminalActions = [] + self.stagedNmapSettings = [] + self.automatedAttacks = [] + + # now that all defaults are set, overwrite with whatever was in the .conf file (stored in appSettings) + if appSettings: + try: + self.generalSettings = appSettings.getGeneralSettings() + self.bruteSettings = appSettings.getBruteSettings() + self.stagedNmapSettings = appSettings.getStagedNmapSettings() + self.toolSettings = appSettings.getToolSettings() + self.guiSettings = appSettings.getGUISettings() + self.hostActions = appSettings.getHostActions() + self.portActions = appSettings.getPortActions() + self.portTerminalActions = appSettings.getPortTerminalActions() + self.automatedAttacks = appSettings.getSchedulerSettings() + + # general + self.general_default_terminal = self.generalSettings['default-terminal'] + self.general_tool_output_black_background = self.generalSettings['tool-output-black-background'] + self.general_screenshooter_timeout = self.generalSettings['screenshooter-timeout'] + self.general_web_services = self.generalSettings['web-services'] + self.general_enable_scheduler = self.generalSettings['enable-scheduler'] + self.general_enable_scheduler_on_import = self.generalSettings['enable-scheduler-on-import'] + self.general_max_fast_processes = self.generalSettings['max-fast-processes'] + self.general_max_slow_processes = self.generalSettings['max-slow-processes'] + + # brute + self.brute_store_cleartext_passwords_on_exit = self.bruteSettings['store-cleartext-passwords-on-exit'] + self.brute_username_wordlist_path = self.bruteSettings['username-wordlist-path'] + self.brute_password_wordlist_path = self.bruteSettings['password-wordlist-path'] + self.brute_default_username = self.bruteSettings['default-username'] + self.brute_default_password = self.bruteSettings['default-password'] + self.brute_services = self.bruteSettings['services'] + self.brute_no_username_services = self.bruteSettings['no-username-services'] + self.brute_no_password_services = self.bruteSettings['no-password-services'] + + # tools + self.tools_nmap_stage1_ports = self.stagedNmapSettings['stage1-ports'] + self.tools_nmap_stage2_ports = self.stagedNmapSettings['stage2-ports'] + self.tools_nmap_stage3_ports = self.stagedNmapSettings['stage3-ports'] + self.tools_nmap_stage4_ports = self.stagedNmapSettings['stage4-ports'] + self.tools_nmap_stage5_ports = self.stagedNmapSettings['stage5-ports'] + self.tools_nmap_stage6_ports = self.stagedNmapSettings['stage6-ports'] + + self.tools_path_nmap = self.toolSettings['nmap-path'] + self.tools_path_hydra = self.toolSettings['hydra-path'] + self.tools_path_cutycapt = self.toolSettings['cutycapt-path'] + self.tools_path_texteditor = self.toolSettings['texteditor-path'] + self.tools_pyshodan_api_key = self.toolSettings['pyshodan-api-key'] + + # gui + self.gui_process_tab_column_widths = self.guiSettings['process-tab-column-widths'] + self.gui_process_tab_detail = self.guiSettings['process-tab-detail'] + + except KeyError as e: + log.info('Something went wrong while loading the configuration file. Falling back to default ' + + 'settings for some settings.') + log.info('Go to the settings menu to fix the issues!') + log.error(str(e)) + + def __eq__(self, other): # returns false if settings objects are different + if type(other) is type(self): + return self.__dict__ == other.__dict__ + return False + + +if __name__ == "__main__": + settings = AppSettings() + s = Settings(settings) + s2 = Settings(settings) + log.info(s == s2) + s2.general_default_terminal = 'whatever' + log.info(s == s2) diff --git a/app/shell/DefaultShell.py b/app/shell/DefaultShell.py new file mode 100644 index 00000000..8d8d98e6 --- /dev/null +++ b/app/shell/DefaultShell.py @@ -0,0 +1,41 @@ +import os +import shutil +import subprocess +import tempfile + +from app.shell.Shell import Shell + + +class DefaultShell(Shell): + def copy(self, source: str, destination: str) -> None: + shutil.copyfile(source, destination) + + def move(self, source: str, destination: str) -> None: + shutil.move(source, destination) + + def directoryOrFileExists(self, path: str) -> bool: + return os.path.exists(path) + + def get_current_working_directory(self) -> str: + return str(subprocess.check_output("echo $PWD", shell=True)[:-1].decode()) + '/' + + def create_directory_recursively(self, directory: str): + os.makedirs(directory) + + def remove_file(self, file_path: str) -> None: + os.remove(file_path) + + def remove_directory(self, directory: str) -> None: + shutil.rmtree(directory) + + def create_temporary_directory(self, prefix: str, suffix: str, directory: str): + return tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=directory) + + def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): + return tempfile.NamedTemporaryFile(prefix=prefix, suffix=suffix, dir=directory, delete=delete_on_close) + + def isDirectory(self, name: str) -> bool: + return os.path.isdir(name) + + def isFile(self, name: str) -> bool: + return os.path.isfile(name) diff --git a/app/shell/Shell.py b/app/shell/Shell.py new file mode 100644 index 00000000..8795858f --- /dev/null +++ b/app/shell/Shell.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod + + +class Shell(ABC): + @abstractmethod + def get_current_working_directory(self) -> str: + pass + + @abstractmethod + def remove_file(self, file_path: str) -> None: + pass + + @abstractmethod + def remove_directory(self, directory: str) -> None: + pass + + @abstractmethod + def create_temporary_directory(self, prefix: str, suffix: str, directory: str): + pass + + @abstractmethod + def create_directory_recursively(self, directory: str): + pass + + @abstractmethod + def create_named_temporary_file(self, prefix: str, suffix: str, directory: str, delete_on_close: bool): + pass + + @abstractmethod + def move(self, source: str, destination: str) -> None: + pass + + @abstractmethod + def copy(self, source: str, destination: str) -> None: + pass + + @abstractmethod + def isDirectory(self, name: str) -> bool: + pass + + @abstractmethod + def isFile(self, name: str) -> bool: + pass + + @abstractmethod + def directoryOrFileExists(self, path: str) -> bool: + pass diff --git a/app/timing.py b/app/timing.py new file mode 100644 index 00000000..23eea809 --- /dev/null +++ b/app/timing.py @@ -0,0 +1,47 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from datetime import datetime +from functools import wraps +from time import time + +from app.logging.legionLog import getAppLogger + +timestampFormats = { + "HUMAN_FORMAT": "%d %b %Y %H:%M:%S.%f", + "STANDARD_TIMESTAMP": '%Y%m%d%H%M%S%f' +} + + +def timing(f): + log = getAppLogger() + + @wraps(f) + def wrap(*args, **kw): + ts = time() + result = f(*args, **kw) + te = time() + tr = te - ts + log.debug('Function:%r args:[%r, %r] took: %2.4f sec' % (f.__name__, args, kw, tr)) + return result + + return wrap + + +def getTimestamp(human: bool = False) -> str: + timeFormat = timestampFormats["HUMAN_FORMAT"] if human else timestampFormats["STANDARD_TIMESTAMP"] + return datetime.fromtimestamp(time()).strftime(timeFormat) diff --git a/app/tools/ToolCoordinator.py b/app/tools/ToolCoordinator.py new file mode 100644 index 00000000..8dde4f2a --- /dev/null +++ b/app/tools/ToolCoordinator.py @@ -0,0 +1,72 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import ntpath + +from app.shell.Shell import Shell +from app.tools.nmap.NmapExporter import NmapExporter +from app.tools.nmap.NmapHelpers import nmapFileExists + + +def xmlFileExists(shell: Shell, fileName: str) -> bool: + return fileExists(shell, f"{fileName}.xml") + + +def textFileExists(shell: Shell, fileName: str) -> bool: + return fileExists(shell, f"{fileName}.txt") + + +def fileExists(shell: Shell, fileName) -> bool: + return shell.directoryOrFileExists(fileName) and shell.isFile(fileName) + + +class ToolCoordinator: + def __init__(self, shell: Shell, nmapExporter: NmapExporter): + self.shell = shell + self.nmapExporter = nmapExporter + + # this function moves the specified tool output file from the temporary 'running' folder + # to the 'tool output' folder + def saveToolOutput(self, projectOutputFolder: str, outputFileName: str) -> None: + tool = self.__determineToolUsedByOutputFilename(outputFileName) + toolOutputFolder = f"{projectOutputFolder}/{tool}" + self.__createToolOutputFolderIfNotExists(toolOutputFolder) + self.__determineToolOutputFiles(outputFileName, toolOutputFolder) + + def __determineToolOutputFiles(self, outputFileName: str, toolOutputFolder: str): + # check if the outputFilename exists, if not try .xml and .txt extensions + # (different tools use different formats) + if fileExists(self.shell, outputFileName): + self.shell.move(outputFileName, toolOutputFolder) + # move all the nmap files (not only the .xml) + elif nmapFileExists(self.shell, outputFileName): + self.nmapExporter.exportOutputToHtml(outputFileName, toolOutputFolder) + self.shell.move(outputFileName + '.xml', toolOutputFolder) + self.shell.move(outputFileName + '.nmap', toolOutputFolder) + self.shell.move(outputFileName + '.gnmap', toolOutputFolder) + elif xmlFileExists(self.shell, outputFileName): + self.shell.move(outputFileName + '.xml', toolOutputFolder) + elif textFileExists(self.shell, outputFileName): + self.shell.move(outputFileName + '.txt', toolOutputFolder) + + @staticmethod + def __determineToolUsedByOutputFilename(outputFileName) -> str: + return ntpath.basename(ntpath.dirname(outputFileName)) + + def __createToolOutputFolderIfNotExists(self, toolOutputFolder: str) -> None: + if not self.shell.directoryOrFileExists(toolOutputFolder): + self.shell.create_directory_recursively(toolOutputFolder) diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/tools/nmap/DefaultNmapExporter.py b/app/tools/nmap/DefaultNmapExporter.py new file mode 100644 index 00000000..c1e3f166 --- /dev/null +++ b/app/tools/nmap/DefaultNmapExporter.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import subprocess +from logging import Logger + +from app.shell.Shell import Shell +from app.timing import timing +from app.tools.nmap.NmapExporter import NmapExporter +import os + + +class DefaultNmapExporter(NmapExporter): + def __init__(self, shell: Shell, logger: Logger): + self.logger = logger + self.shell = shell + + @timing + def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: + try: + command = f"xsltproc -o {fileName}.html /usr/share/nmap/nmap.xsl {fileName}.xml" + p = subprocess.Popen(command, shell=True) + p.wait() + self.shell.move(f"{fileName}.html", outputFolder) + except: + self.logger.error("nmap output export to html attempted, but failed.") + self.logger.error('Could not convert nmap XML to HTML. Try: apt-get install xsltproc') diff --git a/app/tools/nmap/NmapExporter.py b/app/tools/nmap/NmapExporter.py new file mode 100644 index 00000000..3d47ae15 --- /dev/null +++ b/app/tools/nmap/NmapExporter.py @@ -0,0 +1,24 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from abc import ABC + + +class NmapExporter(ABC): + def exportOutputToHtml(self, fileName: str, outputFolder: str) -> None: + pass + diff --git a/app/tools/nmap/NmapHelpers.py b/app/tools/nmap/NmapHelpers.py new file mode 100644 index 00000000..74d2e36c --- /dev/null +++ b/app/tools/nmap/NmapHelpers.py @@ -0,0 +1,28 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from app.shell.Shell import Shell + + +def nmapFileExists(shell: Shell, fileName: str) -> bool: + return shell.directoryOrFileExists(f"{fileName}.xml") and \ + shell.directoryOrFileExists(f"{fileName}.nmap") and \ + shell.directoryOrFileExists(f"{fileName}.gnmap") and \ + shell.isFile(f"{fileName}.xml") and \ + shell.isFile(f"{fileName}.nmap") and \ + shell.isFile(f"{fileName}.gnmap") diff --git a/app/tools/nmap/NmapPaths.py b/app/tools/nmap/NmapPaths.py new file mode 100644 index 00000000..c187f2aa --- /dev/null +++ b/app/tools/nmap/NmapPaths.py @@ -0,0 +1,25 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + + +def getNmapRunningFolder(projectRunningFolder: str) -> str: + return f"{projectRunningFolder}/nmap" + + +def getNmapOutputFolder(projectOutputFolder: str) -> str: + return f"{projectOutputFolder}/nmap" diff --git a/app/tools/nmap/__init__.py b/app/tools/nmap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backup/.gitkeep b/backup/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/backup/NOTHING-HERE-BUT-US-CHICKENS b/backup/NOTHING-HERE-BUT-US-CHICKENS new file mode 100644 index 00000000..e69de29b diff --git a/buildHugeNmapTest.py b/buildHugeNmapTest.py new file mode 100644 index 00000000..feaf03fe --- /dev/null +++ b/buildHugeNmapTest.py @@ -0,0 +1,177 @@ +import xml.etree.ElementTree as ET +from random import randint, choice, choices, sample +import datetime + +def generate_nmap_xml(num_hosts=1000, base_subnet="172.16"): + """ + Generate a full sample nmap XML file with session information included. + + Parameters: + num_hosts (int): Number of hosts to generate. + base_subnet (str): Base subnet for the hosts. + + Returns: + str: XML content as a string. + """ + # XML header + xml_header = ''' + + +''' + + # Create root element + nmaprun = ET.Element("nmaprun", { + "scanner": "nmap", + "args": "nmap -sV -oX", + "start": "123456789", + "startstr": "Mon Nov 20 13:33:54 2023", + "version": "7.94", + "xmloutputversion": "1.05" + }) + + # Create scaninfo, verbose, and debugging elements + scaninfo = ET.SubElement(nmaprun, "scaninfo", { + "type": "syn", + "protocol": "tcp", + "numservices": "1000", + "services": "1-65535" + }) + ET.SubElement(nmaprun, "verbose", {"level": "0"}) + ET.SubElement(nmaprun, "debugging", {"level": "0"}) + + # OS, services, and port mapping + os_services = { + "Linux": {"http": 80, "ssh": 22, "smtp": 25, "ftp": 21, "telnet": 23}, + "Windows": {"http": 80, "msrpc": 135, "netbios-ssn": 139, "rdp": 3389, "smb": 445}, + "Solaris": {"http": 80, "ssh": 22, "telnet": 23, "netbios-ssn": 139, "ftp": 21}, + "Darwin": {"http": 80, "netbios-ssn": 139, "ipp": 631, "afp": 548, "ssh": 22} + } + + # Service products and versions + service_info = { + "http": {"product": "Apache httpd", "version": "2.4.41"}, + "ssh": {"product": "OpenSSH", "version": "8.0"}, + "smtp": {"product": "Postfix SMTP", "version": "3.4.8"}, + "ftp": {"product": "vsftpd", "version": "3.0.3"}, + "telnet": {"product": "Telnet Server", "version": "1.2"}, + "msrpc": {"product": "Microsoft RPC", "version": "5.0"}, + "netbios-ssn": {"product": "Samba smbd", "version": "4.10.10"}, + "rdp": {"product": "Microsoft Terminal Service", "version": "10.0"}, + "smb": {"product": "Microsoft SMB", "version": "3.1.1"}, + "ipp": {"product": "CUPS", "version": "2.3.0"}, + "afp": {"product": "Netatalk AFP", "version": "3.1.12"} + } + + # Expanded lists for generating hostnames + colors = ["Red", "Blue", "Green", "Yellow", "Purple", "Orange", "Cyan", "Magenta", "Lime", "Pink"] + foods = ["Apple", "Burger", "Cake", "Dumpling", "Eclair", "Pizza", "Sushi", "Taco", "Waffle", "Bagel"] + cities = ["Tokyo", "Paris", "London", "NewYork", "Sydney", "Berlin", "Rome", "Madrid", "Moscow", "Beijing"] + verbs = ["Jumping", "Running", "Flying", "Swimming", "Dancing", "Singing", "Playing", "Walking", "Reading", "Writing"] + + # Unique hostname tracker + generated_hostnames = set() + + # Function to create unique random hostnames + def generate_hostname(): + while True: + parts = [choice(colors), choice(foods), choice(cities), choice(verbs)] + hostname = '.'.join(parts) + # Ensure uniqueness by appending a number if needed + if hostname not in generated_hostnames: + generated_hostnames.add(hostname) + return hostname + else: + hostname += str(randint(0, 9999)) + if hostname not in generated_hostnames: + generated_hostnames.add(hostname) + return hostname + + # Function to create a random IP address within the extended subnet range + def random_ip(base_subnet, host_number): + subnet_third_octet = host_number // 254 + host_fourth_octet = host_number % 254 + 1 + return f"{base_subnet}.{subnet_third_octet}.{host_fourth_octet}" + + # Generating hosts with updated IP address and hostname method + for i in range(num_hosts): + host_os = choice(list(os_services.keys())) + + host = ET.Element("host") + ET.SubElement(host, "status", {"state": "up", "reason": "arp-response", "reason_ttl": "0"}) + ET.SubElement(host, "address", {"addr": random_ip(base_subnet, i), "addrtype": "ipv4"}) + + # Hostnames + hostnames = ET.SubElement(host, "hostnames") + num_hostnames = randint(1, 3) # Random number of hostnames per host + for _ in range(num_hostnames): + ET.SubElement(hostnames, "hostname", {"name": generate_hostname(), "type": "user"}) + + # Ports + ports = ET.SubElement(host, "ports") + open_ports_count = randint(1, 5) # Random number of open ports (1 to 5) + services = sample(list(os_services[host_os].items()), open_ports_count) + for service, port in services: + port_element = ET.SubElement(ports, "port", {"protocol": "tcp", "portid": str(port)}) + ET.SubElement(port_element, "state", {"state": "open", "reason": "syn-ack", "reason_ttl": "64"}) + service_element = ET.SubElement(port_element, "service", { + "name": service, + "product": service_info[service]["product"], + "version": service_info[service]["version"], + "ostype": host_os + }) + + # OS element with osmatch + os = ET.SubElement(host, "os") + osclass = ET.SubElement(os, "osclass", { + "type": "general purpose", + "vendor": host_os, + "osfamily": host_os, + "osgen": "Unknown", # OS generation can be set accordingly + "accuracy": "98" + }) + ET.SubElement(osclass, "cpe", text=f"cpe:/o:{host_os.lower()}:{host_os.lower()}") + osmatch = ET.SubElement(os, "osmatch", { + "name": f"{host_os} Generic Match", + "accuracy": str(randint(90, 100)), + "line": str(randint(1000, 2000)) + }) + ET.SubElement(osmatch, "osclass", { + "type": "general purpose", + "vendor": host_os, + "osfamily": host_os, + "osgen": "Unknown", + "accuracy": "98" + }) + + nmaprun.append(host) + + # Runstats + runstats = ET.SubElement(nmaprun, "runstats") + ET.SubElement(runstats, "finished", { + "time": "123456790", + "timestr": "Mon Nov 20 13:34:10 2023", + "summary": f"Nmap done; {num_hosts} IP addresses ({num_hosts} hosts up) scanned in 16.42 seconds", + "elapsed": "16.42", + "exit": "success" + }) + ET.SubElement(runstats, "hosts", { + "up": str(num_hosts), + "down": "0", + "total": str(num_hosts) + }) + + # Convert the XML to string + xml_str = xml_header + '\n' + ET.tostring(nmaprun, encoding='unicode', method='xml') + return xml_str + +def save_nmap_xml(filename, num_hosts=200, base_subnet="172.16"): + # Generate the XML content + xml_content = generate_nmap_xml(num_hosts, base_subnet) + + # Save the content to the specified file + with open(filename, "w") as file: + file.write(xml_content) + +# Specify the filename and call the function to save the file +filename = "huge_nmap.xml" +save_nmap_xml(filename) diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/controller/controller.py b/controller/controller.py new file mode 100644 index 00000000..347edde6 --- /dev/null +++ b/controller/controller.py @@ -0,0 +1,971 @@ +#!/usr/bin/env python +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import signal # for file operations, to kill processes, for regex, for subprocesses +import subprocess + +from app.ApplicationInfo import applicationInfo +from app.Screenshooter import Screenshooter +from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable +from app.importers.NmapImporter import NmapImporter +from app.importers.PythonImporter import PythonImporter +from app.tools.nmap.NmapPaths import getNmapRunningFolder +from app.auxiliary import unixPath2Win, winPath2Unix, getPid, formatCommandQProcess, isWsl +from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver + +try: + import queue +except: + import Queue as queue +from app.logic import * +from app.settings import * + +log = getAppLogger() + +class Controller: + + # initialisations that will happen once - when the program is launched + @timing + def __init__(self, view, logic): + self.logic = logic + self.view = view + self.view.setController(self) + self.view.startOnce() + self.view.startConnections() + + self.loadSettings() # creation of context menu actions from settings file and set up of various settings + updateProgressObservable = UpdateProgressObservable() + updateProgressObserver = QtUpdateProgressObserver(self.view.importProgressWidget) + updateProgressObservable.attach(updateProgressObserver) + + self.initNmapImporter(updateProgressObservable) + self.initPythonImporter() + self.initScreenshooter() + self.initBrowserOpener() + self.start() # initialisations (globals, etc) + self.initTimers() + self.processTimers = {} + self.processMeasurements = {} + + # initialisations that will happen everytime we create/open a project - can happen several times in the + # program's lifetime + def start(self, title='*untitled'): + self.processes = [] # to store all the processes we run (nmaps, niktos, etc) + self.fastProcessQueue = queue.Queue() # to manage fast processes (banner, snmpenum, etc) + self.fastProcessesRunning = 0 # counts the number of fast processes currently running + self.slowProcessesRunning = 0 # counts the number of slow processes currently running + activeProject = self.logic.activeProject + self.nmapImporter.setDB(activeProject.database) # tell nmap importer which db to use + self.nmapImporter.setHostRepository(activeProject.repositoryContainer.hostRepository) + self.pythonImporter.setDB(activeProject.database) + self.updateOutputFolder() # tell screenshooter where the output folder is + self.view.start(title) + + def initNmapImporter(self, updateProgressObservable: UpdateProgressObservable): + self.nmapImporter = NmapImporter(updateProgressObservable, + self.logic.activeProject.repositoryContainer.hostRepository) + self.nmapImporter.done.connect(self.importFinished) + self.nmapImporter.done.connect(self.view.updateInterface) + self.nmapImporter.done.connect(self.view.updateToolsTableView) + self.nmapImporter.done.connect(self.view.updateProcessesTableView) + self.nmapImporter.schedule.connect(self.scheduler) # run automated attacks + self.nmapImporter.log.connect(self.view.ui.LogOutputTextView.append) + + def initPythonImporter(self): + self.pythonImporter = PythonImporter() + self.pythonImporter.done.connect(self.importFinished) + self.pythonImporter.done.connect(self.view.updateInterface) + self.pythonImporter.done.connect(self.view.updateToolsTableView) + self.pythonImporter.done.connect(self.view.updateProcessesTableView) + self.pythonImporter.schedule.connect(self.scheduler) # run automated attacks + self.pythonImporter.log.connect(self.view.ui.LogOutputTextView.append) + + def initScreenshooter(self): + # screenshot taker object (different thread) + self.screenshooter = Screenshooter(self.settings.general_screenshooter_timeout) + self.screenshooter.done.connect(self.screenshotFinished) + self.screenshooter.log.connect(self.view.ui.LogOutputTextView.append) + + def initBrowserOpener(self): + self.browser = BrowserOpener() # browser opener object (different thread) + self.browser.log.connect(self.view.ui.LogOutputTextView.append) + + # these timers are used to prevent from updating the UI several times within a short time period - + # which freezes the UI + def initTimers(self): + self.updateUITimer = QTimer() + self.updateUITimer.setSingleShot(True) + # Moving to deprecate all these general interface update timers + #self.updateUITimer.timeout.connect(self.view.updateProcessesTableView) + #self.updateUITimer.timeout.connect(self.view.updateToolsTableView) + + self.updateUI2Timer = QTimer() + self.updateUI2Timer.setSingleShot(True) + # Moving to deprecate all these general interface update timers + #self.updateUI2Timer.timeout.connect(self.view.updateInterface) + + self.processTableUiUpdateTimer = QTimer() + self.processTableUiUpdateTimer.timeout.connect(self.view.updateProcessesTableView) + # Update only when queue > 0 + self.processTableUiUpdateTimer.start(500) # Faster than this doesn't make anything smoother + + # this function fetches all the settings from the conf file. Among other things it populates the actions lists + # that will be used in the context menus. + def loadSettings(self): + self.settingsFile = AppSettings() + # load settings from conf file (create conf file first if necessary) + self.settings = Settings(self.settingsFile) + # save the original state so that we can know if something has changed when we exit LEGION + self.originalSettings = Settings(self.settingsFile) + self.logic.projectManager.setStoreWordListsOnExit(self.logic.activeProject, + self.settings.brute_store_cleartext_passwords_on_exit == 'True') + self.view.settingsWidget.setSettings(Settings(self.settingsFile)) + + # call this function when clicking 'apply' in the settings menu (after validation) + def applySettings(self, newSettings): + self.settings = newSettings + + def cancelSettings(self): # called when the user presses cancel in the Settings dialog + # resets the dialog's settings to the current application settings to forget any changes made by the user + self.view.settingsWidget.setSettings(self.settings) + + @timing + def saveSettings(self, saveBackup = True): + if not self.settings == self.originalSettings: + log.info('Settings have been changed.') + self.settingsFile.backupAndSave(self.settings, saveBackup) + else: + log.info('Settings have NOT been changed.') + + def getSettings(self): + return self.settings + + #################### AUXILIARY #################### + + def getCWD(self): + return self.logic.activeProject.properties.workingDirectory + + def getProjectName(self): + return self.logic.activeProject.properties.projectName + + def getRunningFolder(self): + return self.logic.activeProject.properties.runningFolder + + def getOutputFolder(self): + return self.logic.activeProject.properties.outputFolder + + def getUserlistPath(self): + return self.logic.activeProject.properties.usernamesWordList.filename + + def getPasslistPath(self): + return self.logic.activeProject.properties.passwordWordList.filename + + def updateOutputFolder(self): + self.screenshooter.updateOutputFolder( + self.logic.activeProject.properties.outputFolder + '/screenshots') # update screenshot folder + + def copyNmapXMLToOutputFolder(self, filename): + self.logic.copyNmapXMLToOutputFolder(filename) + + def isTempProject(self): + return self.logic.activeProject.properties.isTemporary + + def getDB(self): + return self.logic.activeProject.database + + def getRunningProcesses(self): + return self.processes + + def getHostActions(self): + return self.settings.hostActions + + def getPortActions(self): + return self.settings.portActions + + def getPortTerminalActions(self): + return self.settings.portTerminalActions + + #################### ACTIONS #################### + + def createNewProject(self): + self.view.closeProject() # removes temp folder (if any) + self.logic.createNewTemporaryProject() + self.start() # initialisations (globals, etc) + + def openExistingProject(self, filename, projectType='legion'): + self.view.closeProject() + self.view.importProgressWidget.reset('Opening project..') + self.view.importProgressWidget.show() # show the progress widget + self.logic.openExistingProject(filename, projectType) + # initialisations (globals, signals, etc) + self.start(ntpath.basename(self.logic.activeProject.properties.projectName)) + self.view.restoreToolTabs() # restores the tool tabs for each host + self.view.hostTableClick() # click on first host to restore his host tool tabs + self.view.importProgressWidget.hide() # hide the progress widget + + def saveProject(self, lastHostIdClicked, notes): + if not lastHostIdClicked == '': + self.logic.activeProject.repositoryContainer.noteRepository.storeNotes(lastHostIdClicked, notes) + + def saveProjectAs(self, filename, replace=0): + success = self.logic.saveProjectAs(filename, replace) + if success: + self.nmapImporter.setDB(self.logic.activeProject.database) # tell nmap importer which db to use + return success + + def closeProject(self): + self.saveSettings() # backup and save config file, if necessary + self.screenshooter.terminate() + self.initScreenshooter() + self.logic.activeProject.repositoryContainer.processRepository.toggleProcessDisplayStatus(True) + self.view.updateProcessesTableView() # clear process table + self.logic.projectManager.closeProject(self.logic.activeProject) + + def copyToClipboard(self, data): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(data) # Assuming item.text() contains the IP or hostname + + @timing + def addHosts(self, targetHosts, runHostDiscovery, runStagedNmap, nmapSpeed, scanMode, nmapOptions = []): + if targetHosts == '': + log.info('No hosts entered..') + return + + runningFolder = self.logic.activeProject.properties.runningFolder + if scanMode == 'Easy': + if runStagedNmap: + self.runStagedNmap(targetHosts, runHostDiscovery) + elif runHostDiscovery: + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-host-discover' + if isWsl(): + outputfile = unixPath2Win(outputfile) + command = f"nmap -n -sV -O --version-light -T{str(nmapSpeed)} {targetHosts} -oA {outputfile}" + self.runCommand('nmap', 'nmap (discovery)', targetHosts, '', '', command, getTimestamp(True), + outputfile, self.view.createNewTabForHost(str(targetHosts), 'nmap (discovery)', True)) + else: + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-list' + if isWsl(): + outputfile = unixPath2Win(outputfile) + command = "nmap -n -sL -T" + str(nmapSpeed) + " " + targetHosts + " -oA " + outputfile + self.runCommand('nmap', 'nmap (list)', targetHosts, '', '', command, getTimestamp(True), + outputfile, + self.view.createNewTabForHost(str(targetHosts), 'nmap (list)', True)) + elif scanMode == 'Hard': + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmap-custom' + if isWsl(): + outputfile = unixPath2Win(outputfile) + nmapOptionsString = ' '.join(nmapOptions) + if 'randomize' not in nmapOptionsString: + nmapOptionsString = nmapOptionsString + " -T" + str(nmapSpeed) + command = "nmap " + nmapOptionsString + " " + targetHosts + " -oA " + outputfile + self.runCommand('nmap', 'nmap (custom ' + nmapOptionsString + ')', targetHosts, '', '', command, + getTimestamp(True), outputfile, + self.view.createNewTabForHost( + str(targetHosts), 'nmap (custom ' + nmapOptionsString + ')', + True)) + + #################### CONTEXT MENUS #################### + + # showAll exists because in some cases we only want to show host tools excluding portscans and 'mark as checked' + @timing + def getContextMenuForHost(self, isChecked, showAll=True): + menu = QMenu() + self.nmapSubMenu = QMenu('Portscan') + actions = [] + + for a in self.settings.hostActions: + if "nmap" in a[1] or "unicornscan" in a[1]: + actions.append(self.nmapSubMenu.addAction(a[0])) + else: + actions.append(menu.addAction(a[0])) + + if showAll: + actions.append(self.nmapSubMenu.addAction("Run nmap (staged)")) + + menu.addMenu(self.nmapSubMenu) + menu.addSeparator() + + if isChecked == 'True': + menu.addAction('Mark as unchecked') + else: + menu.addAction('Mark as checked') + menu.addAction('Rescan') + menu.addAction('Purge Results') + menu.addAction('Delete') + + return menu, actions + + @timing + def handleHostAction(self, ip, hostid, actions, action): + repositoryContainer = self.logic.activeProject.repositoryContainer + + if action.text() == 'Mark as checked' or action.text() == 'Mark as unchecked': + repositoryContainer.hostRepository.toggleHostCheckStatus(ip) + self.view.updateInterface() + return + + if action.text() == 'Run nmap (staged)': + # if we are running nmap we need to purge previous portscan results + log.info('Purging previous portscan data for ' + str(ip)) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + self.view.updateInterface() + self.runStagedNmap(ip, False) + return + + if action.text() == 'Rescan': + log.info('Rescanning host {0}'.format(str(ip))) + self.runStagedNmap(ip, False) + return + + if action.text() == 'Purge Results': + log.info('Purging previous portscan data for host {0}'.format(str(ip))) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + self.view.updateInterface() + return + + if action.text() == 'Delete': + log.info('Purging previous portscan data for host {0}'.format(str(ip))) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'tcp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'tcp') + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, 'udp'): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, 'udp') + self.logic.activeProject.repositoryContainer.hostRepository.deleteHost(ip) + self.view.updateInterface() + return + + for i in range(0,len(actions)): + if action == actions[i]: + name = self.settings.hostActions[i][1] + invisibleTab = False + # to make sure different nmap scans appear under the same tool name + if 'nmap' in name: + name = 'nmap' + invisibleTab = True + elif 'python-script' in name: + invisibleTab = True + # remove all chars that are not alphanumeric from tool name (used in the outputfile's name) + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(name)) + "/" + getTimestamp() + "-" + \ + re.sub("[^0-9a-zA-Z]", "", str(self.settings.hostActions[i][1])) + "-" + ip + command = str(self.settings.hostActions[i][2]) + command = command.replace('[IP]', ip).replace('[OUTPUT]', outputfile) + if 'nmap' in command: + if isWsl(): + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + else: + command = "{0} -oA {1}".format(command, outputfile) + + # check if same type of nmap scan has already been made and purge results before scanning + if 'nmap' in command: + proto = 'tcp' + if '-sU' in command: + proto = 'udp' + # if we are running nmap we need to purge previous portscan results (of the same protocol) + if repositoryContainer.portRepository.getPortsByIPAndProtocol(ip, proto): + repositoryContainer.portRepository.deleteAllPortsAndScriptsByHostId(hostid, proto) + + tabTitle = self.settings.hostActions[i][1] + self.runCommand(name, tabTitle, ip, '', '', command, getTimestamp(True), outputfile, + self.view.createNewTabForHost(ip, tabTitle, invisibleTab)) + break + + @timing + def getContextMenuForServiceName(self, serviceName='*', menu=None): + if menu == None: # if no menu was given, create a new one + menu = QMenu() + + if serviceName == '*' or serviceName in self.settings.general_web_services.split(","): + menu.addAction("Open in browser") + menu.addAction("Take screenshot") + + actions = [] + for a in self.settings.portActions: + # if the service name exists in the portActions list show the command in the context menu + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': + # in actions list write the service and line number that corresponds to it in portActions + actions.append([self.settings.portActions.index(a), menu.addAction(a[0])]) + + # if the user pressed SHIFT+Right-click show full menu + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: + shiftPressed = True + else: + shiftPressed = False + + return menu, actions, shiftPressed + + @timing + def handleServiceNameAction(self, targets, actions, action, restoring=True): + + if action.text() == 'Take screenshot': + for ip in targets: + url = ip[0] + ':' + ip[1] + self.screenshooter.addToQueue(ip[0], ip[1], url) + self.screenshooter.start() + return + + elif action.text() == 'Open in browser': + for ip in targets: + url = ip[0]+':'+ip[1] + self.browser.addToQueue(url) + self.browser.start() + return + + for i in range(0,len(actions)): + if action == actions[i][1]: + srvc_num = actions[i][0] + for ip in targets: + tool = self.settings.portActions[srvc_num][1] + tabTitle = self.settings.portActions[srvc_num][1]+" ("+ip[1]+"/"+ip[2]+")" + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(tool)) + \ + "/" + getTimestamp() + '-' + tool + "-" + ip[0] + "-" + ip[1] + + command = str(self.settings.portActions[srvc_num][2]) + command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]).replace('[OUTPUT]', outputfile) + if 'nmap' in command: + if isWsl(): + command = "{0} -oA {1}".format(command, unixPath2Win(outputfile)) + else: + command = "{0} -oA {1}".format(command, outputfile) + + if 'nmap' in command and ip[2] == 'udp': + command = command.replace("-sV", "-sVU") + + if 'nmap' in tabTitle: # we don't want to show nmap tabs + restoring = True + elif 'python-script' in tabTitle: # we don't want to show nmap tabs + restoring = True + + self.runCommand(tool, tabTitle, ip[0], ip[1], ip[2], command, getTimestamp(True), outputfile, + self.view.createNewTabForHost(ip[0], tabTitle, restoring)) + break + + @timing + def getContextMenuForPort(self, serviceName='*'): + + menu = QMenu() + + modifiers = QtWidgets.QApplication.keyboardModifiers() # if the user pressed SHIFT+Right-click show full menu + if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: + serviceName='*' + + terminalActions = [] # custom terminal actions from settings file + # if wildcard or the command is valid for this specific service or if the command is valid for all services + for a in self.settings.portTerminalActions: + if serviceName is None or serviceName == '*' or serviceName in a[3].split(",") or a[3] == '': + terminalActions.append([self.settings.portTerminalActions.index(a), menu.addAction(a[0])]) + + menu.addSeparator() + menu.addAction("Send to Brute") + menu.addSeparator() # dummy is there because we don't need the third return value + menu, actions, dummy = self.getContextMenuForServiceName(serviceName, menu) + menu.addSeparator() + menu.addAction("Run custom command") + + return menu, actions, terminalActions + + @timing + def handlePortAction(self, targets, *args): + actions = args[0] + terminalActions = args[1] + action = args[2] + restoring = args[3] + + if action.text() == 'Send to Brute': + for ip in targets: + # ip[0] is the IP, ip[1] is the port number and ip[3] is the service name + self.view.createNewBruteTab(ip[0], ip[1], ip[3]) + return + + if action.text() == 'Run custom command': + log.info('custom command') + return + + terminal = self.settings.general_default_terminal # handle terminal actions + for i in range(0,len(terminalActions)): + if action == terminalActions[i][1]: + srvc_num = terminalActions[i][0] + for ip in targets: + command = str(self.settings.portTerminalActions[srvc_num][2]) + command = command.replace('[IP]', ip[0]).replace('[PORT]', ip[1]) + if "[term]" in command: + command = command.replace("[term]", "") + subprocess.Popen(terminal + " -e './scripts/exec-in-shell " + command + "'", shell=True) + else: + subprocess.Popen("bash -c \"" + command + "; exec bash\"", shell=True) + return + + self.handleServiceNameAction(targets, actions, action, restoring) + + def getContextMenuForProcess(self): + menu = QMenu() + menu.addAction("Kill") + menu.addAction("Clear") + return menu + + # selectedProcesses is a list of tuples (pid, status, procId) + def handleProcessAction(self, selectedProcesses, action): + if action.text() == 'Kill': + if self.view.killProcessConfirmation(): + for p in selectedProcesses: + if p[1] != "Running": + if p[1] == "Waiting": + if str(self.logic.activeProject.repositoryContainer.processRepository.getStatusByProcessId( + p[2])) == 'Running': + self.killProcess(self.view.ProcessesTableModel.getProcessPidForId(p[2]), p[2]) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessCancelStatus( + str(p[2])) + else: + log.info("This process has already been terminated. Skipping.") + else: + self.killProcess(p[0], p[2]) + self.view.updateProcessesTableView() + return + + if action.text() == 'Clear': # hide all the processes that are not running + self.logic.activeProject.repositoryContainer.processRepository.toggleProcessDisplayStatus() + self.view.updateProcessesTableView() + + #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def isHostInDB(self, host): + return self.logic.activeProject.repositoryContainer.hostRepository.exists(host) + + def getHostsFromDB(self, filters): + return self.logic.activeProject.repositoryContainer.hostRepository.getHosts(filters) + + def getServiceNamesFromDB(self, filters): + return self.logic.activeProject.repositoryContainer.serviceRepository.getServiceNames(filters) + + def getProcessStatusForDBId(self, dbId): + return self.logic.activeProject.repositoryContainer.processRepository.getStatusByProcessId(dbId) + + def getPidForProcess(self, dbId): + return self.logic.activeProject.repositoryContainer.processRepository.getPIDByProcessId(dbId) + + def storeCloseTabStatusInDB(self, pid): + return self.logic.activeProject.repositoryContainer.processRepository.storeCloseStatus(pid) + + def getServiceNameForHostAndPort(self, hostIP, port): + return self.logic.activeProject.repositoryContainer.serviceRepository.getServiceNamesByHostIPAndPort(hostIP, + port) + + #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def getPortsAndServicesForHostFromDB(self, hostIP, filters): + return self.logic.activeProject.repositoryContainer.portRepository.getPortsAndServicesByHostIP(hostIP, filters) + + def getHostsAndPortsForServiceFromDB(self, serviceName, filters): + return self.logic.activeProject.repositoryContainer.hostRepository.getHostsAndPortsByServiceName(serviceName, + filters) + + def getHostInformation(self, hostIP): + return self.logic.activeProject.repositoryContainer.hostRepository.getHostInformation(hostIP) + + def getPortStatesForHost(self, hostid): + return self.logic.activeProject.repositoryContainer.portRepository.getPortStatesByHostId(hostid) + + def getScriptsFromDB(self, hostIP): + return self.logic.activeProject.repositoryContainer.scriptRepository.getScriptsByHostIP(hostIP) + + def getCvesFromDB(self, hostIP): + return self.logic.activeProject.repositoryContainer.cveRepository.getCVEsByHostIP(hostIP) + + def getScriptOutputFromDB(self, scriptDBId): + return self.logic.activeProject.repositoryContainer.scriptRepository.getScriptOutputById(scriptDBId) + + def getNoteFromDB(self, hostid): + return self.logic.activeProject.repositoryContainer.noteRepository.getNoteByHostId(hostid) + + def getHostsForTool(self, toolName, closed='False'): + return self.logic.activeProject.repositoryContainer.processRepository.getHostsByToolName(toolName, closed) + + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + + def getProcessesFromDB(self, filters, showProcesses='noNmap', sort='desc', ncol='id'): + return self.logic.activeProject.repositoryContainer.processRepository.getProcesses(filters, showProcesses, sort, + ncol) + + #################### PROCESSES #################### + + def checkProcessQueue(self): + log.debug('Queue maximum concurrent processes: {0}'.format(str(self.settings.general_max_fast_processes))) + log.debug('Queue processes running: {0}'.format(str(self.fastProcessesRunning))) + log.debug('Queue processes waiting: {0}'.format(str(self.fastProcessQueue.qsize()))) + + if not self.fastProcessQueue.empty(): + self.processTableUiUpdateTimer.start(1000) + if (self.fastProcessesRunning <= int(self.settings.general_max_fast_processes)): + next_proc = self.fastProcessQueue.get() + if not self.logic.activeProject.repositoryContainer.processRepository.isCancelledProcess( + str(next_proc.id)): + log.info('Running: ' + str(next_proc.command)) + next_proc.display.clear() + self.processes.append(next_proc) + self.fastProcessesRunning += 1 + # Add Timeout + next_proc.waitForFinished(10) + formattedCommand = formatCommandQProcess(next_proc.command) + log.debug('Up next: {0}, {1}'.format(formattedCommand[0], formattedCommand[1])) + next_proc.start(formattedCommand[0], formattedCommand[1]) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningStatus( + next_proc.id, getPid(next_proc)) + elif not self.fastProcessQueue.empty(): + log.debug('Process was canceled, checking queue again..') + self.checkProcessQueue() + else: + log.info("Halting process panel update timer as all processes are finished.") + self.processTableUiUpdateTimer.stop() + + def cancelProcess(self, dbId): + log.info('Canceling process: ' + str(dbId)) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessCancelStatus( + str(dbId)) # mark it as cancelled + self.updateUITimer.stop() + self.updateUITimer.start(1500) # update the interface soon + + def killProcess(self, pid, dbId): + log.info('Killing process: ' + str(pid)) + self.logic.activeProject.repositoryContainer.processRepository.storeProcessKillStatus(str(dbId)) + try: + os.kill(int(pid), signal.SIGTERM) + except OSError: + log.info('This process has already been terminated.') + except: + log.info("Unexpected error:", sys.exc_info()[0]) + + def killRunningProcesses(self): + log.info('Killing running processes!') + for p in self.processes: + p.finished.disconnect() # experimental + self.killProcess(int(getPid(p)), p.id) + + # this function creates a new process, runs the command and takes care of displaying the ouput. returns the PID + # the last 3 parameters are only used when the command is a staged nmap + def runCommand(self, *args, discovery=True, stage=0, stop=False): + def handleProcStop(*vargs): + updateElapsed.stop() + self.processTimers[qProcess.id] = None + procTime = timer.elapsed() / 1000 + qProcess.elapsed = procTime + self.logic.activeProject.repositoryContainer.processRepository.storeProcessRunningElapsedTime(qProcess.id, + procTime) + + def handleProcUpdate(*vargs): + procTime = timer.elapsed() / 1000 + self.processMeasurements[getPid(qProcess)] = procTime + + name = args[0] + tabTitle = args[1] + hostIp = args[2] + port = args[3] + protocol = args[4] + command = args[5] + startTime = args[6] + outputfile = args[7] + textbox = args[8] + timer = QElapsedTimer() + updateElapsed = QTimer() + + self.logic.createFolderForTool(name) + qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) + qProcess.started.connect(timer.start) + qProcess.finished.connect(handleProcStop) + updateElapsed.timeout.connect(handleProcUpdate) + + processRepository = self.logic.activeProject.repositoryContainer.processRepository + textbox.setProperty('dbId', str(processRepository.storeProcess(qProcess))) + updateElapsed.start(1000) + self.processTimers[qProcess.id] = updateElapsed + self.processMeasurements[getPid(qProcess)] = 0 + + log.info('Queuing: ' + str(command)) + self.fastProcessQueue.put(qProcess) + + self.checkProcessQueue() + + # update the processes table + self.updateUITimer.stop() + # while the process is running, when there's output to read, display it in the GUI + self.updateUITimer.start(900) + + qProcess.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.MergedChannels) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( + str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + + #qProcess.readyReadStandardError.connect(lambda: qProcess.display.appendPlainText( + # str(qProcess.readAllStandardError().data().decode('ISO-8859-1')))) + + qProcess.sigHydra.connect(self.handleHydraFindings) + qProcess.finished.connect(lambda: self.processFinished(qProcess)) + qProcess.errorOccurred.connect(lambda: self.processCrashed(qProcess)) + log.info("runCommand called for stage {0}".format(str(stage))) + + if stage > 0 and stage < 6: # if this is a staged nmap, launch the next stage + log.info("runCommand connected for stage {0}".format(str(stage))) + nextStage = stage + 1 + qProcess.finished.connect( + lambda: self.runStagedNmap(str(hostIp), discovery=discovery, stage=nextStage, + stop=processRepository.isKilledProcess(str(qProcess.id)))) + + return getPid(qProcess) # return the pid so that we can kill the process if needed + + def runPython(self): + textbox = self.view.createNewConsole("python") + name = 'python' + tabTitle = name + hostIp = '127.0.0.1' + port = '22' + protocol = 'tcp' + command = 'python3 /mnt/c/Users/hackm/OneDrive/Documents/Customers/GVIT/GIT/legion/test.py' + startTime = getTimestamp(True) + outputfile = '/tmp/a' + qProcess = MyQProcess(name, tabTitle, hostIp, port, protocol, command, startTime, outputfile, textbox) + + processRepository = self.logic.activeProject.repositoryContainer.processRepository + textbox.setProperty('dbId', str(processRepository.storeProcess(qProcess))) + + log.info('Queuing: ' + str(command)) + self.fastProcessQueue.put(qProcess) + + self.checkProcessQueue() + + self.updateUI2Timer.stop() # update the processes table + # while the process is running, when there's output to read, display it in the GUI + self.updateUI2Timer.start(900) + self.updateUITimer.stop() # update the processes table + self.updateUITimer.start(900) + + qProcess.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.MergedChannels) + qProcess.readyReadStandardOutput.connect(lambda: qProcess.display.appendPlainText( + str(qProcess.readAllStandardOutput().data().decode('ISO-8859-1')))) + + qProcess.sigHydra.connect(self.handleHydraFindings) + qProcess.finished.connect(lambda: self.processFinished(qProcess)) + qProcess.error.connect(lambda: self.processCrashed(qProcess)) + + return getPid(qProcess) + + # recursive function used to run nmap in different stages for quick results + def runStagedNmap(self, targetHosts, discovery = True, stage = 1, stop = False): + log.info("runStagedNmap called for stage {0}".format(str(stage))) + runningFolder = self.logic.activeProject.properties.runningFolder + if not stop: + textbox = self.view.createNewTabForHost(str(targetHosts), 'nmap (stage ' + str(stage) + ')', True) + outputfile = getNmapRunningFolder(runningFolder) + "/" + getTimestamp() + '-nmapstage' + str(stage) + if isWsl(): + outputfile = unixPath2Win(outputfile) + + if stage == 1: + stageData = self.settings.tools_nmap_stage1_ports + elif stage == 2: + stageData = self.settings.tools_nmap_stage2_ports + elif stage == 3: + stageData = self.settings.tools_nmap_stage3_ports + elif stage == 4: + stageData = self.settings.tools_nmap_stage4_ports + elif stage == 5: + stageData = self.settings.tools_nmap_stage5_ports + elif stage == 6: + stageData = self.settings.tools_nmap_stage6_ports + stageDataSplit = str(stageData).split('|') + stageOp = stageDataSplit[0] + stageOpValues = stageDataSplit[1] + log.debug("Stage {0} stageOp {1}".format(str(stage), str(stageOp))) + log.debug("Stage {0} stageOpValues {1}".format(str(stage), str(stageOpValues))) + + if stageOp == "" or stageOp == "NOOP" or stageOp == "SKIP": + log.debug("Skipping stage {0} as stageOp is {1}".format(str(stage), str(stageOp))) + return + + if discovery: # is it with/without host discovery? + command = "nmap -T4 -sV -sSU -O " + else: + command = "nmap -Pn -sSU " + + if stageOp == 'PORTS': + command += '-p ' + stageOpValues + ' -vvvv ' + targetHosts + ' -oA ' + outputfile + elif stageOp == 'NSE': + command = 'nmap -sV --script=' + stageOpValues + ' -vvvv ' + targetHosts + ' -oA ' + outputfile + + log.debug("Stage {0} command: {1}".format(str(stage), str(command))) + + self.runCommand('nmap', 'nmap (stage ' + str(stage) + ')', str(targetHosts), '', '', command, + getTimestamp(True), outputfile, textbox, discovery=discovery, stage=stage, stop=stop) + + def importFinished(self): + # if nmap import was the first action, we need to hide the overlay (note: we shouldn't need to do this + # every time. this can be improved) + self.view.displayAddHostsOverlay(False) + + def screenshotFinished(self, ip, port, filename): + log.info("---------------Screenshoot done. Args %s, %s, %s" % (str(ip), str(port), str(filename))) + outputFolder = self.logic.activeProject.properties.outputFolder + dbId = self.logic.activeProject.repositoryContainer.processRepository.storeScreenshot(str(ip), str(port), + str(filename)) + imageviewer = self.view.createNewTabForHost(ip, 'screenshot (' + port + '/tcp)', True, '', + str(outputFolder) + '/screenshots/' + str(filename)) + imageviewer.setProperty('dbId', QVariant(str(dbId))) + # to make sure the screenshot tab appears when it is launched from the host services tab + self.view.switchTabClick() + #self.updateUITimer.stop() # update the processes table + #self.updateUITimer.start(900) + + def processCrashed(self, proc): + processRepository = self.logic.activeProject.repositoryContainer.processRepository + processRepository.storeProcessCrashStatus(str(proc.id)) + log.info('Process {qProcessId} Crashed!'.format(qProcessId=str(proc.id))) + qProcessOutput = "\n\t" + str(proc.display.toPlainText()).replace('\n', '').replace("b'", "") + # self.view.closeHostToolTab(self, index)) + self.view.findFinishedServiceTab(str(processRepository.getPIDByProcessId(str(proc.id)))) + log.info('Process {qProcessId} Output: {qProcessOutput}'.format(qProcessId=str(proc.id), + qProcessOutput=qProcessOutput)) + log.info('Process {qProcessId} Crash Output: {qProcessOutput}'.format(qProcessId=str(proc.id), + qProcessOutput=proc.errorString())) + + # this function handles everything after a process ends + # def processFinished(self, qProcess, crashed=False): + def processFinished(self, qProcess): + processRepository = self.logic.activeProject.repositoryContainer.processRepository + try: + if not processRepository.isKilledProcess( + str(qProcess.id)): # if process was not killed + log.debug('Process: {0}\nCommand: {1}\noutputfile: {2}'.format(str(qProcess.id), str(qProcess.command), str(qProcess.outputfile))) + if not qProcess.outputfile == '': + # move tool output from runningfolder to output folder if there was an output file + outputfile = winPath2Unix(qProcess.outputfile) + self.logic.toolCoordinator.saveToolOutput(self.logic.activeProject.properties.outputFolder, + outputfile) + if 'nmap' in qProcess.command: # if the process was nmap, use the parser to store it + if qProcess.exitCode() == 0: # if the process finished successfully + log.debug("qProcess.outputfile {0}".format(str(outputfile))) + log.debug("self.logic.activeProject.properties.runningFolder {0}".format( + str(self.logic.activeProject.properties.runningFolder))) + log.debug("self.logic.activeProject.properties.outputFolder {0}".format( + str(self.logic.activeProject.properties.outputFolder))) + newoutputfile = outputfile.replace( + self.logic.activeProject.properties.runningFolder, + self.logic.activeProject.properties.outputFolder) + self.nmapImporter.setFilename(str(newoutputfile) + '.xml') + self.nmapImporter.setOutput(str(qProcess.display.toPlainText())) + self.nmapImporter.start() + elif 'PythonScript' in qProcess.command: + pythonScript = str(qProcess.command).split(' ')[2] + print('PythonImporter running for script: {0}'.format(pythonScript)) + if qProcess.exitCode() == 0: # if the process finished successfully + self.pythonImporter.setOutput(str(qProcess.display.toPlainText())) + self.pythonImporter.setHostIp(str(qProcess.hostIp)) + self.pythonImporter.setPythonScript(pythonScript) + self.pythonImporter.start() + log.info("Process {qProcessId} is done!".format(qProcessId=qProcess.id)) + + processRepository.storeProcessOutput(str(qProcess.id), qProcess.display.toPlainText()) + + if 'hydra' in qProcess.name: # find the corresponding widget and tell it to update its UI + self.view.findFinishedBruteTab(str(processRepository.getPIDByProcessId(str(qProcess.id)))) + + try: + self.fastProcessesRunning -= 1 + self.checkProcessQueue() + self.processes.remove(qProcess) + self.updateUITimer.stop() + self.updateUITimer.start(1000) # update the interface soon + except Exception as e: + log.info("Process Finished Cleanup Exception {e}".format(e=e)) + except Exception as e: # fixes bug when receiving finished signal when project is no longer open. + log.info("Process Finished Exception {e}".format(e=e)) + raise + + # when hydra finds valid credentials we need to save them and change the brute tab title to red + def handleHydraFindings(self, bWidget, userlist, passlist): + self.view.blinkBruteTab(bWidget) + for username in userlist: + self.logic.activeProject.properties.usernamesWordList.add(username) + for password in passlist: + self.logic.activeProject.properties.passwordWordList.add(password) + + # this function parses nmap's output looking for open ports to run automated attacks on + def scheduler(self, parser, isNmapImport): + if isNmapImport and self.settings.general_enable_scheduler_on_import == 'False': + return + if self.settings.general_enable_scheduler == 'True': + log.info('Scheduler started!') + + for h in parser.getAllHosts(): + for p in h.all_ports(): + if p.state == 'open': + s = p.getService() + if not (s is None): + self.runToolsFor(s.name, h.hostname, h.ip, p.portId, p.protocol) + + log.info('-----------------------------------------------') + log.info('Scheduler ended!') + + def findDuplicateTab(self, tabWidget, tabName): + for i in range(tabWidget.count()): + log.debug("Tab text for {0}: {1}".format(str(i), str(tabWidget.tabText(i)))) + if tabWidget.tabText(i) == tabName: + return True + return False + + def runToolsFor(self, service, hostname, ip, port, protocol='tcp'): + log.info('Running tools for: ' + service + ' on ' + ip + ':' + port) + + if service.endswith("?"): # when nmap is not sure it will append a ?, so we need to remove it + service=service[:-1] + + for tool in self.settings.automatedAttacks: + if service in tool[1].split(",") and protocol==tool[2]: + if tool[0] == "screenshooter": + if hostname: + url = hostname+':'+port + else: + url = ip+':'+port + log.info("Screenshooter of URL: %s" % str(url)) + self.screenshooter.addToQueue(ip, port, url) + self.screenshooter.start() + + else: + for a in self.settings.portActions: + if tool[0] == a[1]: + tabTitle = a[1] + " (" + port + "/" + protocol + ")" + # Cheese + outputfile = self.logic.activeProject.properties.runningFolder + "/" + \ + re.sub("[^0-9a-zA-Z]", "", str(tool[0])) + \ + "/" + getTimestamp() + '-' + a[1] + "-" + ip + "-" + port + command = str(a[2]) + command = command.replace('[IP]', ip).replace('[PORT]', port)\ + .replace('[OUTPUT]', outputfile) + log.debug("Running tool command: {0}".format(str(command))) + + if self.findDuplicateTab(self.view.ui.ServicesTabWidget, tabTitle): + log.debug("Duplicate tab name. Tool might have already run.") + break + tab = self.view.ui.HostsTabWidget.tabText(self.view.ui.HostsTabWidget.currentIndex()) + self.runCommand(tool[0], tabTitle, ip, port, protocol, command, + getTimestamp(True), + outputfile, + self.view.createNewTabForHost(ip, tabTitle, not (tab == 'Hosts'))) + break diff --git a/db/RepositoryContainer.py b/db/RepositoryContainer.py new file mode 100644 index 00000000..89f6eec7 --- /dev/null +++ b/db/RepositoryContainer.py @@ -0,0 +1,36 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from typing import NamedTuple + +from db.repositories.CVERepository import CVERepository +from db.repositories.HostRepository import HostRepository +from db.repositories.NoteRepository import NoteRepository +from db.repositories.PortRepository import PortRepository +from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ScriptRepository import ScriptRepository +from db.repositories.ServiceRepository import ServiceRepository + + +class RepositoryContainer(NamedTuple): + serviceRepository: ServiceRepository + processRepository: ProcessRepository + hostRepository: HostRepository + portRepository: PortRepository + cveRepository: CVERepository + noteRepository: NoteRepository + scriptRepository: ScriptRepository diff --git a/db/RepositoryFactory.py b/db/RepositoryFactory.py new file mode 100644 index 00000000..8e821295 --- /dev/null +++ b/db/RepositoryFactory.py @@ -0,0 +1,42 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.RepositoryContainer import RepositoryContainer +from db.SqliteDbAdapter import Database +from db.repositories.CVERepository import CVERepository +from db.repositories.HostRepository import HostRepository +from db.repositories.NoteRepository import NoteRepository +from db.repositories.PortRepository import PortRepository +from db.repositories.ProcessRepository import ProcessRepository +from db.repositories.ScriptRepository import ScriptRepository +from db.repositories.ServiceRepository import ServiceRepository + + +class RepositoryFactory: + def __init__(self, logger): + self.logger = logger + + def buildRepositories(self, database: Database) -> RepositoryContainer: + hostRepository = HostRepository(database) + processRepository = ProcessRepository(database, self.logger) + serviceRepository = ServiceRepository(database) + portRepository: PortRepository = PortRepository(database) + cveRepository: CVERepository = CVERepository(database) + noteRepository: NoteRepository = NoteRepository(database, self.logger) + scriptRepository: ScriptRepository = ScriptRepository(database) + return RepositoryContainer(serviceRepository, processRepository, hostRepository, + portRepository, cveRepository, noteRepository, scriptRepository) diff --git a/db/SqliteDbAdapter.py b/db/SqliteDbAdapter.py new file mode 100644 index 00000000..1e2d5c36 --- /dev/null +++ b/db/SqliteDbAdapter.py @@ -0,0 +1,81 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +from PyQt6.QtCore import QSemaphore +import time +from random import randint + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.scoping import scoped_session + +from app.logging.legionLog import getDbLogger + + +class Database: + def __init__(self, dbfilename): + from db.database import Base + self.log = getDbLogger() + self.base = Base + try: + self.establishSqliteConnection(dbfilename) + except Exception as e: + self.log.error('Could not create SQLite database. Please try again.') + self.log.info(e) + + def openDB(self, dbfilename): + try: + self.establishSqliteConnection(dbfilename) + except: + self.log.error('Could not open SQLite database file. Is the file corrupted?') + + def establishSqliteConnection(self, dbFileName: str): + self.name = dbFileName + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + self.engine = create_engine( + 'sqlite:///{dbFileName}'.format(dbFileName=dbFileName)) + self.session = scoped_session(sessionmaker(bind=self.engine)) + self.session.configure(bind=self.engine, autoflush=False) + self.metadata = self.base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + self.log.info(f"Established SQLite connection on file '{dbFileName}'") + + def commit(self): + self.dbsemaphore.acquire() + self.log.debug("DB lock acquired") + try: + session = self.session() + rnd = float(randint(1, 99)) / 1000.00 + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + time.sleep(rnd) + session.commit() + except Exception as e: + self.log.error("DB Commit issue") + self.log.error(str(e)) + try: + rnd = float(randint(1, 99)) / 100.00 + time.sleep(rnd) + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + session.commit() + except Exception as e: + self.log.error("DB Commit issue on retry") + self.log.error(str(e)) + pass + self.dbsemaphore.release() + self.log.debug("DB lock released") diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/db/database.py b/db/database.py new file mode 100644 index 00000000..6fd13c42 --- /dev/null +++ b/db/database.py @@ -0,0 +1,19 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/db/entities/app.py b/db/entities/app.py new file mode 100644 index 00000000..3ec8e13b --- /dev/null +++ b/db/entities/app.py @@ -0,0 +1,40 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, ForeignKey, Integer + +from db.database import Base + + +class appObj(Base): + __tablename__ = 'appObj' + name = Column(String) + id = Column(Integer, primary_key=True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + cpe = Column(String) + serviceId = Column(String, ForeignKey('serviceObj.id')) + + def __init__(self, name='', product='', version='', extrainfo='', fingerprint='', cpe=''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint + self.cpe = cpe diff --git a/db/entities/cve.py b/db/entities/cve.py new file mode 100644 index 00000000..9db0c02a --- /dev/null +++ b/db/entities/cve.py @@ -0,0 +1,49 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import String, Column, Integer, ForeignKey + +from db.database import Base + + +class cve(Base): + __tablename__ = 'cve' + name = Column(String) + id = Column(Integer, primary_key=True) + url = Column(String) + product = Column(String) + severity = Column(String) + source = Column(String) + version = Column(String) + exploitId = Column(Integer) + exploit = Column(String) + exploitUrl = Column(String) + serviceId = Column(String, ForeignKey('serviceObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, name, url, product, hostId, severity='', source='', version='', exploitId=0, exploit='', + exploitUrl=''): + self.url = url + self.name = name + self.product = product + self.severity = severity + self.source = source + self.version = version + self.exploitId = exploitId + self.exploit = exploit + self.exploitUrl = exploitUrl + self.hostId = hostId diff --git a/db/entities/host.py b/db/entities/host.py new file mode 100644 index 00000000..4486bf88 --- /dev/null +++ b/db/entities/host.py @@ -0,0 +1,91 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship + +from db.database import Base +from db.entities.cve import cve +from db.entities.os import osObj +from db.entities.port import portObj + + +class hostObj(Base): + __tablename__ = 'hostObj' + # State + state = Column(String) + count = Column(String) + checked = Column(String) + + # OS + osMatch = Column(String) + osAccuracy = Column(String) + vendor = Column(String) + uptime = Column(String) + lastboot = Column(String) + + # Network + isp = Column(String) + asn = Column(String) + ip = Column(String) + ipv4 = Column(String) + ipv6 = Column(String) + macaddr = Column(String) + status = Column(String) + hostname = Column(String) + + # ID + hostId = Column(String) + id = Column(Integer, primary_key=True) + + # Location + city = Column(String) + countryCode = Column(String) + postalCode = Column(String) + longitude = Column(String) + latitude = Column(String) + distance = Column(String) + + # host relationships + os = relationship(osObj) + ports = relationship(portObj) + cves = relationship(cve) + + def __init__(self, **kwargs): + self.checked = kwargs.get('checked') or 'False' + self.osMatch = kwargs.get('osMatch') or 'unknown' + self.osAccuracy = kwargs.get('osAccuracy') or 'NaN' + self.ip = kwargs.get('ip') or 'unknown' + self.ipv4 = kwargs.get('ipv4') or 'unknown' + self.ipv6 = kwargs.get('ipv6') or 'unknown' + self.macaddr = kwargs.get('macaddr') or 'unknown' + self.status = kwargs.get('status') or 'unknown' + self.hostname = kwargs.get('hostname') or 'unknown' + self.hostId = kwargs.get('hostname') or 'unknown' + self.vendor = kwargs.get('vendor') or 'unknown' + self.uptime = kwargs.get('uptime') or 'unknown' + self.lastboot = kwargs.get('lastboot') or 'unknown' + self.distance = kwargs.get('distance') or 'unknown' + self.state = kwargs.get('state') or 'unknown' + self.count = kwargs.get('count') or 'unknown' + self.city = kwargs.get('city') or 'unknown' + self.countryCode = kwargs.get('countryCode') or 'unknown' + self.postalCode = kwargs.get('postalCode') or 'unknown' + self.longitude = kwargs.get('longitude') or 'unknown' + self.latitude = kwargs.get('latitude') or 'unknown' + self.isp = kwargs.get('isp') or 'unknown' + self.asn = kwargs.get('asn') or 'unknown' diff --git a/db/entities/l1script.py b/db/entities/l1script.py new file mode 100644 index 00000000..3e3c53d6 --- /dev/null +++ b/db/entities/l1script.py @@ -0,0 +1,36 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, Integer, ForeignKey + +from db.database import Base +from six import u as unicode + + +class l1ScriptObj(Base): + __tablename__ = 'l1ScriptObj' + scriptId = Column(String) + id = Column(Integer, primary_key=True) + output = Column(String) + portId = Column(String, ForeignKey('portObj.id')) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, scriptId, output, portId, hostId): + self.scriptId = scriptId + self.output = unicode(output) + self.portId = portId + self.hostId = hostId diff --git a/db/entities/nmapSession.py b/db/entities/nmapSession.py new file mode 100644 index 00000000..7415ab4d --- /dev/null +++ b/db/entities/nmapSession.py @@ -0,0 +1,42 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import String, Column + +from db.database import Base + + +class nmapSessionObj(Base): + __tablename__ = 'nmapSessionObj' + filename = Column(String, primary_key=True) + startTime = Column(String) + finish_time = Column(String) + nmapVersion = Column(String) + scanArgs = Column(String) + totalHosts = Column(String) + upHosts = Column(String) + downHosts = Column(String) + + def __init__(self, filename, *args, **kwargs): + self.filename = filename + self.startTime = args[0] + self.finish_time = args[1] + self.nmapVersion = kwargs.get('nmapVersion') or 'unknown' + self.scanArgs = kwargs.get('scanArgs') or '' + self.totalHosts = kwargs.get('total_host') or '0' + self.upHosts = kwargs.get('upHosts') or '0' + self.downHosts = kwargs.get('downHosts') or '0' diff --git a/db/entities/note.py b/db/entities/note.py new file mode 100644 index 00000000..8beb78a1 --- /dev/null +++ b/db/entities/note.py @@ -0,0 +1,32 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, Integer, ForeignKey, String + +from db.database import Base +from six import u as unicode + + +class note(Base): + __tablename__ = 'note' + hostId = Column(Integer, ForeignKey('hostObj.id')) + id = Column(Integer, primary_key=True) + text = Column(String) + + def __init__(self, hostId, text): + self.text = unicode(text) + self.hostId = hostId diff --git a/db/entities/os.py b/db/entities/os.py new file mode 100644 index 00000000..e137419d --- /dev/null +++ b/db/entities/os.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Integer, Column, String, ForeignKey + +from db.database import Base + + +class osObj(Base): + __tablename__ = 'osObj' + id = Column(Integer, primary_key=True) + name = Column(String) + family = Column(String) + generation = Column(String) + osType = Column(String) + vendor = Column(String) + accuracy = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) + + def __init__(self, name, *args): + self.name = name + self.family = args[0] + self.generation = args[1] + self.osType = args[2] + self.vendor = args[3] + self.accuracy = args[4] + self.hostId = args[5] diff --git a/db/entities/port.py b/db/entities/port.py new file mode 100644 index 00000000..7f6853ea --- /dev/null +++ b/db/entities/port.py @@ -0,0 +1,38 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Integer, Column, String, ForeignKey + +from db.database import Base + + +class portObj(Base): + __tablename__ = 'portObj' + portId = Column(String) + id = Column(Integer, primary_key=True) + protocol = Column(String) + state = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) + serviceId = Column(String, ForeignKey('serviceObj.id')) + scriptId = Column(String, ForeignKey('l1ScriptObj.id')) + + def __init__(self, portId, protocol, state, host, service=''): + self.portId = portId + self.protocol = protocol + self.state = state + self.serviceId = service + self.hostId = host diff --git a/db/entities/process.py b/db/entities/process.py new file mode 100644 index 00000000..5bcf5e4c --- /dev/null +++ b/db/entities/process.py @@ -0,0 +1,60 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship + +from db.database import Base + + +class process(Base): + __tablename__ = 'process' + pid = Column(String) + id = Column(Integer, primary_key = True) + display = Column(String) + name = Column(String) + tabTitle = Column(String) + hostIp = Column(String) + port = Column(String) + protocol = Column(String) + command = Column(String) + startTime = Column(String) + endTime = Column(String) + estimatedRemaining = Column(Integer) + elapsed = Column(Integer) + outputfile = Column(String) + output = relationship("process_output") + status = Column(String) + closed = Column(String) + + def __init__(self, pid, *args): + self.display = 'True' + self.pid = pid + self.name = args[0] + self.tabTitle = args[1] + self.hostIp = args[2] + self.port = args[3] + self.protocol = args[4] + self.command = args[5] + self.startTime = args[6] + self.endTime = args[7] + self.outputfile = args[8] + self.output = args[10] + self.status = args[9] + self.closed = 'False' + self.estimatedRemaining = args[11] + self.elapsed = args[12] \ No newline at end of file diff --git a/db/entities/processOutput.py b/db/entities/processOutput.py new file mode 100644 index 00000000..e0dc72e5 --- /dev/null +++ b/db/entities/processOutput.py @@ -0,0 +1,31 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import Column, Integer, ForeignKey, String + +from db.database import Base +from six import u as unicode + + +class process_output(Base): + __tablename__ = 'process_output' + processId = Column(Integer, ForeignKey('process.id')) + id = Column(Integer, primary_key=True) + output = Column(String) + + def __init__(self): + self.output = unicode('') diff --git a/db/entities/service.py b/db/entities/service.py new file mode 100644 index 00000000..efebe99f --- /dev/null +++ b/db/entities/service.py @@ -0,0 +1,46 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from sqlalchemy import String, Column, Integer, ForeignKey +from sqlalchemy.orm import relationship + +from db.database import Base +from db.entities.app import appObj +from db.entities.cve import cve +from db.entities.port import portObj + + +class serviceObj(Base): + __tablename__ = 'serviceObj' + name = Column(String) + id = Column(Integer, primary_key=True) + product = Column(String) + version = Column(String) + extrainfo = Column(String) + fingerprint = Column(String) + hostId = Column(String, ForeignKey('hostObj.id')) + port = relationship(portObj) + cves = relationship(cve) + application = relationship(appObj) + + def __init__(self, name, host, product='', version='', extrainfo='', fingerprint=''): + self.name = name + self.product = product + self.version = version + self.extrainfo = extrainfo + self.fingerprint = fingerprint + self.hostId = host diff --git a/db/filters.py b/db/filters.py new file mode 100644 index 00000000..8983b944 --- /dev/null +++ b/db/filters.py @@ -0,0 +1,53 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from db.validation import sanitise + + +def applyFilters(filters): + query_filter = "" + query_filter += applyHostsFilters(filters) + query_filter += applyPortFilters(filters) + return query_filter + +def applyHostsFilters(filters): + query_filter = "" + if not filters.down: + query_filter += " AND hosts.status != 'down'" + if not filters.up: + query_filter += " AND hosts.status != 'up'" + if not filters.checked: + query_filter += " AND hosts.checked != 'True'" + for word in filters.keywords: + query_filter += (f" AND (hosts.ip LIKE '%{sanitise(word)}%'" + f" OR hosts.osMatch LIKE '%{sanitise(word)}%'" + f" OR hosts.hostname LIKE '%{sanitise(word)}%')") + return query_filter + +def applyPortFilters(filters): + query_filter = "" + if not filters.portopen: + query_filter += " AND ports.state != 'open' AND ports.state != 'open|filtered'" + if not filters.portclosed: + query_filter += " AND ports.state != 'closed'" + if not filters.portfiltered: + query_filter += " AND ports.state != 'filtered' AND ports.state != 'open|filtered'" + if not filters.tcp: + query_filter += " AND ports.protocol != 'tcp'" + if not filters.udp: + query_filter += " AND ports.protocol != 'udp'" + return query_filter diff --git a/db/postgresDbAdapter.py b/db/postgresDbAdapter.py new file mode 100644 index 00000000..7978e398 --- /dev/null +++ b/db/postgresDbAdapter.py @@ -0,0 +1,90 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +from PyQt6.QtCore import QSemaphore +import time +from random import randint + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.scoping import scoped_session + +from app.logging.legionLog import getDbLogger + + +class Database: + def __init__(self, user: str, passw: str, db: str, host='localhost', port=5432): + from db.database import Base + self.log = getDbLogger() + self.base = Base + try: + self.establishSqliteConnection(user, passw. db. host, port) + except Exception as e: + self.log.error('Could not create SQLite database. Please try again.') + self.log.info(e) + + def openDB(self, dbfilename): + try: + self.log.error('Not implemented for Postgres yet.') + except: + self.log.error('Could not open SQLite database file. Is the file corrupted?') + + def establishSqliteConnection(self, user: str, passw: str, db: str, host='localhost', port=5432): + self.name = db + self.port = port + self.host = host + self.user = user + self.passw = passw + self.dbsemaphore = QSemaphore(1) # to control concurrent write access to db + url = 'postgresql://{}:{}@{}:{}/{}' + url = url.format(user, password, host, port, db) + # The return value of create_engine() is our connection object + self.engine = sqlalchemy.create_engine( + url, client_encoding='utf8') + # We then bind the connection to MetaData() + #meta = sqlalchemy.MetaData(bind=con, reflect=True) + self.session = scoped_session(sessionmaker()) + self.session.configure(bind=self.engine, autoflush=False) + self.metadata = self.base.metadata + self.metadata.create_all(self.engine) + self.metadata.echo = True + self.metadata.bind = self.engine + self.log.info(f"Established SQLite connection on file '{dbFileName}'") + + def commit(self): + self.dbsemaphore.acquire() + self.log.debug("DB lock acquired") + try: + session = self.session() + rnd = float(randint(1, 99)) / 1000.00 + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + time.sleep(rnd) + session.commit() + except Exception as e: + self.log.error("DB Commit issue") + self.log.error(str(e)) + try: + rnd = float(randint(1, 99)) / 100.00 + time.sleep(rnd) + self.log.debug("Waiting {0}s before commit...".format(str(rnd))) + session.commit() + except Exception as e: + self.log.error("DB Commit issue on retry") + self.log.error(str(e)) + pass + self.dbsemaphore.release() + self.log.debug("DB lock released") diff --git a/db/repositories/CVERepository.py b/db/repositories/CVERepository.py new file mode 100644 index 00000000..92aec4d8 --- /dev/null +++ b/db/repositories/CVERepository.py @@ -0,0 +1,35 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from sqlalchemy import text +from db.SqliteDbAdapter import Database + + +class CVERepository: + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter + + def getCVEsByHostIP(self, hostIP): + session = self.dbAdapter.session() + query = text('SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, ' + 'cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves ' + 'INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId ' + 'WHERE hosts.ip = :hostIP') + result = session.execute(query, {'hostIP': str(hostIP)}).fetchall() + session.close() + return result diff --git a/db/repositories/HostRepository.py b/db/repositories/HostRepository.py new file mode 100644 index 00000000..65d7bfc5 --- /dev/null +++ b/db/repositories/HostRepository.py @@ -0,0 +1,84 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from app.auxiliary import Filters +from sqlalchemy import text +from db.SqliteDbAdapter import Database +from db.entities.host import hostObj +from db.filters import applyFilters, applyHostsFilters + + +class HostRepository: + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter + + def exists(self, host: str): + session = self.dbAdapter.session() + query = text('SELECT host.ip FROM hostObj AS host WHERE host.ip == :host OR host.hostname == :host') + result = session.execute(query, {'host': str(host)}).fetchall() + session.close() + return True if result else False + + def getHosts(self, filters): + session = self.dbAdapter.session() + query = 'SELECT * FROM hostObj AS hosts WHERE 1=1' + query += applyHostsFilters(filters) + query = text(query) + result = session.execute(query).fetchall() + session.close() + return result + + def getHostsAndPortsByServiceName(self, service_name, filters: Filters): + session = self.dbAdapter.session() + query = ("SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId," + "services.name,services.product,services.version,services.extrainfo,services.fingerprint " + "FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + "WHERE services.name=:service_name") + query += applyFilters(filters) + query = text(query) + result = session.execute(query, {'service_name': str(service_name)}).fetchall() + session.close() + return result + + def getHostInformation(self, host_ip_address: str): + session = self.dbAdapter.session() + result = session.query(hostObj).filter_by(ip=str(host_ip_address)).first() + session.close() + return result + + def deleteHost(self, hostIP): + session = self.dbAdapter.session() + host = session.query(hostObj).filter_by(ip=str(hostIP)).first() + if host: + session.delete(host) + session.commit() + session.close() + + def toggleHostCheckStatus(self, ipAddress): + session = self.dbAdapter.session() + host = session.query(hostObj).filter_by(ip=ipAddress).first() + if host: + if host.checked == 'False': + host.checked = 'True' + else: + host.checked = 'False' + session.add(host) + session.commit() + session.close() diff --git a/db/repositories/NoteRepository.py b/db/repositories/NoteRepository.py new file mode 100644 index 00000000..02f5b1b0 --- /dev/null +++ b/db/repositories/NoteRepository.py @@ -0,0 +1,48 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from db.SqliteDbAdapter import Database +from six import u as unicode + +from db.entities.note import note + + +class NoteRepository: + def __init__(self, dbAdapter: Database, log): + self.dbAdapter = dbAdapter + self.log = log + + def getNoteByHostId(self, hostId): + session = self.dbAdapter.session() + result = session.query(note).filter_by(hostId=str(hostId)).first() + session.close() + return result + + def storeNotes(self, hostId, notes): + session = self.dbAdapter.session() + if len(notes) == 0: + notes = unicode("".format(hostId=hostId)) + self.log.debug("Storing notes for {hostId}, Notes {notes}".format(hostId=hostId, notes=notes)) + t_note = self.getNoteByHostId(hostId) + if t_note: + t_note.text = unicode(notes) + else: + t_note = note(hostId, unicode(notes)) + session.add(t_note) + session.commit() + session.close() diff --git a/db/repositories/PortRepository.py b/db/repositories/PortRepository.py new file mode 100644 index 00000000..047e116a --- /dev/null +++ b/db/repositories/PortRepository.py @@ -0,0 +1,71 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from sqlalchemy import text +from db.SqliteDbAdapter import Database +from db.entities.l1script import l1ScriptObj +from db.entities.port import portObj +from db.filters import applyPortFilters + + +class PortRepository: + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter + + def getPortsByIPAndProtocol(self, host_ip, protocol): + session = self.dbAdapter.session() + query = text("SELECT ports.portId FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "WHERE hosts.ip = :host_ip and ports.protocol = :protocol") + result = session.execute(query, {'host_ip': str(host_ip), 'protocol': str(protocol)}).first() + session.close() + return result + + def getPortStatesByHostId(self, host_id): + session = self.dbAdapter.session() + query = text('SELECT port.state FROM portObj as port WHERE port.hostId = :host_id') + result = session.execute(query, {'host_id': str(host_id)}).fetchall() + session.close() + return result + + def getPortsAndServicesByHostIP(self, host_ip, filters): + session = self.dbAdapter.session() + query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId WHERE hosts.ip = :host_ip") + query += applyPortFilters(filters) + query = text(query) + result = session.execute(query, {'host_ip': str(host_ip)}).fetchall() + session.close() + return result + + # used to delete all port/script data related to a host - to overwrite portscan info with the latest scan + def deleteAllPortsAndScriptsByHostId(self, hostID, protocol): + session = self.dbAdapter.session() + ports_for_host = session.query(portObj)\ + .filter(portObj.hostId == hostID)\ + .filter(portObj.protocol == str(protocol)).all() + + for p in ports_for_host: + scripts_for_ports = session.query(l1ScriptObj).filter(l1ScriptObj.portId == p.id).all() + for s in scripts_for_ports: + session.delete(s) + for p in ports_for_host: + session.delete(p) + session.commit() + session.close() diff --git a/db/repositories/ProcessRepository.py b/db/repositories/ProcessRepository.py new file mode 100644 index 00000000..79b6df9a --- /dev/null +++ b/db/repositories/ProcessRepository.py @@ -0,0 +1,221 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from typing import Union + +from six import u as unicode + +from app.timing import getTimestamp +from sqlalchemy import text +from db.SqliteDbAdapter import Database +from db.entities.process import process +from db.entities.processOutput import process_output + + +class ProcessRepository: + def __init__(self, dbAdapter: Database, log): + self.dbAdapter = dbAdapter + self.log = log + + # the showProcesses flag is used to ensure we don't display processes in the process table after we have cleared + # them or when an existing project is opened. + # to speed up the queries we replace the columns we don't need by zeros (the reason we need all the columns is + # we are using the same model to display process information everywhere) + + def getProcesses(self, filters, showProcesses: Union[str, bool] = 'noNmap', sort: str = 'desc', ncol: str = 'id'): + # we do not fetch nmap processes because these are not displayed in the host tool tabs / tools + session = self.dbAdapter.session() + if showProcesses == 'noNmap': + query = text('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' + 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' + 'GROUP BY process.name') + result = session.execute(query).fetchall() + elif not showProcesses: + query = text('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' + 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' + 'WHERE process.display = :display AND process.closed = "False" order by process.id desc') + result = session.execute(query, {'display': str(showProcesses)}).fetchall() + else: + query = text('SELECT * FROM process AS process WHERE process.display=:display order by {0} {1}'.format(ncol, sort)) + result = session.execute(query, {'display': str(showProcesses)}).fetchall() + session.close() + return result + + def storeProcess(self, proc): + session = self.dbAdapter.session() + p_output = process_output() + + #p = process(str(proc.processId()), str(proc.name), str(proc.tabTitle), + p = process(str(proc.processId()), str(proc.name), str(proc.tabTitle), + str(proc.hostIp), str(proc.port), str(proc.protocol), + unicode(proc.command), proc.startTime, "", str(proc.outputfile), + 'Waiting', [p_output], 100, 0) + + self.log.info(f"Adding process: {p}") + session.add(p) + session.commit() + proc.id = p.id + session.close() + return proc.id + + def storeProcessOutput(self, process_id: str, output: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=process_id).first() + + if not proc: + session.close() + return False + + proc_output = session.query(process_output).filter_by(id=process_id).first() + if proc_output: + self.log.info("Storing process output into db: {0}".format(str(proc_output))) + proc_output.output = unicode(output) + session.add(proc_output) + + proc.endTime = getTimestamp(True) + + if proc.status == "Killed" or proc.status == "Cancelled" or proc.status == "Crashed": + #session.commit() # Needed? + session.close() + return True + else: + proc.status = 'Finished' + session.add(proc) + session.commit() + session.close() + + def getStatusByProcessId(self, process_id: str): + return self.getFieldByProcessId("status", process_id) + + def getPIDByProcessId(self, process_id: str): + return self.getFieldByProcessId("pid", process_id) + + def isKilledProcess(self, process_id: str) -> bool: + status = self.getFieldByProcessId("status", process_id) + return True if status == "Killed" else False + + def isCancelledProcess(self, process_id: str) -> bool: + status = self.getFieldByProcessId("status", process_id) + return True if status == "Cancelled" else False + + def getFieldByProcessId(self, field_name: str, process_id: str): + session = self.dbAdapter.session() + query = text("SELECT process.{0} FROM process AS process WHERE process.id=:process_id".format(field_name)) + p = session.execute(query, {'process_id': str(process_id)}).fetchall() + result = p[0][0] if p else -1 + session.close() + return result + + def getHostsByToolName(self, toolName: str, closed: str = "False"): + session = self.dbAdapter.session() + if closed == 'FetchAll': + query = text('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' + 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=:toolName') + else: + query = text('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' + 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' + 'WHERE process.name=:toolName and process.closed="False"') + result = session.execute(query, {'toolName': str(toolName)}).fetchall() + session.close() + return result + + def storeProcessCrashStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc and not proc.status == 'Killed' and not proc.status == 'Cancelled': + proc.status = 'Crashed' + proc.endTime = getTimestamp(True) + session.add(proc) + session.commit() + session.close() + + def storeProcessCancelStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.status = 'Cancelled' + proc.endTime = getTimestamp(True) + session.add(proc) + session.commit() + session.close() + + def storeProcessKillStatus(self, processId: str): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc and not proc.status == 'Finished': + proc.status = 'Killed' + proc.endTime = getTimestamp(True) + session.add(proc) + session.commit() + session.close() + + def storeProcessRunningStatus(self, processId: str, pid): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.status = 'Running' + proc.pid = str(pid) + session.add(proc) + session.commit() + session.close() + + def storeProcessRunningElapsedTime(self, processId: str, elapsed): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.elapsed = elapsed + session.add(proc) + session.commit() + + def storeCloseStatus(self, processId): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(id=processId).first() + if proc: + proc.closed = 'True' + session.add(proc) + session.commit() + session.close() + + def storeScreenshot(self, ip: str, port: str, filename: str): + session = self.dbAdapter.session() + p = process(0, "screenshooter", "screenshot (" + str(port) + "/tcp)", str(ip), str(port), "tcp", "", + getTimestamp(True), getTimestamp(True), str(filename), "Finished", [process_output()], 2, 0) + if p: + session.add(p) + session.commit() + pD = p.id + session.close() + return pD + + def toggleProcessDisplayStatus(self, resetAll=False): + session = self.dbAdapter.session() + proc = session.query(process).filter_by(display='True').all() + for p in proc: + session.add(self.toggleProcessStatusField(p, resetAll)) + session.commit() + session.close() + + @staticmethod + def toggleProcessStatusField(p, reset_all): + not_running = p.status != 'Running' + not_waiting = p.status != 'Waiting' + + if (reset_all and not_running) or (not_running and not_waiting): + p.display = 'False' + + return p diff --git a/db/repositories/ScriptRepository.py b/db/repositories/ScriptRepository.py new file mode 100644 index 00000000..61fd10fd --- /dev/null +++ b/db/repositories/ScriptRepository.py @@ -0,0 +1,40 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +from sqlalchemy import text +from db.SqliteDbAdapter import Database + +class ScriptRepository: + def __init__(self, dbAdapter: Database): + self.dbAdapter = dbAdapter + + def getScriptsByHostIP(self, hostIP): + session = self.dbAdapter.session() + query = text("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " + "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " + "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=:hostIP") + result = session.execute(query, {'hostIP': str(hostIP)}).fetchall() + session.close() + return result + + def getScriptOutputById(self, scriptDBId): + session = self.dbAdapter.session() + query = text("SELECT script.output FROM l1ScriptObj as script WHERE script.id = :scriptDBId") + result = session.execute(query, {'scriptDBId': str(scriptDBId)}).fetchall() + session.close() + return result diff --git a/db/repositories/ServiceRepository.py b/db/repositories/ServiceRepository.py new file mode 100644 index 00000000..670181c2 --- /dev/null +++ b/db/repositories/ServiceRepository.py @@ -0,0 +1,49 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import Filters +from sqlalchemy import text +from db.SqliteDbAdapter import Database +from db.filters import applyFilters + + +class ServiceRepository: + def __init__(self, db_adapter: Database): + self.dbAdapter = db_adapter + + def getServiceNames(self, filters: Filters): + session = self.dbAdapter.session() + query = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1") + query += applyFilters(filters) + query += ' ORDER BY service.name ASC' + query = text(query) + result = session.execute(query).fetchall() + session.close() + return result + + def getServiceNamesByHostIPAndPort(self, host_ip, port): + session = self.dbAdapter.session() + query = text("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=:host_ip and ports.portId = :port") + result = session.execute(query, {'host_ip': str(host_ip), 'port': str(port)}).first() + session.close() + return result diff --git a/db/validation.py b/db/validation.py new file mode 100644 index 00000000..1a966674 --- /dev/null +++ b/db/validation.py @@ -0,0 +1,23 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + + +# this function makes a string safe for use in sql query. the main point is to prevent us from breaking, +# not so much SQLi as such. +def sanitise(string: str) -> str: + return string.replace('\'', '\'\'') diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..83b2aaa6 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,43 @@ +legion (0.4.0-0) UNRELEASED; urgency=medium + + * Refactored to Python 3.8+ + * Refactored to PyQt 6 + * Database calls migrated to sessions (dramatically improves performance, reliability and huge memory reductions) + * Refactored Logging + * General cleanup + * Screenshot tool revised to use PhantomJs webdriver (other webdrivers coming soon) + * Simplify startup scripts, dependancy installations scripts, etc + * Eliminate support for distributions other than Kali 2022 and later or Ubuntu 20.04 or later + * Improved WSL support (handling of path conversions for calling the Windows NMAP installation from the program) + + -- Shane Scott Thur, 09 Nov 2023 19:04:55 -0600 + +legion (0.4.1-0) UNRELEASED; urgency=medium + + * Add checkXserver.sh to help users troubleshoot connections to X + * Fix a few missing dependencies + + -- Shane Scott Thur, 13 Nov 2023 10:54:55 -0600 + +legion (0.4.2-0) UNRELEASED; urgency=medium + + * Tweak the screenshooter to use eyewitness as suggested by daniruiz + * Add a Wsl check before running unixPath2Win + * Include Revision by daniruiz to tempPath creation routine + * Revise to monospaced font to improve readability as suggested by daniruiz + * Revise dependancies to resolve missing PhantomJs import + * Set log level to Info + * Eliminate some temporary code, debug lines, and other cleanup + * Revise screenshooter to use schema://ip:port when url is a single node + * Fix typo in startLegion.sh + + -- Shane Scott Mon, 20 Nov 2023 12:50:55 -0600 + +legion (0.4.3-0) UNRELEASED; urgency=medium + + * Revise NMAP import process + * Fix import progress calculations + * Doubleckick to copy hostname (Linux only) + * Script to generate huge bogus NMAP XML imports for testing. + + -- Shane Scott Mon, 20 Nov 2023 19:17:00 -0600 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..cbdca1d1 --- /dev/null +++ b/debian/control @@ -0,0 +1,45 @@ +Source: legion +Section: misc +Priority: optional +Maintainer: Hackman238 +Uploaders: Shane Scott +Build-Depends: debhelper, python3, python3-requests +Standards-Version: 0.4.3 +Homepage: https://github.com/Hackman238/Legion + +Package: legion +Architecture: all +Depends: ${misc:Depends}, + python3, + nmap, + finger, + hydra, + nikto, + nbtscan, + nfs-common, + rpcbind, + smbclient, + sra-toolkit + ldap-utils, + sslscan, + rwho, + rsh-client, + x11-apps, + cutycapt, + featherpad, + xvfb, + imagemagick, + eog, + hping3, + sqlmap, + wapiti, + libqt5core5a, + python3-pip, + ruby, + perl, + urlscan, + git, + xsltproc, + python3-impacket, + whatweb, + medusa diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..0f33153c --- /dev/null +++ b/debian/copyright @@ -0,0 +1,86 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: legion +Source: https://github.com/Hackman238/Legion + +Files: * +Copyright: 2024 Hackman238 +License: GPL-3+ + +Files: scripts/rdp-sec-check.pl +Copyright: 2014 Mark lowe +License: Special + This tool may be used for legal purposes only. Users take full responsibility + for any actions performed using this tool. The author accepts no liability + for damage caused by this tool. If these terms are not acceptable to you, then + do not use this tool. + . + In all other respects the GPL version 2 applies: + . + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +Files: scripts/ndr.py +Copyright: 2007 Cody Pierce +License: BSD-3-clause + All rights reserved. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + . + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Files: debian/* +Copyright: 2024 Hackman238 +License: GPL-3+ + +License: GPL-3+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/debian/docs b/debian/docs new file mode 100644 index 00000000..b43bf86b --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/debian/legion.install b/debian/legion.install new file mode 100644 index 00000000..f108bd88 --- /dev/null +++ b/debian/legion.install @@ -0,0 +1,11 @@ +app usr/share/legion/ +controller usr/share/legion/ +db usr/share/legion/ +images usr/share/legion/ +parsers usr/share/legion/ +scripts usr/share/legion/ +legion.py usr/share/legion/ +ui usr/share/legion/ +wordlists usr/share/legion/ +legion usr/bin/ +legion.conf etc/ diff --git a/debian/legion.links b/debian/legion.links new file mode 100644 index 00000000..66d30b47 --- /dev/null +++ b/debian/legion.links @@ -0,0 +1 @@ +etc/legion.conf usr/share/legion/legion.conf diff --git a/debian/rules b/debian/rules new file mode 100644 index 00000000..05b0cfe6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f + +PACKAGE_DIR=debian/legion/usr/share/legion +%: + dh $@ + +override_dh_install: + PYTHONPATH=. python3 app/settings.py + dh_install -X.pyc + + +override_dh_fixperms: + dh_fixperms + chmod 755 debian/legion/usr/share/legion/scripts/* + chmod 755 debian/legion/usr/share/legion/deps/* + chmod 755 debian/legion/usr/share/legion/startLegion.sh diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 00000000..3e6e3dd6 --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=0.4.0 +opts="filenamemangle=s/.*\/v(\d.*).tar.gz/legion-$1.tar.gz/" \ +https://github.com/Hackman238/Legion/releases .*/v(\d.*)\.tar\.gz diff --git a/deps/apt.sh b/deps/apt.sh new file mode 100755 index 00000000..b5b8e55d --- /dev/null +++ b/deps/apt.sh @@ -0,0 +1,56 @@ +function trimString() +{ + local -r string="${1}" + + sed -e 's/^ *//g' -e 's/ *$//g' <<< "${string}" +} + +function isEmptyString() +{ + local -r string="${1}" + + if [[ "$(trimString "${string}")" = '' ]] + then + echo 'true' + else + echo 'false' + fi +} + +function info() +{ + local -r message="${1}" + + echo -e "\033[1;36m${message}\033[0m" 2>&1 +} + +function getLastAptGetUpdate() +{ + local aptDate="$(stat -c %Y '/var/cache/apt')" + local nowDate="$(date +'%s')" + + echo $((nowDate - aptDate)) +} + +function runAptGetUpdate() +{ + local updateInterval="${1}" + + local lastAptGetUpdate="$(getLastAptGetUpdate)" + + if [[ "$(isEmptyString "${updateInterval}")" = 'true' ]] + then + # Default To 24 hours + updateInterval="$((24 * 60 * 60))" + fi + + if [[ "${lastAptGetUpdate}" -gt "${updateInterval}" ]] + then + info "apt-get update" + apt-get update -m + else + local lastUpdate="$(date -u -d @"${lastAptGetUpdate}" +'%-Hh %-Mm %-Ss')" + + info "\nSkip apt-get update because its last run was '${lastUpdate}' ago" + fi +} diff --git a/deps/checkXserver.sh b/deps/checkXserver.sh new file mode 100755 index 00000000..98cbb2e2 --- /dev/null +++ b/deps/checkXserver.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +checkWsl() { +# Check if WSL is in use +if grep -q Microsoft /proc/version; then + if grep -q Microsoft-standard /proc/version; then + ip addr show eth0 | grep -q "172\. " + if [ $? -eq 0 ]; then + echo "WSL 2 detected. You are using a bridged network configuration." + echo "Verify your network configuration and %userprofile%\\.wslconfig." + echo "Make sure XMing is running. Check the XMing long." + echo "When you start XMing, make sure access control is turned off unless you've setup a cookie." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + else + echo "WSL 2 detected. You are using a NAT-only network configuration." + echo "You cannot reach Xming on the Windows desktop easily this way. Please switch to a bridged network configuration." + echo "See https://learn.microsoft.com/en-us/windows/wsl/wsl-config and https://blog.alexbal.com/2022/01/26/12/." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + fi + else + echo "WSL 1 detected. Verify your network configuration and /etc/wsl.conf." + echo "Make sure XMing is running. Check the XMing long." + echo "When you start XMing, make sure access control is turned off unless you've setup a cookie." + echo "Ultimately the Xorg port cannot be reached or being rejected." + exit 1 + fi +fi +} + +# Check if DISPLAY variable is set +if [ -z "$DISPLAY" ]; then + echo "DISPLAY variable is not set. Please export the DISPLAY variable and try again." + exit 1 +fi + +# Extract the host and port from the DISPLAY variable +xorgHost=$(echo $DISPLAY | sed 's/:.*//') + +# Check if the Xorg port is open +nc -zv $xorgHost 6000 &> /dev/null +if [ $? -ne 0 ]; then + echo "Cannot reach the X server at $xorgHost:6000. Please check your X server configuration and verify your DISPLAY variable has been exported correctly." + checkWsl + exit 1 +fi + +echo "X server is reachable. You're good to go!" diff --git a/deps/detectOs.sh b/deps/detectOs.sh new file mode 100755 index 00000000..53ee5aff --- /dev/null +++ b/deps/detectOs.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +unameOutput=`uname -a` +releaseVersion=`grep 'VERSION_ID' /etc/os-release | cut -d '"' -f 2` +releaseName=`grep "^NAME=\"" /etc/os-release | cut -d '"' -f 2` +wslEnv="" + +# Detect WSL and enable XForwaridng to Xming +if [[ ${unameOutput} == *"Microsoft"* ]] +then + export DISPLAY=localhost:0.0 + wslEnv="WSL" +fi + +echo "Detected ${releaseName} ${releaseVersion} ${wslEnv}" + +# Figure Linux Version +if [[ ${releaseName} == *"Ubuntu"* ]] +then + if [[ ${releaseVersion} != *"20.04"* ]] && [[ ${releaseVersion} != *"20.10"* ]] && [[ ${releaseVersion} != *"21."* ]] && [[ ${releaseVersion} != *"22."* ]] && [[ ${releaseVersion} != *"23."* ]] + then + echo "Unsupported Ubuntu version. Please use Ubuntu 20.04 or later." + exit 1 + else + echo "Some tools are not available under Ubuntu. Run under Kali if you're missing something." + fi +elif [[ ${releaseName} == *"Kali"* ]] +then + if [[ ${releaseVersion} != *"2022"* ]] && [[ ${releaseVersion} != *"2023"* ]] + then + echo "Unsupported Kali version. Please use Kali 2022 or later." + exit 1 + fi +else + echo "Unsupported distrubution, version or both." + exit 1 +fi + +export OS_RELEASE=${releaseName} +export OS_RELEASE_VERSION=${releaseVersion} +export ISWSL=${wslEnv} diff --git a/deps/detectPython.sh b/deps/detectPython.sh new file mode 100755 index 00000000..15b75128 --- /dev/null +++ b/deps/detectPython.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +echo "Checking Apt..." +# runAptGetUpdate +apt-get update -m + +echo "Installing bcs..." +export DEBIAN_FRONTEND="noninteractive" +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install bc + +# Check if python or python3 is installed +if command -v python &> /dev/null || command -v python3 &> /dev/null +then + # Get the python or python3 version and path + if command -v python &> /dev/null + then + python_version=$(python --version | awk -F '[ ]' '{print $2}' | awk -F '[.]' '{print $1"."$2}') + python_path=$(which python) + else + python_version=$(python3 --version | awk -F '[ ]' '{print $2}' | awk -F '[.]' '{print $1"."$2}') + python_path=$(which python3) + fi + echo "Python version: $python_version" + echo "Python path: $python_path" + + # Check if the python version is 3.8 or later + if (( $(echo "$python_version >= 3.8" |bc -l) )) + then + export PYTHON3BIN=$python_path + echo "PYTHON3BIN is set to $PYTHON3BIN" + else + echo "Your Python version is below 3.8, which may cause compatibility issues with some packages." + fi +else + echo "Python is not installed." +fi + +# Check if pip or pip3 is installed +if command -v pip &> /dev/null || command -v pip3 &> /dev/null +then + # Get the pip or pip3 version and path + if command -v pip &> /dev/null + then + pip_version=$(pip --version) + pip_path=$(which pip) + else + pip_version=$(pip3 --version) + pip_path=$(which pip3) + fi + echo "Pip version: $pip_version" + echo "Pip path: $pip_path" + + export PIP3BIN=$pip_path + echo "PIP3BIN is set to $PIP3BIN" +else + echo "Pip is not installed." +fi + diff --git a/deps/detectScripts.sh b/deps/detectScripts.sh new file mode 100755 index 00000000..1ee44ae3 --- /dev/null +++ b/deps/detectScripts.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +echo "Checking for additional Sparta scripts..." +curPath=`pwd` + +scripts=("smbenum.sh" "snmpbrute.py" "ms08-067_check.py" "rdp-sec-check.pl", "ndr.py", "installDeps.sh", "snmpcheck.rb", "smtp-user-enum.pl") + +for script in "${scripts[@]}"; do + if [ -a "scripts/$script" ]; then + echo "$script is already installed" + else + wget -v -P scripts/ "https://raw.githubusercontent.com/Hackman238/sparta-scripts/master/$script" + fi +done + +declare -A externalRepos +externalRepos["CloudFail"]="https://github.com/m0rtem/CloudFail.git" + +for externalRepo in "${!externalRepos[@]}"; do + if [ -d "scripts/$externalRepos" ]; then + echo "$externalRepo is already installed" + else + git clone "${externalRepos[$externalRepo]}" scripts/$externalRepo + fi +done + +if [ ! -f ".initialized" ] + then + chmod a+x scripts/installDeps.sh + ./scripts/installDeps.sh +fi + +cd ${curPath} diff --git a/deps/fixQt.sh b/deps/fixQt.sh new file mode 100755 index 00000000..8c1eaad8 --- /dev/null +++ b/deps/fixQt.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +strip --remove-section=.note.ABI-tag /usr/local/lib/python3.8/dist-packages/PyQt6/Qt6/lib/libQt6Core.so.6 +strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 diff --git a/deps/installDeps.sh b/deps/installDeps.sh new file mode 100755 index 00000000..11c7ecb7 --- /dev/null +++ b/deps/installDeps.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +source ./deps/apt.sh + +# Install deps + echo "Checking Apt..." + +# runAptGetUpdate +apt-get update -m + +echo "Installing deps..." +export DEBIAN_FRONTEND="noninteractive" +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install sra-toolkit sslscan +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install nmap finger hydra nikto nbtscan nfs-common rpcbind smbclient ldap-utils rwho x11-apps cutycapt featherpad xvfb imagemagick eog hping3 sqlmap libqt5core5a python3-pip ruby perl urlscan git xsltproc hping3 +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install libgl1-mesa-glx libegl-mesa0 libegl1 libxcb-cursor0 libxcb-icccm4 python3-xvfbwrapper python3-selenium phantomjs libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxkbcommon-x11-0 +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install dnsmap +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install wapiti +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install python3-impacket +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install whatweb +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install medusa +apt-get -yqqqm --allow-unauthenticated -o DPkg::Options::="--force-overwrite" -o DPkg::Options::="--force-confdef" install eyewitness diff --git a/deps/installPythonLibs.sh b/deps/installPythonLibs.sh new file mode 100755 index 00000000..dad2654e --- /dev/null +++ b/deps/installPythonLibs.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +source ./deps/detectPython.sh + +# Setup Python deps +${PIP3BIN} install -r requirements.txt --upgrade +${PIP3BIN} install service-identity --upgrade + +${PYTHON3BIN} ./deps/primeExploitDb.py diff --git a/deps/nmap-wsl.sh b/deps/nmap-wsl.sh new file mode 100755 index 00000000..56ac2f3e --- /dev/null +++ b/deps/nmap-wsl.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ ! -f "/mnt/c/Program Files (x86)/Nmap/nmap.exe" ] +then + echo "Install Windows NMAP for NMAP support in WSL. Linux NMAP will not work." + exit 1 +fi + +"/mnt/c/Program Files (x86)/Nmap/nmap.exe" "$@" diff --git a/deps/primeExploitDb.py b/deps/primeExploitDb.py new file mode 100755 index 00000000..0e61a184 --- /dev/null +++ b/deps/primeExploitDb.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +from pyExploitDb import PyExploitDb + + +def prime(): + pEdb = PyExploitDb() + pEdb.debug = False + pEdb.openFile() + + +if __name__ == "__main__": + prime() diff --git a/deps/setupWsl.sh b/deps/setupWsl.sh new file mode 100755 index 00000000..1d35be0d --- /dev/null +++ b/deps/setupWsl.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Setup linked Windows NMAP +if [ -f "/usr/bin/nmap" ] +then + nmapBinCheck=$(cat /usr/bin/nmap | grep -c "nmap.exe") +else + nmapBinCheck=1 +fi + +if [ ! -f "/sbin/nmap" ] | [ ${nmapBinCheck} -eq 0 ] +then + echo "Installing Link to Windows NMAP..." + today=$(date +%s) + mv /usr/bin/nmap /usr/bin/nmap_lin_${today} + cp ./deps/nmap-wsl.sh /sbin/nmap + chmod a+x /sbin/nmap + if [ ! -f "/sbin/nmap" ] + then + ln -s /sbin/nmap /usr/bin/nmap + fi +else + echo "Link to Windows NMAP already exists; skipping." +fi diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..d7c60db3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN git clone https://github.com/Hackman238/legion.git +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 00000000..f461032a --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN git clone https://github.com/Hackman238/legion.git -b development +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local new file mode 100644 index 00000000..f8cc487b --- /dev/null +++ b/docker/Dockerfile.local @@ -0,0 +1,18 @@ +FROM gvit/python-secbase:latest +WORKDIR / +ENV DISPLAY :0 +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=America/Chicago +RUN mkdir -p /legion +COPY . /legion +RUN cd legion && \ + chmod +x ./startLegion.sh && \ + chmod +x ./deps/* -R && \ + chmod +x ./scripts/* -R && \ + mkdir -p /legion/tmp +RUN cd legion && \ + pip3 install -r requirements.txt --upgrade +RUN cd legion && \ + bash ./deps/detectScripts.sh +WORKDIR /legion +CMD ["python3", "legion.py"] diff --git a/docker/buildIt.sh b/docker/buildIt.sh new file mode 100644 index 00000000..d3d4458f --- /dev/null +++ b/docker/buildIt.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +testBranch=`git branch | grep "development" | grep "*"` + +if [[ -z $testBranch ]] +then + echo "Master Branch" + docker build -t legion . --no-cache +else + echo "Development Branch" + docker build -f Dockerfile.dev -t legion -t development . --no-cache +fi diff --git a/docker/buildItLocal.sh b/docker/buildItLocal.sh new file mode 100644 index 00000000..8656b980 --- /dev/null +++ b/docker/buildItLocal.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Building local branch" +docker build -f ./docker/Dockerfile.local -t legion:local . --no-cache diff --git a/docker/runIt.sh b/docker/runIt.sh new file mode 100755 index 00000000..56d7bf37 --- /dev/null +++ b/docker/runIt.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +docker pull gvit/legion + +if [[ ! -z $1 ]] +then + export DISPLAY=$1:0.0 + XSOCK=/tmp/.X11-unix + XAUTH=/tmp/.docker.xauth + rm /tmp/.docker.xauth* -f + touch $XAUTH + xauth add $DISPLAY - `mcookie` + xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - + docker run -ti -v $XSOCK -v $XAUTH -e XAUTHORITY=$XAUTH -e DISPLAY=$DISPLAY gvit/legion +else + docker run -ti -e DISPLAY=$DISPLAY --net=host --security-opt=apparmor:unconfined --security-opt=label:disable gvit/legion +fi diff --git a/images/LegionBanner.png b/images/LegionBanner.png new file mode 100644 index 00000000..06579bff Binary files /dev/null and b/images/LegionBanner.png differ diff --git a/images/add.png b/images/add.png new file mode 100644 index 00000000..328e5d15 Binary files /dev/null and b/images/add.png differ diff --git a/images/advanced.png b/images/advanced.png new file mode 100644 index 00000000..6e775dbf Binary files /dev/null and b/images/advanced.png differ diff --git a/images/browser-big.jpg b/images/browser-big.jpg new file mode 100644 index 00000000..3d8ca8c1 Binary files /dev/null and b/images/browser-big.jpg differ diff --git a/images/cancel-delete.png b/images/cancel-delete.png new file mode 100644 index 00000000..8dadbed3 Binary files /dev/null and b/images/cancel-delete.png differ diff --git a/images/cisco-big.jpg b/images/cisco-big.jpg new file mode 100644 index 00000000..5479e65f Binary files /dev/null and b/images/cisco-big.jpg differ diff --git a/images/cisco-icon.png b/images/cisco-icon.png new file mode 100644 index 00000000..806c46ac Binary files /dev/null and b/images/cisco-icon.png differ diff --git a/images/close.png b/images/close.png new file mode 100644 index 00000000..bc0f5761 Binary files /dev/null and b/images/close.png differ diff --git a/images/closed.gif b/images/closed.gif new file mode 100644 index 00000000..e7af476c Binary files /dev/null and b/images/closed.gif differ diff --git a/images/closetab-hover.png b/images/closetab-hover.png new file mode 100644 index 00000000..affb7201 Binary files /dev/null and b/images/closetab-hover.png differ diff --git a/images/closetab-press.png b/images/closetab-press.png new file mode 100644 index 00000000..2d897211 Binary files /dev/null and b/images/closetab-press.png differ diff --git a/images/closetab-small.png b/images/closetab-small.png new file mode 100644 index 00000000..36dba35c Binary files /dev/null and b/images/closetab-small.png differ diff --git a/images/closetab.png b/images/closetab.png new file mode 100644 index 00000000..1d658996 Binary files /dev/null and b/images/closetab.png differ diff --git a/images/filtered.gif b/images/filtered.gif new file mode 100644 index 00000000..aee5caeb Binary files /dev/null and b/images/filtered.gif differ diff --git a/images/finished.gif b/images/finished.gif new file mode 100644 index 00000000..8757111f Binary files /dev/null and b/images/finished.gif differ diff --git a/images/finished.png b/images/finished.png new file mode 100644 index 00000000..d9b0c45f Binary files /dev/null and b/images/finished.png differ diff --git a/images/hp-big.jpg b/images/hp-big.jpg new file mode 100644 index 00000000..a3ee7f78 Binary files /dev/null and b/images/hp-big.jpg differ diff --git a/images/hp-icon.png b/images/hp-icon.png new file mode 100644 index 00000000..86fb6d44 Binary files /dev/null and b/images/hp-icon.png differ diff --git a/images/icons/Backup_of_legion.cdr b/images/icons/Backup_of_legion.cdr new file mode 100644 index 00000000..1952f8e8 Binary files /dev/null and b/images/icons/Backup_of_legion.cdr differ diff --git a/images/icons/Backup_of_legion_adj.cdr b/images/icons/Backup_of_legion_adj.cdr new file mode 100644 index 00000000..140e995f Binary files /dev/null and b/images/icons/Backup_of_legion_adj.cdr differ diff --git a/images/icons/Legion-N_128x128.svg b/images/icons/Legion-N_128x128.svg new file mode 100644 index 00000000..6b900e0d --- /dev/null +++ b/images/icons/Legion-N_128x128.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/knife.png b/images/icons/knife.png new file mode 100644 index 00000000..0d6e7e1c Binary files /dev/null and b/images/icons/knife.png differ diff --git a/images/icons/knife_tiny.png b/images/icons/knife_tiny.png new file mode 100644 index 00000000..02f6dc3b Binary files /dev/null and b/images/icons/knife_tiny.png differ diff --git a/images/icons/legion.cdr b/images/icons/legion.cdr new file mode 100644 index 00000000..1d2882d1 Binary files /dev/null and b/images/icons/legion.cdr differ diff --git a/images/icons/legion.ico b/images/icons/legion.ico new file mode 100644 index 00000000..4217cecd Binary files /dev/null and b/images/icons/legion.ico differ diff --git a/images/icons/legion_adj.cdr b/images/icons/legion_adj.cdr new file mode 100644 index 00000000..48ff6de4 Binary files /dev/null and b/images/icons/legion_adj.cdr differ diff --git a/images/icons/legion_adj.svg b/images/icons/legion_adj.svg new file mode 100644 index 00000000..56e9bc7f --- /dev/null +++ b/images/icons/legion_adj.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/images/icons/legion_medium.svg b/images/icons/legion_medium.svg new file mode 100644 index 00000000..90f58fa9 --- /dev/null +++ b/images/icons/legion_medium.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + L + + + + + + + + + diff --git a/images/icons/legion_tiny.cdr b/images/icons/legion_tiny.cdr new file mode 100644 index 00000000..c910cac2 Binary files /dev/null and b/images/icons/legion_tiny.cdr differ diff --git a/images/icons/legion_tiny.svg b/images/icons/legion_tiny.svg new file mode 100644 index 00000000..3f718f24 --- /dev/null +++ b/images/icons/legion_tiny.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + diff --git a/images/icons/logo.png b/images/icons/logo.png new file mode 100644 index 00000000..9988263c Binary files /dev/null and b/images/icons/logo.png differ diff --git a/images/icons/logo_big.png b/images/icons/logo_big.png new file mode 100644 index 00000000..c0444c49 Binary files /dev/null and b/images/icons/logo_big.png differ diff --git a/images/icons/logo_small.png b/images/icons/logo_small.png new file mode 100644 index 00000000..0a76f23e Binary files /dev/null and b/images/icons/logo_small.png differ diff --git a/images/icons/zombie-horde-clipart-1.bmp b/images/icons/zombie-horde-clipart-1.bmp new file mode 100644 index 00000000..ac1568eb Binary files /dev/null and b/images/icons/zombie-horde-clipart-1.bmp differ diff --git a/images/killed.gif b/images/killed.gif new file mode 100644 index 00000000..b0c367b1 Binary files /dev/null and b/images/killed.gif differ diff --git a/images/killed.png b/images/killed.png new file mode 100644 index 00000000..0cc9920e Binary files /dev/null and b/images/killed.png differ diff --git a/images/legionConsole.png b/images/legionConsole.png new file mode 100755 index 00000000..b97975b6 Binary files /dev/null and b/images/legionConsole.png differ diff --git a/images/linux-icon.png b/images/linux-icon.png new file mode 100644 index 00000000..ff634188 Binary files /dev/null and b/images/linux-icon.png differ diff --git a/images/minus-black.png b/images/minus-black.png new file mode 100644 index 00000000..5ffc6ee1 Binary files /dev/null and b/images/minus-black.png differ diff --git a/images/open.gif b/images/open.gif new file mode 100644 index 00000000..3b758fbc Binary files /dev/null and b/images/open.gif differ diff --git a/images/question-icon.png b/images/question-icon.png new file mode 100644 index 00000000..75ebfc92 Binary files /dev/null and b/images/question-icon.png differ diff --git a/images/running.gif b/images/running.gif new file mode 100644 index 00000000..8fc37154 Binary files /dev/null and b/images/running.gif differ diff --git a/images/save.png b/images/save.png new file mode 100644 index 00000000..39f08014 Binary files /dev/null and b/images/save.png differ diff --git a/images/screenshot-big.jpg b/images/screenshot-big.jpg new file mode 100644 index 00000000..35d8f700 Binary files /dev/null and b/images/screenshot-big.jpg differ diff --git a/images/search.png b/images/search.png new file mode 100644 index 00000000..2be35d42 Binary files /dev/null and b/images/search.png differ diff --git a/images/solaris-icon-big.png b/images/solaris-icon-big.png new file mode 100644 index 00000000..8819e122 Binary files /dev/null and b/images/solaris-icon-big.png differ diff --git a/images/solaris-icon.png b/images/solaris-icon.png new file mode 100644 index 00000000..3a6237e4 Binary files /dev/null and b/images/solaris-icon.png differ diff --git a/images/vmware-big.jpg b/images/vmware-big.jpg new file mode 100644 index 00000000..08eba2f0 Binary files /dev/null and b/images/vmware-big.jpg differ diff --git a/images/vxworks-icon-big.png b/images/vxworks-icon-big.png new file mode 100644 index 00000000..574f88cb Binary files /dev/null and b/images/vxworks-icon-big.png differ diff --git a/images/vxworks-icon.png b/images/vxworks-icon.png new file mode 100644 index 00000000..140984cd Binary files /dev/null and b/images/vxworks-icon.png differ diff --git a/images/waiting.gif b/images/waiting.gif new file mode 100644 index 00000000..d91b8e35 Binary files /dev/null and b/images/waiting.gif differ diff --git a/images/windows-icon.png b/images/windows-icon.png new file mode 100644 index 00000000..f8124bfe Binary files /dev/null and b/images/windows-icon.png differ diff --git a/legion.conf b/legion.conf new file mode 100644 index 00000000..7c472580 --- /dev/null +++ b/legion.conf @@ -0,0 +1,332 @@ +[BruteSettings] +default-password=password +default-username=root +no-password-services="oracle-sid,rsh,smtp-enum" +no-username-services="cisco,cisco-enable,oracle-listener,s7-300,snmp,vnc" +password-wordlist-path=/usr/share/wordlists/ +services="asterisk,afp,cisco,cisco-enable,cvs,firebird,ftp,ftps,http-head,http-get,https-head,https-get,http-get-form,https-get-form,http-post-form,https-post-form,http-proxy,http-proxy-urlenum,icq,imap,imaps,irc,ldap2,ldap2s,ldap3,ldap3s,ldap3-crammd5,ldap3-crammd5s,ldap3-digestmd5,ldap3-digestmd5s,mssql,mysql,ncp,nntp,oracle-listener,oracle-sid,pcanywhere,pcnfs,pop3,pop3s,postgres,rdp,rexec,rlogin,rsh,s7-300,sip,smb,smtp,smtps,smtp-enum,snmp,socks5,ssh,sshkey,svn,teamspeak,telnet,telnets,vmauthd,vnc,xmpp" +store-cleartext-passwords-on-exit=True +username-wordlist-path=/usr/share/wordlists/ + +[GUISettings] +process-tab-column-widths="125,0,100,150,100,0,196,100,0,0,0,0,0,0,0,125,100" +process-tab-detail=true + +[GeneralSettings] +default-terminal=xterm +enable-scheduler=True +enable-scheduler-on-import=False +max-fast-processes=5 +max-slow-processes=5 +screenshooter-timeout=15000 +tool-output-black-background=False +web-services="http,https,ssl,soap,http-proxy,http-alt,https-alt" + +[HostActions] +icmp-timestamp=ICMP timestamp, hping3 -V -C 13 -c 1 [IP] +nmap-discover=Run nmap-discover, nmap -n -sV -O --version-light -T4 [IP] +nmap-fast-tcp=Run nmap (fast TCP), nmap -Pn -sV -sC -F -T4 -vvvv [IP] +nmap-fast-udp=Run nmap (fast UDP), nmap -n -Pn -sU -F --min-rate=1000 -vvvvv [IP] +nmap-full-tcp=Run nmap (full TCP), nmap -Pn -sV -sC -O -p- -T4 -vvvvv [IP] +nmap-full-udp=Run nmap (full UDP), nmap -n -Pn -sU -p- -T4 -vvvvv [IP] +nmap-script-Vulners=Run nmap script - Vulners, nmap -sV --script=vulners.nse -vvvv [IP] +nmap-udp-1000=Run nmap (top 1000 quick UDP), nmap -n -Pn -sU --min-rate=1000 -vvvvv [IP] +python-script-PyShodan=Run PyShodan python script, /bin/echo PythonScript pyShodan +python-script-macvendors=Run macvendors python script, /bin/echo PythonScript macvendors +unicornscan-full-udp=Run unicornscan (full UDP), unicornscan -mU -Ir 1000 [IP]:a -v + +[PortActions] +banner=Grab banner, bash -c \"echo \"\" | nc -v -n -w1 [IP] [PORT]\", +broadcast-ms-sql-discover.nse=broadcast-ms-sql-discover.nse, "nmap -Pn [IP] -p [PORT] --script=broadcast-ms-sql-discover.nse --script-args=unsafe=1", ms-sql +citrix-brute-xml.nse=citrix-brute-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-brute-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps-xml.nse=citrix-enum-apps-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps-xml.nse --script-args=unsafe=1", citrix +citrix-enum-apps.nse=citrix-enum-apps.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-apps.nse --script-args=unsafe=1", citrix +citrix-enum-servers-xml.nse=citrix-enum-servers-xml.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers-xml.nse --script-args=unsafe=1", citrix +citrix-enum-servers.nse=citrix-enum-servers.nse, "nmap -Pn [IP] -p [PORT] --script=citrix-enum-servers.nse --script-args=unsafe=1", citrix +cloudfail=Run cloudfail, python3.7 scripts/CloudFail/cloudfail.py --target [IP] --tor, cloudfail +dirbuster=Launch dirbuster, java -Xmx256M -jar /usr/share/dirbuster/DirBuster-1.0-RC1.jar -u http://[IP]:[PORT]/, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +dnsmap=Run dnsmap, dnsmap [IP] -w ./wordlists/gvit_subdomain_wordlist.txt -r [OUTPUT], dnsmap +enum4linux=Run enum4linux, enum4linux [IP], "netbios-ssn,microsoft-ds" +finger=Enumerate users (finger), ./scripts/fingertool.sh [IP], finger +ftp-anon.nse=ftp-anon.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-anon.nse --script-args=unsafe=1", ftp +ftp-bounce.nse=ftp-bounce.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-bounce.nse --script-args=unsafe=1", ftp +ftp-brute.nse=ftp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-brute.nse --script-args=unsafe=1", ftp +ftp-default=Check for default ftp credentials, hydra -s [PORT] -C ./wordlists/ftp-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] ftp, ftp +ftp-libopie.nse=ftp-libopie.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-libopie.nse --script-args=unsafe=1", ftp +ftp-proftpd-backdoor.nse=ftp-proftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-proftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vsftpd-backdoor.nse=ftp-vsftpd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vsftpd-backdoor.nse --script-args=unsafe=1", ftp +ftp-vuln-cve2010-4221.nse=ftp-vuln-cve2010-4221.nse, "nmap -Pn [IP] -p [PORT] --script=ftp-vuln-cve2010-4221.nse --script-args=unsafe=1", ftp +http-adobe-coldfusion-apsa1301.nse=http-adobe-coldfusion-apsa1301.nse, "nmap -Pn [IP] -p [PORT] --script=http-adobe-coldfusion-apsa1301.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-affiliate-id.nse=http-affiliate-id.nse, "nmap -Pn [IP] -p [PORT] --script=http-affiliate-id.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-apache-negotiation.nse=http-apache-negotiation.nse, "nmap -Pn [IP] -p [PORT] --script=http-apache-negotiation.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth-finder.nse=http-auth-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-auth.nse=http-auth.nse, "nmap -Pn [IP] -p [PORT] --script=http-auth.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-awstatstotals-exec.nse=http-awstatstotals-exec.nse, "nmap -Pn [IP] -p [PORT] --script=http-awstatstotals-exec.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-axis2-dir-traversal.nse=http-axis2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-axis2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-backup-finder.nse=http-backup-finder.nse, "nmap -Pn [IP] -p [PORT] --script=http-backup-finder.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-barracuda-dir-traversal.nse=http-barracuda-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-barracuda-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-brute.nse=http-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cakephp-version.nse=http-cakephp-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-cakephp-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-chrono.nse=http-chrono.nse, "nmap -Pn [IP] -p [PORT] --script=http-chrono.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-coldfusion-subzero.nse=http-coldfusion-subzero.nse, "nmap -Pn [IP] -p [PORT] --script=http-coldfusion-subzero.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-comments-displayer.nse=http-comments-displayer.nse, "nmap -Pn [IP] -p [PORT] --script=http-comments-displayer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-config-backup.nse=http-config-backup.nse, "nmap -Pn [IP] -p [PORT] --script=http-config-backup.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-cors.nse=http-cors.nse, "nmap -Pn [IP] -p [PORT] --script=http-cors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-csrf.nse=http-csrf.nse, "nmap -Pn [IP] -p [PORT] --script=http-csrf.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-date.nse=http-date.nse, "nmap -Pn [IP] -p [PORT] --script=http-date.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-default-accounts.nse=http-default-accounts.nse, "nmap -Pn [IP] -p [PORT] --script=http-default-accounts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-devframework.nse=http-devframework.nse, "nmap -Pn [IP] -p [PORT] --script=http-devframework.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dlink-backdoor.nse=http-dlink-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=http-dlink-backdoor.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-dombased-xss.nse=http-dombased-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-dombased-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-domino-enum-passwords.nse=http-domino-enum-passwords.nse, "nmap -Pn [IP] -p [PORT] --script=http-domino-enum-passwords.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-enum-users.nse=http-drupal-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-enum-users.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-drupal-modules.nse=http-drupal-modules.nse, "nmap -Pn [IP] -p [PORT] --script=http-drupal-modules.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-email-harvest.nse=http-email-harvest.nse, "nmap -Pn [IP] -p [PORT] --script=http-email-harvest.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-enum.nse=http-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-errors.nse=http-errors.nse, "nmap -Pn [IP] -p [PORT] --script=http-errors.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-exif-spider.nse=http-exif-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-exif-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-favicon.nse=http-favicon.nse, "nmap -Pn [IP] -p [PORT] --script=http-favicon.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-feed.nse=http-feed.nse, "nmap -Pn [IP] -p [PORT] --script=http-feed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-fileupload-exploiter.nse=http-fileupload-exploiter.nse, "nmap -Pn [IP] -p [PORT] --script=http-fileupload-exploiter.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-brute.nse=http-form-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-form-fuzzer.nse=http-form-fuzzer.nse, "nmap -Pn [IP] -p [PORT] --script=http-form-fuzzer.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-frontpage-login.nse=http-frontpage-login.nse, "nmap -Pn [IP] -p [PORT] --script=http-frontpage-login.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-generator.nse=http-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-git.nse=http-git.nse, "nmap -Pn [IP] -p [PORT] --script=http-git.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-gitweb-projects-enum.nse=http-gitweb-projects-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-gitweb-projects-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-google-malware.nse=http-google-malware.nse, "nmap -Pn [IP] -p [PORT] --script=http-google-malware.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-grep.nse=http-grep.nse, "nmap -Pn [IP] -p [PORT] --script=http-grep.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-headers.nse=http-headers.nse, "nmap -Pn [IP] -p [PORT] --script=http-headers.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-huawei-hg5xx-vuln.nse=http-huawei-hg5xx-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-huawei-hg5xx-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-findmyiphone.nse=http-icloud-findmyiphone.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-findmyiphone.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-icloud-sendmsg.nse=http-icloud-sendmsg.nse, "nmap -Pn [IP] -p [PORT] --script=http-icloud-sendmsg.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-short-name-brute.nse=http-iis-short-name-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-short-name-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-iis-webdav-vuln.nse=http-iis-webdav-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-iis-webdav-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-joomla-brute.nse=http-joomla-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-joomla-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-litespeed-sourcecode-download.nse=http-litespeed-sourcecode-download.nse, "nmap -Pn [IP] -p [PORT] --script=http-litespeed-sourcecode-download.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-majordomo2-dir-traversal.nse=http-majordomo2-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-majordomo2-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-malware-host.nse=http-malware-host.nse, "nmap -Pn [IP] -p [PORT] --script=http-malware-host.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-method-tamper.nse=http-method-tamper.nse, "nmap -Pn [IP] -p [PORT] --script=http-method-tamper.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-methods.nse=http-methods.nse, "nmap -Pn [IP] -p [PORT] --script=http-methods.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-mobileversion-checker.nse=http-mobileversion-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-mobileversion-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-ntlm-info.nse=http-ntlm-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-ntlm-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-proxy.nse=http-open-proxy.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-proxy.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-open-redirect.nse=http-open-redirect.nse, "nmap -Pn [IP] -p [PORT] --script=http-open-redirect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-passwd.nse=http-passwd.nse, "nmap -Pn [IP] -p [PORT] --script=http-passwd.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-php-version.nse=http-php-version.nse, "nmap -Pn [IP] -p [PORT] --script=http-php-version.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpmyadmin-dir-traversal.nse=http-phpmyadmin-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpmyadmin-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-phpself-xss.nse=http-phpself-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-phpself-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-proxy-brute.nse=http-proxy-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-proxy-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-put.nse=http-put.nse, "nmap -Pn [IP] -p [PORT] --script=http-put.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-qnap-nas-info.nse=http-qnap-nas-info.nse, "nmap -Pn [IP] -p [PORT] --script=http-qnap-nas-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-referer-checker.nse=http-referer-checker.nse, "nmap -Pn [IP] -p [PORT] --script=http-referer-checker.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-rfi-spider.nse=http-rfi-spider.nse, "nmap -Pn [IP] -p [PORT] --script=http-rfi-spider.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robots.txt.nse=http-robots.txt.nse, "nmap -Pn [IP] -p [PORT] --script=http-robots.txt.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-reverse-ip.nse=http-robtex-reverse-ip.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-reverse-ip.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-robtex-shared-ns.nse=http-robtex-shared-ns.nse, "nmap -Pn [IP] -p [PORT] --script=http-robtex-shared-ns.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-server-header.nse=http-server-header.nse, "nmap -Pn [IP] -p [PORT] --script=http-server-header.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sitemap-generator.nse=http-sitemap-generator.nse, "nmap -Pn [IP] -p [PORT] --script=http-sitemap-generator.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris-check.nse=http-slowloris-check.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris-check.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-slowloris.nse=http-slowloris.nse, "nmap -Pn [IP] -p [PORT] --script=http-slowloris.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sql-injection.nse=http-sql-injection.nse, "nmap -Pn [IP] -p [PORT] --script=http-sql-injection.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-sqlmap=mysql-sqlmap, "sqlmap -v 2 --url=http://[IP] --user-agent=SQLMAP --delay=1 --timeout=15 --retries=2 --keep-alive --threads=5 --eta --batch --level=5 --risk=3 --banner --is-dba --dbs --tables --technique=BEUST -s [OUTPUT] --flush-session -t [OUTPUT] --fresh-queries > [OUTPUT]", mysql +http-stored-xss.nse=http-stored-xss.nse, "nmap -Pn [IP] -p [PORT] --script=http-stored-xss.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-title.nse=http-title.nse, "nmap -Pn [IP] -p [PORT] --script=http-title.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-tplink-dir-traversal.nse=http-tplink-dir-traversal.nse, "nmap -Pn [IP] -p [PORT] --script=http-tplink-dir-traversal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-trace.nse=http-trace.nse, "nmap -Pn [IP] -p [PORT] --script=http-trace.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-traceroute.nse=http-traceroute.nse, "nmap -Pn [IP] -p [PORT] --script=http-traceroute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-unsafe-output-escaping.nse=http-unsafe-output-escaping.nse, "nmap -Pn [IP] -p [PORT] --script=http-unsafe-output-escaping.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-useragent-tester.nse=http-useragent-tester.nse, "nmap -Pn [IP] -p [PORT] --script=http-useragent-tester.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-userdir-enum.nse=http-userdir-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-userdir-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vhosts.nse=http-vhosts.nse, "nmap -Pn [IP] -p [PORT] --script=http-vhosts.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-virustotal.nse=http-virustotal.nse, "nmap -Pn [IP] -p [PORT] --script=http-virustotal.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vlcstreamer-ls.nse=http-vlcstreamer-ls.nse, "nmap -Pn [IP] -p [PORT] --script=http-vlcstreamer-ls.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vmware-path-vuln.nse=http-vmware-path-vuln.nse, "nmap -Pn [IP] -p [PORT] --script=http-vmware-path-vuln.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2009-3960.nse=http-vuln-cve2009-3960.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2009-3960.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-0738.nse=http-vuln-cve2010-0738.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-0738.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2010-2861.nse=http-vuln-cve2010-2861.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2010-2861.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3192.nse=http-vuln-cve2011-3192.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3192.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2011-3368.nse=http-vuln-cve2011-3368.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2011-3368.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2012-1823.nse=http-vuln-cve2012-1823.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2012-1823.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-cve2013-0156.nse=http-vuln-cve2013-0156.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-cve2013-0156.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-vuln-zimbra-lfi.nse=http-vuln-zimbra-lfi.nse, "nmap -Pn [IP] -p [PORT] --script=http-vuln-zimbra-lfi.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-detect.nse=http-waf-detect.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-detect.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-waf-fingerprint.nse=http-waf-fingerprint.nse, "nmap -Pn [IP] -p [PORT] --script=http-waf-fingerprint.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wapiti=http-wapiti, wapiti http://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], http +http-wordpress-brute.nse=http-wordpress-brute.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-brute.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-enum.nse=http-wordpress-enum.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-enum.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-wordpress-plugins.nse=http-wordpress-plugins.nse, "nmap -Pn [IP] -p [PORT] --script=http-wordpress-plugins.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +http-xssed.nse=http-xssed.nse, "nmap -Pn [IP] -p [PORT] --script=http-xssed.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +https-wapiti=https-wapiti, wapiti https://[IP] -n 10 -b folder -u -v 1 -f txt -o [OUTPUT], https +imap-brute.nse=imap-brute.nse, "nmap -Pn [IP] -p [PORT] --script=imap-brute.nse --script-args=unsafe=1", imap +imap-capabilities.nse=imap-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=imap-capabilities.nse --script-args=unsafe=1", imap +irc-botnet-channels.nse=irc-botnet-channels.nse, "nmap -Pn [IP] -p [PORT] --script=irc-botnet-channels.nse --script-args=unsafe=1", irc +irc-brute.nse=irc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-brute.nse --script-args=unsafe=1", irc +irc-info.nse=irc-info.nse, "nmap -Pn [IP] -p [PORT] --script=irc-info.nse --script-args=unsafe=1", irc +irc-sasl-brute.nse=irc-sasl-brute.nse, "nmap -Pn [IP] -p [PORT] --script=irc-sasl-brute.nse --script-args=unsafe=1", irc +irc-unrealircd-backdoor.nse=irc-unrealircd-backdoor.nse, "nmap -Pn [IP] -p [PORT] --script=irc-unrealircd-backdoor.nse --script-args=unsafe=1", irc +ldapsearch=Run ldapsearch, ldapsearch -h [IP] -p [PORT] -x -s base, ldap +membase-http-info.nse=membase-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=membase-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +ms-sql-brute.nse=ms-sql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-brute.nse --script-args=unsafe=1", ms-sql +ms-sql-config.nse=ms-sql-config.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-config.nse --script-args=unsafe=1", ms-sql +ms-sql-dac.nse=ms-sql-dac.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dac.nse --script-args=unsafe=1", ms-sql +ms-sql-dump-hashes.nse=ms-sql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-dump-hashes.nse --script-args=unsafe=1", ms-sql +ms-sql-empty-password.nse=ms-sql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-empty-password.nse --script-args=unsafe=1", ms-sql +ms-sql-hasdbaccess.nse=ms-sql-hasdbaccess.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-hasdbaccess.nse --script-args=unsafe=1", ms-sql +ms-sql-info.nse=ms-sql-info.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-info.nse --script-args=unsafe=1", ms-sql +ms-sql-query.nse=ms-sql-query.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-query.nse --script-args=unsafe=1", ms-sql +ms-sql-tables.nse=ms-sql-tables.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-tables.nse --script-args=unsafe=1", ms-sql +ms-sql-xp-cmdshell.nse=ms-sql-xp-cmdshell.nse, "nmap -Pn [IP] -p [PORT] --script=ms-sql-xp-cmdshell.nse --script-args=unsafe=1", ms-sql +msrpc-enum.nse=msrpc-enum.nse, "nmap -Pn [IP] -p [PORT] --script=msrpc-enum.nse --script-args=unsafe=1", msrpc +mssql-default=Check for default mssql credentials, hydra -s [PORT] -C ./wordlists/mssql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mssql, ms-sql-s +mysql-audit.nse=mysql-audit.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-audit.nse --script-args=unsafe=1", mysql +mysql-brute.nse=mysql-brute.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-brute.nse --script-args=unsafe=1", mysql +mysql-databases.nse=mysql-databases.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-databases.nse --script-args=unsafe=1", mysql +mysql-default=Check for default mysql credentials, hydra -s [PORT] -C ./wordlists/mysql-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] mysql, mysql +mysql-dump-hashes.nse=mysql-dump-hashes.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-dump-hashes.nse --script-args=unsafe=1", mysql +mysql-empty-password.nse=mysql-empty-password.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-empty-password.nse --script-args=unsafe=1", mysql +mysql-enum.nse=mysql-enum.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-enum.nse --script-args=unsafe=1", mysql +mysql-info.nse=mysql-info.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-info.nse --script-args=unsafe=1", mysql +mysql-query.nse=mysql-query.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-query.nse --script-args=unsafe=1", mysql +mysql-users.nse=mysql-users.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-users.nse --script-args=unsafe=1", mysql +mysql-variables.nse=mysql-variables.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-variables.nse --script-args=unsafe=1", mysql +mysql-vuln-cve2012-2122.nse=mysql-vuln-cve2012-2122.nse, "nmap -Pn [IP] -p [PORT] --script=mysql-vuln-cve2012-2122.nse --script-args=unsafe=1", mysql +nbtscan=Run nbtscan, nbtscan -v -h [IP], netbios-ns +nfs-ls.nse=nfs-ls.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-ls.nse --script-args=unsafe=1", nfs +nfs-showmount.nse=nfs-showmount.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-showmount.nse --script-args=unsafe=1", nfs +nfs-statfs.nse=nfs-statfs.nse, "nmap -Pn [IP] -p [PORT] --script=nfs-statfs.nse --script-args=unsafe=1", nfs +nikto=Run nikto, nikto -o [OUTPUT].txt -p [PORT] -h [IP] -C all, "http,https,ssl,soap,http-proxy,http-alt,https-alt" +nmap=Run nmap (scripts) on port, nmap -Pn -sV -sC -vvvvv -p[PORT] [IP], +oracle-brute-stealth.nse=oracle-brute-stealth.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute-stealth.nse --script-args=unsafe=1", oracle +oracle-brute.nse=oracle-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-brute.nse --script-args=unsafe=1", oracle +oracle-default=Check for default oracle credentials, hydra -s [PORT] -C ./wordlists/oracle-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] oracle-listener, oracle-tns +oracle-enum-users.nse=oracle-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-enum-users.nse --script-args=unsafe=1", oracle +oracle-sid=Oracle SID enumeration, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/sid_enum; set RHOSTS [IP]; run; exit -y\"", oracle-tns +oracle-sid-brute.nse=oracle-sid-brute.nse, "nmap -Pn [IP] -p [PORT] --script=oracle-sid-brute.nse --script-args=unsafe=1", oracle +oracle-version=Get Oracle version, "msfconsole -q -n -L -x \"color false; spool [OUTPUT].txt; use auxiliary/scanner/oracle/tnslsnr_version; set RHOSTS [IP]; run; exit -y\"", oracle-tns +polenum=Extract password policy (polenum), polenum [IP], "netbios-ssn,microsoft-ds" +pop3-brute.nse=pop3-brute.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-brute.nse --script-args=unsafe=1", pop3 +pop3-capabilities.nse=pop3-capabilities.nse, "nmap -Pn [IP] -p [PORT] --script=pop3-capabilities.nse --script-args=unsafe=1", pop3 +postgres-default=Check for default postgres credentials, hydra -s [PORT] -C ./wordlists/postgres-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] postgres, postgresql +rdp-sec-check=Run rdp-sec-check.pl, perl ./scripts/rdp-sec-check.pl [IP]:[PORT], ms-wbt-server +realvnc-auth-bypass.nse=realvnc-auth-bypass.nse, "nmap -Pn [IP] -p [PORT] --script=realvnc-auth-bypass.nse --script-args=unsafe=1", vnc +riak-http-info.nse=riak-http-info.nse, "nmap -Pn [IP] -p [PORT] --script=riak-http-info.nse --script-args=unsafe=1", "http,https,ssl,soap,http-proxy,http-alt,https-alt" +rpcinfo=Run rpcinfo, rpcinfo -p [IP], rpcbind +rwho=Run rwho, rwho -a [IP], who +samba-vuln-cve-2012-1182.nse=samba-vuln-cve-2012-1182.nse, "nmap -Pn [IP] -p [PORT] --script=samba-vuln-cve-2012-1182.nse --script-args=unsafe=1", samba +samrdump=Run samrdump, python /usr/share/doc/python-impacket-doc/examples/samrdump.py [IP] [PORT]/SMB, "netbios-ssn,microsoft-ds" +showmount=Show nfs shares, showmount -e [IP], nfs +smb-brute.nse=smb-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smb-brute.nse --script-args=unsafe=1", smb +smb-check-vulns.nse=smb-check-vulns.nse, "nmap -Pn [IP] -p [PORT] --script=smb-check-vulns.nse --script-args=unsafe=1", smb +smb-enum-admins=Enumerate domain admins (net), "net rpc group members \"Domain Admins\" -I [IP] -U% ", "netbios-ssn,microsoft-ds" +smb-enum-domains.nse=smb-enum-domains.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-domains.nse --script-args=unsafe=1", smb +smb-enum-groups=Enumerate groups (nmap), "nmap -p[PORT] --script=smb-enum-groups [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-groups.nse=smb-enum-groups.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-groups.nse --script-args=unsafe=1", smb +smb-enum-policies=Extract password policy (nmap), "nmap -p[PORT] --script=smb-enum-domains [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-processes.nse=smb-enum-processes.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-processes.nse --script-args=unsafe=1", smb +smb-enum-sessions=Enumerate logged in users (nmap), "nmap -p[PORT] --script=smb-enum-sessions [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-sessions.nse=smb-enum-sessions.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-sessions.nse --script-args=unsafe=1", smb +smb-enum-shares=Enumerate shares (nmap), "nmap -p[PORT] --script=smb-enum-shares [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-shares.nse=smb-enum-shares.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-shares.nse --script-args=unsafe=1", smb +smb-enum-users=Enumerate users (nmap), "nmap -p[PORT] --script=smb-enum-users [IP] -vvvvv", "netbios-ssn,microsoft-ds" +smb-enum-users-rpc=Enumerate users (rpcclient), bash -c \"echo 'enumdomusers' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-enum-users.nse=smb-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smb-enum-users.nse --script-args=unsafe=1", smb +smb-flood.nse=smb-flood.nse, "nmap -Pn [IP] -p [PORT] --script=smb-flood.nse --script-args=unsafe=1", smb +smb-ls.nse=smb-ls.nse, "nmap -Pn [IP] -p [PORT] --script=smb-ls.nse --script-args=unsafe=1", smb +smb-mbenum.nse=smb-mbenum.nse, "nmap -Pn [IP] -p [PORT] --script=smb-mbenum.nse --script-args=unsafe=1", smb +smb-null-sessions=Check for null sessions (rpcclient), bash -c \"echo 'srvinfo' | rpcclient [IP] -U%\", "netbios-ssn,microsoft-ds" +smb-os-discovery.nse=smb-os-discovery.nse, "nmap -Pn [IP] -p [PORT] --script=smb-os-discovery.nse --script-args=unsafe=1", smb +smb-print-text.nse=smb-print-text.nse, "nmap -Pn [IP] -p [PORT] --script=smb-print-text.nse --script-args=unsafe=1", smb +smb-psexec.nse=smb-psexec.nse, "nmap -Pn [IP] -p [PORT] --script=smb-psexec.nse --script-args=unsafe=1", smb +smb-security-mode.nse=smb-security-mode.nse, "nmap -Pn [IP] -p [PORT] --script=smb-security-mode.nse --script-args=unsafe=1", smb +smb-server-stats.nse=smb-server-stats.nse, "nmap -Pn [IP] -p [PORT] --script=smb-server-stats.nse --script-args=unsafe=1", smb +smb-system-info.nse=smb-system-info.nse, "nmap -Pn [IP] -p [PORT] --script=smb-system-info.nse --script-args=unsafe=1", smb +smb-vuln-ms10-054.nse=smb-vuln-ms10-054.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-054.nse --script-args=unsafe=1", smb +smb-vuln-ms10-061.nse=smb-vuln-ms10-061.nse, "nmap -Pn [IP] -p [PORT] --script=smb-vuln-ms10-061.nse --script-args=unsafe=1", smb +smbenum=Run smbenum, bash ./scripts/smbenum.sh [IP], "netbios-ssn,microsoft-ds" +smbv2-enabled.nse=smbv2-enabled.nse, "nmap -Pn [IP] -p [PORT] --script=smbv2-enabled.nse --script-args=unsafe=1", smb +smtp-brute.nse=smtp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-brute.nse --script-args=unsafe=1", smtp +smtp-commands.nse=smtp-commands.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-commands.nse --script-args=unsafe=1", smtp +smtp-enum-expn=Enumerate SMTP users (EXPN), smtp-user-enum -M EXPN -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-rcpt=Enumerate SMTP users (RCPT), smtp-user-enum -M RCPT -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-enum-users.nse=smtp-enum-users.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-enum-users.nse --script-args=unsafe=1", smtp +smtp-enum-vrfy=Enumerate SMTP users (VRFY), smtp-user-enum -M VRFY -U /usr/share/metasploit-framework/data/wordlists/unix_users.txt -t [IP] -p [PORT], smtp +smtp-open-relay.nse=smtp-open-relay.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-open-relay.nse --script-args=unsafe=1", smtp +smtp-strangeport.nse=smtp-strangeport.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-strangeport.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2010-4344.nse=smtp-vuln-cve2010-4344.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2010-4344.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1720.nse=smtp-vuln-cve2011-1720.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1720.nse --script-args=unsafe=1", smtp +smtp-vuln-cve2011-1764.nse=smtp-vuln-cve2011-1764.nse, "nmap -Pn [IP] -p [PORT] --script=smtp-vuln-cve2011-1764.nse --script-args=unsafe=1", smtp +snmp-brute=Bruteforce community strings (medusa), bash -c \"medusa -h [IP] -u root -P ./wordlists/snmp-default.txt -M snmp | grep SUCCESS\", "snmp,snmptrap" +snmp-brute.nse=snmp-brute.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-brute.nse --script-args=unsafe=1", snmp +snmp-default=Check for default community strings, python ./scripts/snmpbrute.py -t [IP] -p [PORT] -f ./wordlists/snmp-default.txt -b --no-colours, "snmp,snmptrap" +snmp-hh3c-logins.nse=snmp-hh3c-logins.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-hh3c-logins.nse --script-args=unsafe=1", snmp +snmp-interfaces.nse=snmp-interfaces.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-interfaces.nse --script-args=unsafe=1", snmp +snmp-ios-config.nse=snmp-ios-config.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-ios-config.nse --script-args=unsafe=1", snmp +snmp-netstat.nse=snmp-netstat.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-netstat.nse --script-args=unsafe=1", snmp +snmp-processes.nse=snmp-processes.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-processes.nse --script-args=unsafe=1", snmp +snmp-sysdescr.nse=snmp-sysdescr.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-sysdescr.nse --script-args=unsafe=1", snmp +snmp-win32-services.nse=snmp-win32-services.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-services.nse --script-args=unsafe=1", snmp +snmp-win32-shares.nse=snmp-win32-shares.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-shares.nse --script-args=unsafe=1", snmp +snmp-win32-software.nse=snmp-win32-software.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-software.nse --script-args=unsafe=1", snmp +snmp-win32-users.nse=snmp-win32-users.nse, "nmap -Pn [IP] -p [PORT] --script=snmp-win32-users.nse --script-args=unsafe=1", snmp +snmpcheck=Run snmpcheck, snmpcheck -t [IP], "snmp,snmptrap" +ssh-default=Check for default ssh credentials, hydra -s [PORT] -C ./wordlists/ssh-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-root=Check for default ssh root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +ssh-default-router=Check for default ssh router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] ssh, ssh +sslscan=Run sslscan, sslscan --no-failed [IP]:[PORT], "https,ssl,https-alt" +sslyze=Run sslyze, sslyze --regular [IP]:[PORT], "https,ssl,ms-wbt-server,imap,pop3,smtp,https-alt" +telnet-default=Check for default telnet credentials, hydra -s [PORT] -C ./wordlists/telnet-betterdefaultpasslist.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-root=Check for default telnet root credentials, hydra -s [PORT] -C ./wordlists/root-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +telnet-default-router=Check for default telnet router credentials, hydra -s [PORT] -C ./wordlists/routers-userpass.txt -u -t 4 -o \"[OUTPUT].txt\" -f [IP] telnet, telnet +tftp-enum.nse=tftp-enum.nse, "nmap -Pn [IP] -p [PORT] --script=tftp-enum.nse --script-args=unsafe=1", tftp +theharvester=Run theharvester, theharvester -d [IP]:[PORT] -b all -n -c -t -h, dns +vnc-brute.nse=vnc-brute.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-brute.nse --script-args=unsafe=1", vnc +vnc-default=Check for default VNC credentials, hydra -s [PORT] -C ./wordlists/vnc-betterdefaultpasslist.txt -u -o \"[OUTPUT].txt\" -f [IP] vnc, vnc +vnc-info.nse=vnc-info.nse, "nmap -Pn [IP] -p [PORT] --script=vnc-info.nse --script-args=unsafe=1", vnc +wafw00f=Run wafw00f, wafw00f [IP]:[PORT], "https,ssl,https-alt" +whatweb=Run whatweb, "whatweb [IP]:[PORT] --color=never --log-brief=[OUTPUT].txt", "http,https,ssl,https-alt" +wpscan=Run wpscan, wpscan --url [IP], "http,https,ssl,https-alt" + +[PortTerminalActions] +firefox=Open with firefox, firefox [IP]:[PORT], +ftp=Open with ftp client, ftp [IP] [PORT], [term] ftp +mssql=Open with mssql client (as sa), [term] python /usr/share/doc/python-impacket-doc/examples/mssqlclient.py -p [PORT] sa@[IP], "mys-sql-s,codasrv-se" +mysql=Open with mysql client (as root), "[term] mysql -u root -h [IP] --port=[PORT] -p", mysql +netcat=Open with netcat, [term] nc -v [IP] [PORT], +psql=Open with postgres client (as postgres), [term] psql -h [IP] -p [PORT] -U postgres, postgres +rdesktop=Open with rdesktop, [term] rdesktop [IP]:[PORT], ms-wbt-server +rlogin=Open with rlogin, [term] rlogin -i root -p [PORT] [IP], login +rpcclient=Open with rpcclient (NULL session), [term] rpcclient [IP] -p [PORT] -U%, "netbios-ssn,microsoft-ds" +rsh=Open with rsh, rsh -l root [IP], [term] shell +ssh=Open with ssh client (as root), [term] ssh root@[IP] -p [PORT], ssh +telnet=Open with telnet, [term] telnet [IP] [PORT], +vncviewer=Open with vncviewer, vncviewer [IP]:[PORT], vnc +xephyr=Open with Xephyr, [term] Xephyr -query [IP] :1, xdmcp +xterm=Open terminal, [term] bash, + +[SchedulerSettings] +ftp-default=ftp, tcp +mssql-default=ms-sql-s, tcp +mysql-default=mysql, tcp +oracle-default=oracle-tns, tcp +postgres-default=postgresql, tcp +screenshooter="http,https,ssl,http-proxy,http-alt,https-alt", tcp +smbenum=microsoft-ds, tcp +smtp-enum-vrfy=smtp, tcp +snmp-default=snmp, udp +x11screen=X11, tcp + +[StagedNmapSettings] +stage1-ports="PORTS|T:80,81,443,4443,8080,8081,8082" +stage2-ports="PORTS|T:25,135,137,139,445,1433,3306,5432,U:137,161,162,1434" +stage3-ports=NSE|vulners +stage4-ports="PORTS|T:23,21,22,110,111,2049,3389,8080,U:500,5060" +stage5-ports="PORTS|T:0-20,24,26-79,81-109,112-134,136,138,140-442,444,446-1432,1434-2048,2050-3305,3307-3388,3390-5431,5433-8079,8081-29999" +stage6-ports=PORTS|T:30000-65535 + +[ToolSettings] +cutycapt-path=/usr/bin/cutycapt +hydra-path=/usr/bin/hydra +nmap-path=/sbin/nmap +pyshodan-api-key=YourKeyGoesHere +texteditor-path=/usr/bin/featherpad diff --git a/legion.py b/legion.py new file mode 100644 index 00000000..fddd3688 --- /dev/null +++ b/legion.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +import shutil + +from app.ApplicationInfo import getConsoleLogo +from app.ProjectManager import ProjectManager +from app.logging.legionLog import getStartupLogger, getDbLogger +from app.shell.DefaultShell import DefaultShell +from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter +from db.RepositoryFactory import RepositoryFactory +from ui.eventfilter import MyEventFilter +from ui.ViewState import ViewState +from ui.gui import * +from ui.gui import Ui_MainWindow + +startupLog = getStartupLogger() + +# check for dependencies first (make sure all non-standard dependencies are checked for here) +try: + from sqlalchemy.orm.scoping import ScopedSession as scoped_session +except ImportError as e: + startupLog.error( + "Import failed. SQL Alchemy library not found. If on Ubuntu or similar try: apt-get install python3-sqlalchemy*" + ) + startupLog.error(e) + exit(1) + +try: + from PyQt6 import QtWidgets, QtGui, QtCore + from PyQt6.QtCore import QCoreApplication +except ImportError as e: + startupLog.error("Import failed. PyQt6 library not found. If on Ubuntu or similar try: " + "apt-get install python3-pyqt5") + startupLog.error(e) + exit(1) + +try: + import qasync + import asyncio +except ImportError as e: + startupLog.error("Import failed. Quamash or asyncio not found.") + startupLog.error(e) + exit(1) + +try: + import sys + from colorama import init + + init(strip=not sys.stdout.isatty()) + from termcolor import cprint + from pyfiglet import figlet_format +except ImportError as e: + startupLog.error("Import failed. One or more of the terminal drawing libraries not found.") + startupLog.error(e) + exit(1) + + +# Check Nmap version is not 7.92- it segfaults under zsh constantly +import subprocess +checkNmapVersion = subprocess.check_output(['nmap', '-version']) + +# Quite upgrade of pyExploitDb +upgradeExploitDb = os.system('pip install pyExploitDb --upgrade > /dev/null 2>&1') + + +def doPathSetup(): + import os + if not os.path.isdir(os.path.expanduser("~/.local/share/legion/backup")): + os.makedirs(os.path.expanduser("~/.local/share/legion/backup")) + + if not os.path.exists(os.path.expanduser('~/.local/share/legion/legion.conf')): + shutil.copy('./legion.conf', os.path.expanduser('~/.local/share/legion/legion.conf')) + + +from ui.view import * +from controller.controller import * + +# Main application declaration and loop +if __name__ == "__main__": + cprint(getConsoleLogo()) + + doPathSetup() + + app = QApplication(sys.argv) + loop = qasync.QEventLoop(app) + asyncio.set_event_loop(loop) + + MainWindow = QtWidgets.QMainWindow() + Screen = QGuiApplication.primaryScreen() + app.setWindowIcon(QIcon('./images/icons/Legion-N_128x128.svg')) + + app.setStyleSheet("* { font-family: \"monospace\"; font-size: 10pt; }") + + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + + if os.geteuid()!=0: + startupLog.error("Legion must run as root for raw socket access. Please start legion using sudo.") + notice=QMessageBox() + notice.setIcon(QMessageBox.Icon.Critical) + notice.setText("Legion must run as root for raw socket access. Please start legion using sudo.") + notice.exec() + exit(1) + + if '7.92' in checkNmapVersion.decode(): + startupLog.error("Cannot continue. NMAP version is 7.92, which has problems segfaulting under zsh.") + startupLog.error("Please follow the instructions at https://github.com/Hackman238/legion/ to resolve.") + notice=QMessageBox() + notice.setIcon(QMessageBox.Icon.Critical) + notice.setText("Cannot continue. The installed NMAP version is 7.92, which has segfaults under zsh.\nPlease follow the instructions at https://github.com/Hackman238/legion/ to resolve.") + notice.exec_() + exit(1) + + # Possibly unneeded + #MainWindow.setStyleSheet(qss_file) + + shell = DefaultShell() + + dbLog = getDbLogger() + appLogger = getAppLogger() + + repositoryFactory = RepositoryFactory(dbLog) + projectManager = ProjectManager(shell, repositoryFactory, appLogger) + nmapExporter = DefaultNmapExporter(shell, appLogger) + toolCoordinator = ToolCoordinator(shell, nmapExporter) + + # Model prep (logic, db and models) + logic = Logic(shell, projectManager, toolCoordinator) + + startupLog.info("Creating temporary project at application start...") + logic.createNewTemporaryProject() + + viewState = ViewState() + view = View(viewState, ui, MainWindow, shell, app, loop) # View prep (gui) + controller = Controller(view, logic) # Controller prep (communication between model and view) + + # Possibly unneeded + #view.qss = qss_file + + myFilter = MyEventFilter(view, MainWindow) # to capture events + app.installEventFilter(myFilter) + + # Center the application in screen + screenCenter = Screen.availableGeometry().center() + MainWindow.move(screenCenter - MainWindow.rect().center()) + + # Show main window + #MainWindow.showMaximized() + + startupLog.info("Legion started successfully.") + try: + sys.exit(loop.run_forever()) + except KeyboardInterrupt: + pass + + #app.deleteLater() + #app.quit() + #loop.close() + #sys.exit() diff --git a/log/.gitkeep b/log/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/nmap.xsl b/nmap.xsl new file mode 100644 index 00000000..1be7ead5 --- /dev/null +++ b/nmap.xsl @@ -0,0 +1,1071 @@ + + + + + + + +0.9c + + + + + + + + + + + + + + + + + + + + +generated with nmap.xsl - version by Benjamin Erb - http://www.benjamin-erb.de/nmap_xsl.php + + + + Nmap Scan Report - Scanned at <xsl:value-of select="$start" /> + + + + + + + + +
+ +

Nmap Scan Report - Scanned at

+ +
+ + + scansummary + + + + +

Scan Summary

+ +

+ Nmap was initiated at with these arguments:
+
+

+

+ Verbosity: ; Debug level +

+ +

+ +

+ + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + host_ + + + + + +

+ + + + + / + + + + (online) +

+ +
+ + +

+ + + + + / + + + + + javascript:toggle('hostblock_'); + host_down + (click to expand) + + (offline)

+
+ +
+ + + + hostblock_ + + + + unhidden + + + + hidden + + + + + +

Address

+ +
    + +
  • + + - + + + + () +
  • +
    +
+
+ + + + +
+ + + javascript:toggle('metrics_'); + Misc Metrics (click to expand) + + + + + metrics_ + hidden + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricValue
Ping Results + + from + + +
System Uptime seconds (last reboot: ) +
Network Distance hops
TCP Sequence PredictionDifficulty= ()
IP ID Sequence Generation
+
+ +
+ +
+ + + + + + + +

Hostnames

+
+ + + + + +
  • ()
  • +
    + + + + + + +

    Ports

    + + +

    The ports scanned but not shown below are in state:

    +
    + +
      + + +
    • ports replied with:

    • +
      +
      +
    +
    + + + + + + + + + + + porttable_ + 1 + + + Port + State + + javascript:togglePorts('porttable_','closed'); + (toggle closed [] + + + javascript:togglePorts('porttable_','filtered'); + | filtered []) + + + Service + Reason + Product + Version + Extra info + + + + + +
    + + + + + + + + + + + + +   + + + from + + + +   +   +   + + + + + +   + +
      
    + + + +
    +
    + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + + + + + + +   + + + from + + + +   +   +   + + + +
    +
    + + + + + +

    Remote Operating System Detection

    + +

    Unable to identify operating system.

    + +
      + +
    • Used port: / ()
    • +
      + + +
    • OS match: (%)
    • +
      +
    + + + +
    + + + + + + + + + + + + +
      +
    • Cannot determine exact operating system. Fingerprint provided below.
    • +
    • If you know what OS is running on it, see https://nmap.org/submit/
    • +
    + + + + + + + +
    Operating System fingerprint
    + +
    + + +
      +
    • OS identified but the fingerprint was requested at scan time. + + + javascript:toggle('osblock_'); + (click to expand) + +
    • +
    + + + osblock_ + hidden + + + + + + + + +
    Operating System fingerprint
    + +
    + +
    + +
    + +
    + + + + + + + + + + prescript + + +

    Pre-Scan Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + + + + + postscript + + +

    Post-Scan Script Putput

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +           
    +        
    +
    +
    + + + + + + +

    Host Script Output

    + + + + + + + + + + + + + + +
    Script NameOutput
    +   + +
    +              
    +          
    +
    +
    + + + + + +

    Smurf Responses

    +
      +
    • responses counted
    • +
    +
    +
    + + + + + + + + + + + + + + javascript:toggle('trace_'); + Traceroute Information (click to expand) + + + + trace_ + hidden + + + + +
    • Traceroute data generated using port /
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    HopRttIPHost
    --
    +
    + +
    +
    + +
    diff --git a/parsers/CVE.py b/parsers/CVE.py new file mode 100644 index 00000000..b65ddbd7 --- /dev/null +++ b/parsers/CVE.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 + + +class CVE: + name = '' + product = '' + version = '' + url = '' + source = '' + severity = '' + exploitId = '' + exploit = '' + exploitUrl = '' + + def __init__(self, cveData): + self.name = cveData.get('id', 'unknown') + self.product = cveData.get('product', 'unknown') + self.version = cveData.get('version', 'unknown') + self.url = cveData.get('url', 'unknown') + self.source = cveData.get('source', 'unknown') + self.severity = cveData.get('severity', 'unknown') + self.exploitId = cveData.get('exploitId', 'unknown') + self.exploit = cveData.get('exploit', 'unknown') + self.exploitUrl = cveData.get('exploitUrl', 'unknown') diff --git a/parsers/Host.py b/parsers/Host.py new file mode 100644 index 00000000..d971d608 --- /dev/null +++ b/parsers/Host.py @@ -0,0 +1,118 @@ +#!/usr/bin/python3 + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' +__modified_by = 'ketchup' + +import parsers.Service as Service +import parsers.Script as Script +import parsers.OS as OS +import parsers.Port as Port + +class Host: + ipv4 = '' + ipv6 = '' + macaddr = '' + status = None + hostname = '' + vendor = '' + uptime = '' + lastboot = '' + distance = 0 + state = '' + count = '' + + def __init__( self, HostNode ): + self.hostNode = HostNode + self.status = HostNode.getElementsByTagName('status')[0].getAttribute('state') + for e in HostNode.getElementsByTagName('address'): + if e.getAttribute('addrtype') == 'ipv4': + self.ipv4 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'ipv6': + self.ipv6 = e.getAttribute('addr') + elif e.getAttribute('addrtype') == 'mac': + self.macaddr = e.getAttribute('addr') + self.vendor = e.getAttribute('vendor') + self.ip = HostNode.getElementsByTagName('address')[0].getAttribute('addr'); + #self.ip = self.ipv4 # for compatibility with the original library + if len(HostNode.getElementsByTagName('hostname')) > 0: + self.hostname = HostNode.getElementsByTagName('hostname')[0].getAttribute('name') + if len(HostNode.getElementsByTagName('uptime')) > 0: + self.uptime = HostNode.getElementsByTagName('uptime')[0].getAttribute('seconds') + self.lastboot = HostNode.getElementsByTagName('uptime')[0].getAttribute('lastboot') + if len(HostNode.getElementsByTagName('distance')) > 0: + self.distance = int(HostNode.getElementsByTagName('distance')[0].getAttribute('value')) + if len(HostNode.getElementsByTagName('extraports')) > 0: + self.state = HostNode.getElementsByTagName('extraports')[0].getAttribute('state') + self.count = HostNode.getElementsByTagName('extraports')[0].getAttribute('count') + + def getOs(self): + oss = [] + + for osNode in self.hostNode.getElementsByTagName('osfamily'): + os = OS.OS(osNode) + oss.append(os) + + for osNode in self.hostNode.getElementsByTagName('osclass'): + os = OS.OS(osNode) + oss.append(os) + + for osNode in self.hostNode.getElementsByTagName('osmatch'): + os = OS.OS(osNode) + oss.append(os) + + return oss + + def all_ports( self ): + + ports = [] + + for portNode in self.hostNode.getElementsByTagName('port'): + p = Port.Port(portNode) + ports.append(p) + + return ports + + def getPorts( self, protocol, state ): + '''get a list of ports which is in the special state''' + + open_ports = [] + + for portNode in self.hostNode.getElementsByTagName('port'): + if portNode.getAttribute('protocol') == protocol and portNode.getElementsByTagName('state')[0]\ + .getAttribute('state') == state: + open_ports.append( portNode.getAttribute('portid') ) + + return open_ports + + def getScripts( self ): + + scripts = [] + + for scriptNode in self.hostNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) + scr.hostId = self.ipv4 + scripts.append(scr) + + return scripts + + def getHostScripts( self ): + + scripts = [] + for hostscriptNode in self.hostNode.getElementsByTagName('hostscript'): + for scriptNode in hostscriptNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) + scripts.append(scr) + + return scripts + + def getService( self, protocol, port ): + '''return a Service object''' + + for portNode in self.hostNode.getElementsByTagName('port'): + if portNode.getAttribute('protocol') == protocol and portNode.getAttribute('portid') == port and \ + len(portNode.getElementsByTagName('service')) > 0: + service_node = portNode.getElementsByTagName('service')[0] + service = Service.Service( service_node ) + return service + return None diff --git a/parsers/OS.py b/parsers/OS.py new file mode 100644 index 00000000..8df11da0 --- /dev/null +++ b/parsers/OS.py @@ -0,0 +1,23 @@ +#!/usr/bin/python3 + +__author__ = 'ketchup' +__version__= '0.1' +__modified_by = 'ketchup' + +class OS: + name = '' + family = '' + generation = '' + osType = '' + vendor = '' + accuracy = 0 + + def __init__(self, OSNode): + if not (OSNode is None): + self.name = OSNode.getAttribute('name') + self.family = OSNode.getAttribute('osfamily') + self.generation = OSNode.getAttribute('osgen') + self.osType = OSNode.getAttribute('type') + self.vendor = OSNode.getAttribute('vendor') + self.accuracy = OSNode.getAttribute('accuracy') + diff --git a/parsers/Parser.py b/parsers/Parser.py new file mode 100644 index 00000000..9c49ff8e --- /dev/null +++ b/parsers/Parser.py @@ -0,0 +1,99 @@ +#!/usr/bin/python3 + +'''this module used to parse nmap xml report''' + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__ = '0.2' + +from typing import Optional +from xml.dom.minidom import parse, Document + +__modified_by = 'ketchup' +__modified_by = 'SECFORCE' + +import parsers.Session as Session +import parsers.Host as Host + + +class MalformedXmlDocumentException(BaseException): + pass + + +class Parser: + '''Parser class, parse a xml format nmap report''' + + def __init__(self, dom: Document): + self.__dom = dom + self.__session = None + self.__hosts = {} + for hostNode in self.__dom.getElementsByTagName('host'): + __host = Host.Host(hostNode) + self.__hosts[__host.ip] = __host + + def getSession(self): + '''get this scans information, return a Session object''' + run_node = self.__dom.getElementsByTagName('nmaprun')[0] + hosts_node = self.__dom.getElementsByTagName('hosts')[0] + + finish_time = self.__dom.getElementsByTagName('finished')[0].getAttribute('timestr') + + nmapVersion = run_node.getAttribute('version') + startTime = run_node.getAttribute('startstr') + scanArgs = run_node.getAttribute('args') + + totalHosts = hosts_node.getAttribute('total') + upHosts = hosts_node.getAttribute('up') + downHosts = hosts_node.getAttribute('down') + + MySession = {'finish_time': finish_time, + 'nmapVersion': nmapVersion, + 'scanArgs': scanArgs, + 'startTime': startTime, + 'totalHosts': totalHosts, + 'upHosts': upHosts, + 'downHosts': downHosts} + + self.__session = Session.Session(MySession) + + return self.__session + + def getHost(self, ipaddr: str) -> Optional[Host.Host]: + '''get a Host object by ip address''' + return self.__hosts.get(ipaddr) + + def getAllHosts(self, status=None): + '''get a list of Host object''' + if status is None: + return self.__hosts.values() + + else: + __tmp_hosts = [] + for __host in self.__hosts.values(): + + if __host.status == status: + __tmp_hosts.append(__host) + + return __tmp_hosts + + def getAllIps(self, status=None): + '''get a list of ip address''' + __tmp_ips = [] + + if status is None: + for __host in self.__hosts.values(): + __tmp_ips.append(__host.ip) + + else: + for __host in self.__hosts.values(): + + if __host.status == status: + __tmp_ips.append(__host.ip) + + return __tmp_ips + + +def parseNmapReport(nmapXmlReportFileName: str) -> Parser: + try: + return Parser(parse(nmapXmlReportFileName)) + except Exception as e: + raise MalformedXmlDocumentException(e) diff --git a/parsers/Port.py b/parsers/Port.py new file mode 100644 index 00000000..45d31ff7 --- /dev/null +++ b/parsers/Port.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +__author__ = 'SECFORCE' +__version__ = '0.1' + +from typing import Optional + +import parsers.Service as Service +import parsers.Script as Script + + +class Port: + portId: str = '' + protocol: str = '' + state: str = '' + + def __init__(self, PortNode): + if not (PortNode is None): + self.portNode = PortNode + self.portId = PortNode.getAttribute('portid') + self.protocol = PortNode.getAttribute('protocol') + self.state = PortNode.getElementsByTagName('state')[0].getAttribute('state') + + def getService(self) -> Optional[Service.Service]: + service_node = self.portNode.getElementsByTagName('service') + + if len(service_node) > 0: + return Service.Service(service_node[0]) + + return None + + # def get_cpe(self): + + # cpes = [] + # cpe = self.portNode.getElementsByTagName('cpe') + # print(cpe) + + # if len(cpe) > 0: + # return CPE.CPE(cpe[0]) + + # return None + + def getScripts(self): + scripts = [] + for scriptNode in self.portNode.getElementsByTagName('script'): + scr = Script.Script(scriptNode) + scripts.append(scr) + + return scripts diff --git a/parsers/Script.py b/parsers/Script.py new file mode 100644 index 00000000..c548ed41 --- /dev/null +++ b/parsers/Script.py @@ -0,0 +1,136 @@ +#!/usr/bin/python3 +from db.entities.cve import cve + +__author__ = 'ketchup' +__version__= '0.1' +__modified_by = 'ketchup' + +import parsers.CVE as CVE +from pyExploitDb import PyExploitDb + +class Script: + scriptId = '' + output = '' + + def __init__(self, ScriptNode): + if not (ScriptNode is None): + self.scriptId = ScriptNode.getAttribute('id') + self.output = ScriptNode.getAttribute('output') + + def processShodanScriptOutput(self, shodanOutput): + output = shodanOutput.replace('\t\t\t','\t') + output = output.replace('\t\t','\t') + output = output.replace('\t',';') + output = output.replace('\n;','\n') + output = output.replace(' ','') + output = output.split('\n') + output = [entry for entry in output if len(entry) > 1] + print(str(output)) + + + def processVulnersScriptOutput(self, vulnersOutput): + output = vulnersOutput.replace('\t\t\t','\t') + output = output.replace('\t\t','\t') + output = output.replace('\t',';') + output = output.replace('\n;','\n') + output = output.replace(' ','') + output = output.split('\n') + output = [entry for entry in output if len(entry) > 1] + + pyExploitDb = PyExploitDb() + pyExploitDb.debug = False + pyExploitDb.autoUpdate = False + pyExploitDb.openFile() + + cpeList = [] + count = 0 + for entry in output: + if 'cpe' in entry: + cpeList.append(entry) + output[count] = 'CPE' + count = count + 1 + + output = ' '.join(output) + output = output.split('CPE') + output = [entry for entry in output if len(entry) > 1] + + resultsDict = {} + counter = 0 + for cpeEntry in cpeList: + resultCpeData = cpeEntry.split(':') + resultCpeData = [entry for entry in resultCpeData if len(entry) > 1] + resultCpeDetails = {} + resultCpeDetails['type'] = resultCpeData[1] + resultCpeDetails['source'] = resultCpeData[2] + resultCpeDetails['product'] = resultCpeData[3] + resultCpeDetails['version'] = resultCpeData[4] + resultCves = output[counter] + resultCves = resultCves.split(' ') + resultCves = [entry for entry in resultCves if len(entry) > 1] + resultCvesProcessed = [] + for resultCve in resultCves: + resultCveDict = {} + resultCveData = resultCve.split(';') + resultCveDict['id'] = resultCveData[0] + resultCveDict['severity'] = resultCveData[1] + resultCveDict['url'] = resultCveData[2] + exploitResults = pyExploitDb.searchCve(resultCveData[0]) + print(exploitResults) + if exploitResults: + resultCveDict['exploitId'] = exploitResults['edbid'] + resultCveDict['exploit'] = exploitResults['exploit'] + resultCveDict['exploitUrl'] = "https://www.exploit-db.com/exploits/{0}".\ + format(resultCveDict['exploitId']) + resultCvesProcessed.append(resultCveDict) + resultCpeDetails['cves'] = resultCvesProcessed + resultsDict[resultCpeData[3]] = resultCpeDetails + count = count + 1 + + return resultsDict + + def getCves(self): + cveOutput = self.output + cveObjects = [] + + if len(cveOutput) > 0: + cvesResults = self.processVulnersScriptOutput(cveOutput) + + for cpeEntry in cvesResults: + cpeData = cvesResults[cpeEntry] + cpeProduct = cpeEntry + cpeType = cpeData['type'] + cpeVersion = cpeData['version'] + cpeSource = cpeData['source'] + cpeCves = cpeData['cves'] + for cveEntry in cpeCves: + cveData = cveEntry + cveData['type'] = cpeType + cveData['version'] = cpeVersion + cveData['source'] = cpeSource + cveData['product'] = cpeProduct + print("NEW CVE: {0}".format(cveData)) + cveObj = CVE.CVE(cveData) + cveObjects.append(cveObj) + return cveObjects + return None + + def scriptSelector(self, host): + scriptId = str(self.scriptId).lower() + results = [] + if 'vulners' in scriptId: + print("------------------------VULNERS") + cveResults = self.getCves() + for cveEntry in cveResults: + t_cve = cve(name=cveEntry.name, url=cveEntry.url, source=cveEntry.source, + severity=cveEntry.severity, product=cveEntry.product, version=cveEntry.version, + hostId=host.id, exploitId=cveEntry.exploitId, exploit=cveEntry.exploit, + exploitUrl=cveEntry.exploitUrl) + results.append(t_cve) + return results + elif 'shodan-api' in scriptId: + print("------------------------SHODAN") + #self.processShodanScriptOutput(self.output) + return results + else: + print("-----------------------*{0}".format(scriptId)) + return results diff --git a/parsers/Service.py b/parsers/Service.py new file mode 100644 index 00000000..d55257dc --- /dev/null +++ b/parsers/Service.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__ = '0.2' +__modified_by = 'ketchup' + + +class Service: + extrainfo: str = '' + name: str = '' + product: str = '' + fingerprint: str = '' + version: str = '' + + def __init__(self, ServiceNode): + self.extrainfo = ServiceNode.getAttribute('extrainfo') + self.name = ServiceNode.getAttribute('name') + self.product = ServiceNode.getAttribute('product') + self.fingerprint = ServiceNode.getAttribute('servicefp') + self.version = ServiceNode.getAttribute('version') diff --git a/parsers/Session.py b/parsers/Session.py new file mode 100644 index 00000000..6330314a --- /dev/null +++ b/parsers/Session.py @@ -0,0 +1,15 @@ +#!/usr/bin/python3 + +__author__ = 'yunshu(wustyunshu@hotmail.com)' +__version__= '0.2' + +class Session: + def __init__( self, SessionHT ): + self.startTime = SessionHT.get('startTime', '') + self.finish_time = SessionHT.get('finish_time', '') + self.nmapVersion = SessionHT.get('nmapVersion', '') + self.scanArgs = SessionHT.get('scanArgs', '') + self.totalHosts = SessionHT.get('totalHosts', '') + self.upHosts = SessionHT.get('upHosts', '') + self.downHosts = SessionHT.get('downHosts', '') + diff --git a/parsers/__init__.py b/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/parsers/examples/HostExample.py b/parsers/examples/HostExample.py new file mode 100644 index 00000000..c10de10d --- /dev/null +++ b/parsers/examples/HostExample.py @@ -0,0 +1,59 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import sys + +import xml + +from app.logging.legionLog import getAppLogger +from parsers.Host import Host + +log = getAppLogger() + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('/tmp/test_pwn01.xml') + hostNodes = dom.getElementsByTagName('host') + + if len(hostNodes) == 0: + sys.exit( ) + + hostNode = dom.getElementsByTagName('host')[0] + + h = Host( hostNode ) + log.info('host status: ' + h.status) + log.info('host ip: ' + h.ip) + + for port in h.getPorts( 'tcp', 'open' ): + log.info(port + " is open") + + log.info("script output:") + for scr in h.getScripts(): + log.info("script id:" + scr.scriptId) + log.info("Output:") + log.info(scr.output) + + log.info("service of tcp port 80:") + s = h.getService( 'tcp', '80' ) + if s is None: + log.info("\tno service") + + else: + log.info("\t" + s.name) + log.info("\t" + s.product) + log.info("\t" + s.version) + log.info("\t" + s.extrainfo) + log.info("\t" + s.fingerprint) \ No newline at end of file diff --git a/parsers/examples/OsExample.py b/parsers/examples/OsExample.py new file mode 100644 index 00000000..19c9afd6 --- /dev/null +++ b/parsers/examples/OsExample.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import xml + +from app.auxiliary import log +from parsers.OS import OS + +if __name__ == '__main__': + dom = xml.dom.minidom.parse('test.xml') + osclass = dom.getElementsByTagName('osclass')[0] + osmatch = dom.getElementsByTagName('osmatch')[0] + + os = OS(osclass) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.osType) + log.info(os.vendor) + log.info(str(os.accuracy)) + + os = OS(osmatch) + log.info(os.name) + log.info(os.family) + log.info(os.generation) + log.info(os.osType) + log.info(os.vendor) + log.info(str(os.accuracy)) \ No newline at end of file diff --git a/parsers/examples/ParserExample.py b/parsers/examples/ParserExample.py new file mode 100644 index 00000000..6d470b90 --- /dev/null +++ b/parsers/examples/ParserExample.py @@ -0,0 +1,65 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +from app.auxiliary import log +from parsers.Parser import parseNmapReport + +if __name__ == '__main__': + parser = parseNmapReport('a-full.xml') + + log.info('\nscan session:') + session = parser.getSession() + log.info("\tstart time:\t" + session.startTime) + log.info("\tstop time:\t" + session.finish_time) + log.info("\tnmap version:\t" + session.nmapVersion) + log.info("\tnmap args:\t" + session.scanArgs) + log.info("\ttotal hosts:\t" + session.totalHosts) + log.info("\tup hosts:\t" + session.upHosts) + log.info("\tdown hosts:\t" + session.downHosts) + + for h in parser.getAllHosts(): + + log.info('host ' + h.ip + ' is ' + h.status) + + for port in h.getPorts('tcp', 'open'): + print(port) + log.info("\t---------------------------------------------------") + log.info("\tservice of tcp port " + port + ":") + s = h.getService('tcp', port) + + if s == None: + log.info("\t\tno service") + + else: + log.info("\t\t" + s.name) + log.info("\t\t" + s.product) + log.info("\t\t" + s.version) + log.info("\t\t" + s.extrainfo) + log.info("\t\t" + s.fingerprint) + + log.info("\tscript output:") + sc = port.getScripts() + + if sc == None: + log.info("\t\tno scripts") + + else: + for scr in sc: + log.info("Script ID: " + scr.scriptId) + log.info("Output: ") + log.info(scr.output) + + log.info("\t---------------------------------------------------") \ No newline at end of file diff --git a/parsers/examples/ScriptExample.py b/parsers/examples/ScriptExample.py new file mode 100644 index 00000000..6d402d5b --- /dev/null +++ b/parsers/examples/ScriptExample.py @@ -0,0 +1,28 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +import xml + +from app.auxiliary import log +from parsers.Script import Script + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('a-full.xml') + + for scriptNode in dom.getElementsByTagName('script'): + script = Script(scriptNode) + log.info(script.scriptId) + log.info(script.output) diff --git a/parsers/examples/ServiceExample.py b/parsers/examples/ServiceExample.py new file mode 100644 index 00000000..13809cae --- /dev/null +++ b/parsers/examples/ServiceExample.py @@ -0,0 +1,38 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import sys +import xml + +from app.auxiliary import log +from parsers.Service import Service + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + + service_nodes = dom.getElementsByTagName('service') + if len(service_nodes) == 0: + sys.exit() + + node = dom.getElementsByTagName('service')[0] + + s = Service( node ) + log.info(s.name) + log.info(s.product) + log.info(s.version) + log.info(s.extrainfo) + log.info(s.fingerprint) \ No newline at end of file diff --git a/parsers/examples/SessionExample.py b/parsers/examples/SessionExample.py new file mode 100644 index 00000000..aed1b64a --- /dev/null +++ b/parsers/examples/SessionExample.py @@ -0,0 +1,40 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" +import xml + +from app.auxiliary import log +from parsers.Session import Session + +if __name__ == '__main__': + + dom = xml.dom.minidom.parse('i.xml') + dom.getElementsByTagName('finished')[0].getAttribute('timestr') + + MySession = { 'finish_time': dom.getElementsByTagName('finished')[0].getAttribute('timestr'), + 'nmapVersion' : '4.79', 'scanArgs' : '-sS -sV -A -T4', + 'startTime' : dom.getElementsByTagName('nmaprun')[0].getAttribute('startstr'), + 'totalHosts' : '1', 'upHosts' : '1', 'downHosts' : '0' } + + s = Session( MySession ) + + log.info('startTime:' + s.startTime) + log.info('finish_time:' + s.finish_time) + log.info('nmapVersion:' + s.nmapVersion) + log.info('nmap_args:' + s.scanArgs) + log.info('total hosts:' + s.totalHosts) + log.info('up hosts:' + s.upHosts) + log.info('down hosts:' + s.downHosts) \ No newline at end of file diff --git a/parsers/examples/__init__.py b/parsers/examples/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/parsers/examples/__init__.py @@ -0,0 +1 @@ + diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/precommit.sh b/precommit.sh new file mode 100755 index 00000000..a2234284 --- /dev/null +++ b/precommit.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Update last update in controller +sed -i -r "s/update\": .*?/update\": '`date '+%m\/%d\/%Y'`',/g" ./app/ApplicationInfo.py +sed -i -r "s/build\": .*?/build\": '`date '+%s'`',/g" ./app/ApplicationInfo.py + +# Clear logs + +echo "" > ./log/legion.log +echo "" > ./log/legion-db.log +echo "" > ./log/legion-startup.log + +# Prep hidden files +rm -f .initialized +touch .justcloned + +# Clear tmp +rm -Rf ./tmp/* + +# Clear all pyc and pyc +find . -name \*.pyc -delete +find . -name \*.pyo -delete + +# Remove cloned scripts +rm -Rf ./scripts/CloudFail/ + +# Removed backups +rm -Rf ./backup/*.conf + +find . -type f -exec sed -i 's/Copyright (c) 2024 Shane Scott/Copyright (c) 2024 Shane Scott/' {} \; diff --git a/primeExploitDb.py b/primeExploitDb.py new file mode 100755 index 00000000..0e61a184 --- /dev/null +++ b/primeExploitDb.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +from pyExploitDb import PyExploitDb + + +def prime(): + pEdb = PyExploitDb() + pEdb.debug = False + pEdb.openFile() + + +if __name__ == "__main__": + prime() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ce15286e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +asyncio +six +qasync +SQLAlchemy==1.4.50 +PyQt6 +requests +pyfiglet +colorama +termcolor +pyExploitDb +pyShodan +GitPython +pandas +flake8 +rich +urllib3==1.24.3 +selenium==3.141.0 diff --git a/scripts/exec-in-shell b/scripts/exec-in-shell new file mode 100755 index 00000000..679ac013 --- /dev/null +++ b/scripts/exec-in-shell @@ -0,0 +1,5 @@ +#!/bin/sh +eval $@ +USER=${USER:-$(whoami)} +SHELL=${SHELL:-$(getent passwd $USER|cut -d: -f7)} +${SHELL:-bash} -i diff --git a/scripts/fingertool.sh b/scripts/fingertool.sh new file mode 100755 index 00000000..06223d92 --- /dev/null +++ b/scripts/fingertool.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# fingertool - This script will enumerate users using finger +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 []" + echo "eg: $0 10.10.10.10 users.txt" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + WORDLIST="/usr/share/metasploit-framework/data/wordlists/unix_users.txt" + else + WORDLIST="$2" +fi + + +for username in $(cat $WORDLIST | sort -u| uniq) + do output=$(finger -l $username@$IP) + if [[ $output == *"Directory"* ]] + then + echo "Found user: $username" + fi + done + +echo "Finished!" \ No newline at end of file diff --git a/scripts/nmap/shodan-api.nse b/scripts/nmap/shodan-api.nse new file mode 100755 index 00000000..8dab2298 --- /dev/null +++ b/scripts/nmap/shodan-api.nse @@ -0,0 +1,223 @@ +local http = require "http" +local io = require "io" +local ipOps = require "ipOps" +local json = require "json" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local tab = require "tab" +local table = require "table" +local openssl = stdnse.silent_require "openssl" + + +-- Set your Shodan API key here to avoid typing it in every time: +local apiKey = "" + +author = "Glenn Wilkinson (idea: Charl van der Walt )" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe", "external"} + +description = [[ +Queries Shodan API for given targets and produces similar output to +a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script +argument, or hardcoded in the .nse file itself. You can get a free key from +https://developer.shodan.io + +N.B if you want this script to run completely passively make sure to +include the -sn -Pn -n flags. +]] + +--- +-- @usage +-- nmap --script shodan-api x.y.z.0/24 -sn -Pn -n --script-args 'shodan-api.outfile=potato.csv,shodan-api.apikey=SHODANAPIKEY' +-- nmap --script shodan-api --script-args 'shodan-api.target=x.y.z.a,shodan-api.apikey=SHODANAPIKEY' +-- +-- @output +-- | shodan-api: Report for 2600:3c01::f03c:91ff:fe18:bb2f (scanme.nmap.org) +-- | PORT PROTO PRODUCT VERSION +-- | 80 tcp Apache httpd +-- | 3306 tcp MySQL 5.5.40-0+wheezy1 +-- | 22 tcp OpenSSH 6.0p1 Debian 4+deb7u2 +-- |_443 tcp +-- +--@args shodan-api.outfile Write the results to the specified CSV file +--@args shodan-api.apikey Specify the ShodanAPI key. This can also be hardcoded in the nse file. +--@args shodan-api.target Specify a single target to be scanned. +-- +--@xmloutput +-- +-- scanme.nmap.org +--
    +-- +--
    +-- tcp +-- 22 +--
    +-- +-- 2.4.7 +-- Apache httpd +-- tcp +-- 80 +--
    +-- + +-- ToDo: * Have an option to complement non-banner scans with shodan data (e.g. -sS scan, but +-- grab service info from Shodan +-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of +-- scan, verbose port output (e.g. smb share info) +-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host +-- * Accept IP ranges via the script argument 'target' parameter + + +-- Begin +if not nmap.registry[SCRIPT_NAME] then + nmap.registry[SCRIPT_NAME] = { + apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey") or apiKey, + count = 0 + } +end +local registry = nmap.registry[SCRIPT_NAME] +local outFile = stdnse.get_script_args(SCRIPT_NAME .. ".outfile") +local arg_target = stdnse.get_script_args(SCRIPT_NAME .. ".target") + +local function lookup_target (target) + local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. registry.apiKey, {any_af = true}) + if response.status == 404 then + stdnse.debug1("Host not found: %s", target) + return nil + elseif (response.status ~= 200) then + stdnse.debug1("Bad response from Shodan for IP %s : %s", target, response.status) + return nil + end + + local stat, resp = json.parse(response.body) + if not stat then + stdnse.debug1("Error parsing Shodan response: %s", resp) + return nil + end + + return resp +end + +local function format_output(resp) + if resp.error then + return resp.error + end + + if resp.data then + registry.count = registry.count + 1 + local out = { hostnames = resp.hostnames, ports = {} } + local ports = out.ports + local tab_out = tab.new() + tab.addrow(tab_out, "PORT", "PROTO", "PRODUCT", "VERSION") + + for key, e in ipairs(resp.data) do + ports[#ports+1] = { + number = e.port, + protocol = e.transport, + product = e.product, + version = e.version, + } + tab.addrow(tab_out, e.port, e.transport, e.product or "", e.version or "") + end + return out, tab.dump(tab_out) + else + return "Unable to query data" + end +end + +prerule = function () + if (outFile ~= nil) then + local file = io.open(outFile, "w") + io.output(file) + io.write("IP,Port,Proto,Product,Version\n") + end + + if registry.apiKey == "" then + registry.apiKey = nil + end + + if not registry.apiKey then + stdnse.verbose1("Error: Please specify your ShodanAPI key with the %s.apikey argument", SCRIPT_NAME) + return false + end + + local response = http.get("api.shodan.io", 443, "/api-info?key=" .. registry.apiKey, {any_af=true}) + if (response.status ~= 200) then + stdnse.verbose1("Error: Your ShodanAPI key (%s) is invalid", registry.apiKey) + -- Prevent further stages from running + registry.apiKey = nil + return false + end + + if arg_target then + local is_ip, err = ipOps.expand_ip(arg_target) + if not is_ip then + stdnse.verbose1("Error: %s.target must be an IP address", SCRIPT_NAME) + return false + end + return true + end +end + +generic_action = function(ip) + local resp = lookup_target(ip) + if not resp then return nil end + local out, tabular = format_output(resp) + if type(out) == "string" then + -- some kind of error + return out + end + local result = string.format( + "Report for %s (%s)\n%s", + ip, + table.concat(out.hostnames, ", "), + tabular + ) + if (outFile ~= nil) then + for _, port in ipairs(out.ports) do + io.write( string.format("%s,%s,%s,%s,%s\n", + ip, port.number, port.protocol, port.product or "", port.version or "") + ) + end + end + return out, result +end + +preaction = function() + return generic_action(arg_target) +end + +hostrule = function(host) + return registry.apiKey and not ipOps.isPrivate(host.ip) +end + +hostaction = function(host) + return generic_action(host.ip) +end + +postrule = function () + return registry.apiKey +end + +postaction = function () + local out = { "Shodan done: ", registry.count, " hosts up." } + if outFile then + io.close() + out[#out+1] = "\nWrote Shodan output to: " + out[#out+1] = outFile + end + return table.concat(out) +end + +local ActionsTable = { + -- prerule: scan target from script-args + prerule = preaction, + -- hostrule: look up a host in Shodan + hostrule = hostaction, + -- postrule: report results + postrule = postaction +} + +-- execute the action function corresponding to the current rule +action = function(...) return ActionsTable[SCRIPT_TYPE](...) end diff --git a/scripts/nmap/shodan-hq.nse b/scripts/nmap/shodan-hq.nse new file mode 100755 index 00000000..c11eddb7 --- /dev/null +++ b/scripts/nmap/shodan-hq.nse @@ -0,0 +1,135 @@ +local http = require "http" +local io = require "io" +local json = require "json" +local stdnse = require "stdnse" +local openssl = stdnse.silent_require "openssl" + + +-- Set your Shodan API key here to avoid typing it in every time: +local apiKey = "" + +author = "Glenn Wilkinson <@glennzw> (idea: Charl van der Walt <@charlvdwalt> )" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} +url = "https://github.com/glennzw/shodan-hq-nse" + +description = [[ +Queries Shodan API for given targets and produces similar output to +a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script +argument, or hardcoded in the .nse file itself. + +N.B if you want this script to run completely passively make sure to +include the -sn -Pn -n flags. + +Example usage: + +nmap --script shodan-hq.nse x.y.z.0/24 -sn -Pn -n --script-args 'outfile=potato.csv,apikey=SHODANAPIKEY' + + +You can also specify a single target with a script argument: + +nmap --script shodan-hq.nse --script-args 'target=x.y.z.a' + +Contact: [glenn|charl]@sensepost.com +Code : https://github.com/glennzw/shodan-hq-nse +]] + +--- +-- @output +-- | +-- | PORT STATE SERVICE VERSION +-- | 80/tcp open Apache httpd +-- | 3306/tcp open MySQL 5.5.40-0+wheezy1 +-- | 22/tcp open OpenSSH 6.0p1 Debian 4+deb7u2 +-- +--@args outfile Write the results to the specified CSV file +--@args apiKey Specify the ShodanAPI key. This can also be hardcoded in the nse file. +--@args target Specify a single target to be scanned. + +-- ToDo: * Have an option to compliment non banner scans with shodan data (e.g. -sS scan, but +-- grab service info from Shodan +-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of +-- scan, verbose port output (e.g. smb share info) +-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host +-- * Accept IP ranges via the script argument 'target' parameter + + +-- Begin +local scriptApiKey = stdnse.get_script_args("apikey") +if (scriptApiKey ~= nil) then apiKey = scriptApiKey end +local outFile = stdnse.get_script_args("outfile") +local target = stdnse.get_script_args("target") + +function ts(v) + if v == nil then return "" end + return v +end + +hostrule = function() return true end + + +prerule = function () + if (outFile ~= nil) then + file = io.open(outFile, "w") io.output(file) io.write("IP, Port, Service\n") + end + + if (apiKey == "") then + print("\nError: Please specify your ShodanAPI key with --script-args='apikey=', or set it in the .nse file. You can get a free key from https://developer.shodan.io\n") + end + if (target ~= nil) then + print("Scanning single host ".. target) + return true + end +end + +postrule = function () + nmap.registry.count = (nmap.registry.count or 0) + print("+ Shodan done: " .. nmap.registry.count .. " hosts up.") + if (outFile ~= nil) then io.close() print ("+ Wrote Shodan output to '" .. outFile .. "'\n") end +end + +action = function(host) + if (apiKey == "") then return nil end + + if (target == nil) then target=host.ip end + + local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. apiKey) + if (response.status == 401) then + return "Received 'Unauthorized' from Shodan API. Double check your API key." + elseif (response.status == 404) then + return "No information for IP " .. target + elseif (response.status ~= 200) then + return "Bad response from Shodan for IP " .. target .. " : " .. response.status + end + + local stat, resp = json.parse(response.body) + if (resp.error ~= nil) then + return resp.error + end + + if (resp.data ~= nil) then + nmap.registry.count = (nmap.registry.count or 0) + 1 + hostnames = "" + for k, h in pairs(resp.hostnames) + do + hostnames = h .. " " .. hostnames + end + local result = "Report for " .. target + if (string.len(hostnames) > 0) + then + result = result .. " (" .. hostnames .. ")" + end + result = result .. "\n\nPORT\t\tSTATE\tSERVICE\tVERSION\n" + for key,e in ipairs(resp.data) + do + result = result .. ts(e.port) .. "/" .. ts(e.transport) .. "\topen" .. ts(e.service) .. "\t" .. ts(e.product) .. "\t" .. ts(e.version) .. "\n" + if (outFile ~= nil) then + out = target .. ", " .. ts(e.port) .. ", " .. ts(e.service) .. " " .. ts(e.product) .. "\n" + io.write(out) + end + end + return result + else + return "Unable to query data for IP " .. target + end +end diff --git a/scripts/nmap/vulners.nse b/scripts/nmap/vulners.nse new file mode 100755 index 00000000..284b2be9 --- /dev/null +++ b/scripts/nmap/vulners.nse @@ -0,0 +1,220 @@ +description = [[ +For each available CPE the script prints out known vulns (links to the correspondent info) and correspondent CVSS scores. + +Its work is pretty simple: +- work only when some software version is identified for an open port +- take all the known CPEs for that software (from the standard nmap -sV output) +- make a request to a remote server (vulners.com API) to learn whether any known vulns exist for that CPE + - if no info is found this way - try to get it using the software name alone +- print the obtained info out + +NB: +Since the size of the DB with all the vulns is more than 250GB there is no way to use a local db. +So we do make requests to a remote service. Still all the requests contain just two fields - the +software name and its version (or CPE), so one can still have the desired privacy. +]] + +--- +-- @usage +-- nmap -sV --script vulners [--script-args mincvss=] +-- +-- @output +-- +-- 53/tcp open domain ISC BIND DNS +-- | vulners: +-- | ISC BIND DNS: +-- | CVE-2012-1667 8.5 https://vulners.com/cve/CVE-2012-1667 +-- | CVE-2002-0651 7.5 https://vulners.com/cve/CVE-2002-0651 +-- | CVE-2002-0029 7.5 https://vulners.com/cve/CVE-2002-0029 +-- | CVE-2015-5986 7.1 https://vulners.com/cve/CVE-2015-5986 +-- | CVE-2010-3615 5.0 https://vulners.com/cve/CVE-2010-3615 +-- | CVE-2006-0987 5.0 https://vulners.com/cve/CVE-2006-0987 +-- | CVE-2014-3214 5.0 https://vulners.com/cve/CVE-2014-3214 +-- + +author = 'gmedian AT vulners DOT com' +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"vuln", "safe", "external"} + + +local http = require "http" +local json = require "json" +local string = require "string" +local table = require "table" + +local api_version="1.2" +local mincvss=nmap.registry.args.mincvss and tonumber(nmap.registry.args.mincvss) or 0.0 + + +portrule = function(host, port) + local vers=port.version + return vers ~= nil and vers.version ~= nil +end + + +--- +-- Return a string with all the found cve's and correspondent links +-- +-- @param vulns a table with the parsed json response from the vulners server +-- +function make_links(vulns) + local output_str="" + local is_exploit=false + local cvss_score="" + + -- NOTE[gmedian]: data.search is a "list" already, so just use table.sort with a custom compare function + -- However, for the future it might be wiser to create a copy rather than do it in-place + + local vulns_result = {} + for _, v in ipairs(vulns.data.search) do + table.insert(vulns_result, v) + end + + -- Sort the acquired vulns by the CVSS score + table.sort(vulns_result, function(a, b) + return a._source.cvss.score > b._source.cvss.score + end + ) + + for _, vuln in ipairs(vulns_result) do + -- Mark the exploits out + is_exploit = vuln._source.bulletinFamily:lower() == "exploit" + + -- Sometimes it might happen, so check the score availability + cvss_score = vuln._source.cvss and (type(vuln._source.cvss.score) == "number") and (vuln._source.cvss.score) or "" + + -- NOTE[gmedian]: exploits seem to have cvss == 0, so print them anyway + if is_exploit or (cvss_score ~= "" and mincvss <= tonumber(cvss_score)) then + output_str = string.format("%s\n\t%s", output_str, vuln._source.id .. "\t\t" .. cvss_score .. '\t\thttps://vulners.com/' .. vuln._source.type .. '/' .. vuln._source.id .. (is_exploit and '\t\t*EXPLOIT*' or '')) + end + end + + return output_str +end + + +--- +-- Issues the requests, receives json and parses it, calls make_links when successfull +-- +-- @param what string, future value for the software query argument +-- @param vers string, the version query argument +-- @param type string, the type query argument +-- +function get_results(what, vers, type) + local v_host="vulners.com" + local v_port=443 + local response, path + local status, error + local vulns + local option={header={}} + + option['header']['User-Agent'] = string.format('Vulners NMAP Plugin %s', api_version) + + path = '/api/v3/burp/software/' .. '?software=' .. what .. '&version=' .. vers .. '&type=' .. type + + response = http.get(v_host, v_port, path, option) + + status = response.status + if status == nil then + -- Something went really wrong out there + -- According to the NSE way we will die silently rather than spam user with error messages + return "" + elseif status ~= 200 then + -- Again just die silently + return "" + end + + status, vulns = json.parse(response.body) + + if status == true then + if vulns.result == "OK" then + return make_links(vulns) + end + end + + return "" +end + + +--- +-- Calls get_results for type="software" +-- +-- It is called from action when nothing is found for the available cpe's +-- +-- @param software string, the software name +-- @param version string, the software version +-- +function get_vulns_by_software(software, version) + return get_results(software, version, "software") +end + + +--- +-- Calls get_results for type="cpe" +-- +-- Takes the version number from the given cpe and tries to get the result. +-- If none found, changes the given cpe a bit in order to possibly separate version number from the patch version +-- And makes another attempt. +-- Having failed returns an empty string. +-- +-- @param cpe string, the given cpe +-- +function get_vulns_by_cpe(cpe) + local vers + local vers_regexp=":([%d%.%-%_]+)([^:]*)$" + local output_str="" + + -- TODO[gmedian]: add check for cpe:/a as we might be interested in software rather than in OS (cpe:/o) and hardware (cpe:/h) + -- TODO[gmedian]: work not with the LAST part but simply with the THIRD one (according to cpe doc it must be version) + + -- NOTE[gmedian]: take only the numeric part of the version + _, _, vers = cpe:find(vers_regexp) + + + if not vers then + return "" + end + + output_str = get_results(cpe, vers, "cpe") + + if output_str == "" then + local new_cpe + + new_cpe = cpe:gsub(vers_regexp, ":%1:%2") + output_str = get_results(new_cpe, vers, "cpe") + end + + return output_str +end + + +action = function(host, port) + local tab={} + local changed=false + local response + local output_str="" + + for i, cpe in ipairs(port.version.cpe) do + output_str = get_vulns_by_cpe(cpe, port.version) + if output_str ~= "" then + tab[cpe] = output_str + changed = true + end + end + + -- NOTE[gmedian]: issue request for type=software, but only when nothing is found so far + if not changed then + local vendor_version = port.version.product .. " " .. port.version.version + output_str = get_vulns_by_software(port.version.product, port.version.version) + if output_str ~= "" then + tab[vendor_version] = output_str + changed = true + end + end + + if (not changed) then + return + end + return tab +end + diff --git a/scripts/python/__init__.py b/scripts/python/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/scripts/python/dummy.py b/scripts/python/dummy.py new file mode 100755 index 00000000..3548452c --- /dev/null +++ b/scripts/python/dummy.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 + +import sys +print('Dummy!') +sys.exit(1) diff --git a/scripts/python/macvendors.py b/scripts/python/macvendors.py new file mode 100755 index 00000000..f63a074f --- /dev/null +++ b/scripts/python/macvendors.py @@ -0,0 +1,30 @@ +import requests +import json + +class macvendorsScript(): + def __init__(self): + self.dbHost = None + self.session = None + + def setDbHost(self, dbHost): + self.dbHost = dbHost + + def setSession(self, session): + self.session = session + + def run(self): + print('Running MacVendors class') + url = "https://api.macvendors.com/" + str(self.dbHost.macaddr) + if self.dbHost: + r = requests.get(url) + result = str(r.text) + if type(result) == str: + if result: + if type(result) != str or "error" in result: + result = "unknown" + self.dbHost.vendor = result + print('The vendor is: ' + result) + self.session.add(self.dbHost) + +if __name__ == "__main__": + pass diff --git a/scripts/python/pyShodan.py b/scripts/python/pyShodan.py new file mode 100755 index 00000000..abe147d6 --- /dev/null +++ b/scripts/python/pyShodan.py @@ -0,0 +1,32 @@ +from pyShodan import PyShodan + +class PyShodanScript(): + def __init__(self): + self.dbHost = None + self.session = None + + def setDbHost(self, dbHost): + self.dbHost = dbHost + + def setSession(self, session): + self.session = session + + def run(self): + print('Running PyShodan Class') + if self.dbHost: + pyShodanObj = PyShodan() + pyShodanObj.apiKey = "SNYEkE0gdwNu9BRURVDjWPXePCquXqht" + pyShodanObj.createSession() + pyShodanResults = pyShodanObj.searchIp(self.dbHost.ipv4, allData = True) + if type(pyShodanResults) == type(dict()): + if pyShodanResults: + self.dbHost.latitude = pyShodanResults.get('latitude', 'unknown') + self.dbHost.longitude = pyShodanResults.get('longitude', 'unknown') + self.dbHost.asn = pyShodanResults.get('asn', 'unknown') + self.dbHost.isp = pyShodanResults.get('isp', 'unknown') + self.dbHost.city = pyShodanResults.get('city', 'unknown') + self.dbHost.countryCode = pyShodanResults.get('country_code', 'unknown') + self.session.add(self.dbHost) + +if __name__ == "__main__": + pass diff --git a/scripts/x11screenshot.sh b/scripts/x11screenshot.sh new file mode 100755 index 00000000..2444703f --- /dev/null +++ b/scripts/x11screenshot.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# X11screenshot- This script will take a screenshot over X11, save it to an output folder and open it +# SECFORCE - Antonio Quina + +if [ $# -eq 0 ] + then + echo "Usage: $0 " + echo "eg: $0 10.10.10.10 0 /outputfolder" + exit + else + IP="$1" +fi + +if [ "$2" == "" ] + then + DSP="0" + else + DSP="$2" +fi + +if [ "$3" == "" ] + then + OUTFOLDER="/tmp" + else + OUTFOLDER="$3" + if [ ! -d "$OUTFOLDER" ] + then + mkdir $OUTFOLDER + fi +fi + +echo "xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd" +xwd -root -screen -silent -display $IP:$DSP > $OUTFOLDER/x11screenshot-$IP.xwd + +echo "convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg" +convert $OUTFOLDER/x11screenshot-$IP.xwd $OUTFOLDER/x11screenshot-$IP.jpg + +if [ -f "$OUTFOLDER/x11screenshot-$IP.jpg" ] +then + echo "xdg-open $OUTFOLDER/x11screenshot-$IP.jpg" + xdg-open $OUTFOLDER/x11screenshot-$IP.jpg +fi diff --git a/startLegion.sh b/startLegion.sh new file mode 100755 index 00000000..22d1d804 --- /dev/null +++ b/startLegion.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +echo "Strap yourself in, we're starting Legion..." + +# Set everything we might need as executable +chmod a+x -R ./deps/* +chmod a+x -R ./scripts/* + +# Determine OS, version and if WSL +source ./deps/detectOs.sh + +# Determine and set the Python and Pip paths +source ./deps/detectPython.sh + +# Figure if fist run or recloned and install deps +if [ ! -f ".initialized" ] | [ -f ".justcloned" ] +then + echo "First run here (or you did a pull to update). Let's try to automatically install all the dependancies..." + if [ ! -d "tmp" ] + then + mkdir tmp + fi + + # Setup WSL bits if needed + if [ ! -z $ISWSL ] + then + echo "WSL Setup..." + bash ./deps/setupWsl.sh + fi + + # Install dependancies from package manager + echo "Installing Packages from APT..." + ./deps/installDeps.sh + + # Install python dependancies + echo "Installing Python Libraries..." + ./deps/installPythonLibs.sh + + # Patch Qt + echo "Stripping some ABIs from Qt libraries..." + ./deps/fixQt.sh + + # Determine if additional Sparta scripts are installed + bash ./deps/detectScripts.sh + + touch .initialized + rm .justcloned -f +fi + +export QT_XCB_NATIVE_PAINTING=0 +export QT_AUTO_SCREEN_SCALE_FACTOR=1.5 + +# Verify X can be reached +source ./deps/checkXserver.sh + +if [[ $1 != 'setup' ]] +then + /usr/bin/env python3 legion.py +fi diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/actions/__init__.py b/tests/app/actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/actions/updateProgress/__init__.py b/tests/app/actions/updateProgress/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/actions/updateProgress/test_UpdateProgressObservable.py b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py new file mode 100644 index 00000000..030f2df9 --- /dev/null +++ b/tests/app/actions/updateProgress/test_UpdateProgressObservable.py @@ -0,0 +1,66 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver +from app.actions.updateProgress.UpdateProgressObservable import UpdateProgressObservable + + +class MockObserver(AbstractUpdateProgressObserver): + started = False + finished = False + progress = 0 + + def onProgressUpdate(self, progress) -> None: + self.progress = progress + + def onStart(self) -> None: + self.started = True + + def onFinished(self) -> None: + self.finished = True + + +class UpdateProgressObservableTest(unittest.TestCase): + def setUp(self) -> None: + self.updateProgressObservable = UpdateProgressObservable() + self.someObserver = MockObserver() + self.anotherObserver = MockObserver() + self.updateProgressObservable.attach(self.someObserver) + self.updateProgressObservable.attach(self.anotherObserver) + + def test_start_notifiesAllObservers(self): + self.assertFalse(self.someObserver.started) + self.assertFalse(self.anotherObserver.started) + self.updateProgressObservable.start() + self.assertTrue(self.someObserver.started) + self.assertTrue(self.anotherObserver.started) + + def test_finished_notifiesAllObservers(self): + self.assertFalse(self.someObserver.finished) + self.assertFalse(self.anotherObserver.finished) + self.updateProgressObservable.finished() + self.assertTrue(self.someObserver.finished) + self.assertTrue(self.anotherObserver.finished) + + def test_updateProgress_notifiesAllObservers(self): + self.assertEqual(0, self.someObserver.progress) + self.assertEqual(0, self.anotherObserver.progress) + self.updateProgressObservable.updateProgress(25) + self.assertEqual(25, self.someObserver.progress) + self.assertEqual(25, self.anotherObserver.progress) diff --git a/tests/app/http/__init__.py b/tests/app/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/http/test_isHttps.py b/tests/app/http/test_isHttps.py new file mode 100644 index 00000000..0ab6b46c --- /dev/null +++ b/tests/app/http/test_isHttps.py @@ -0,0 +1,31 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch, MagicMock + + +class isHttpsTest(unittest.TestCase): + def test_isHttps_GivenAnIpAddrAndPortThatIsHttps_ReturnsTrue(self): + with patch("urllib.request.urlopen") as urlopen, patch("urllib.request.Request") as Request: + expectedUserAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0 Iceweasel/22.0' + from app.http.isHttps import isHttps + mockOpenedUrl = MagicMock() + Request.return_value = MagicMock() + urlopen.return_value.read.return_value = mockOpenedUrl + self.assertTrue(isHttps("some-ip", "8080")) + Request.assert_called_with("https://some-ip:8080", headers={"User-Agent": expectedUserAgent}) diff --git a/tests/app/shell/__init__.py b/tests/app/shell/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/shell/test_DefaultShell.py b/tests/app/shell/test_DefaultShell.py new file mode 100644 index 00000000..5e6a5a1a --- /dev/null +++ b/tests/app/shell/test_DefaultShell.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +import os.path + + +class DefaultShellTest(unittest.TestCase): + def setUp(self) -> None: + from app.shell.DefaultShell import DefaultShell + self.shell = DefaultShell() + + def test_isDirectory_whenProvidedADirectory_ReturnsTrue(self): + os.path.isdir = lambda name: True + self.assertTrue(self.shell.isDirectory("some-directory")) + + def test_isDirectory_whenProvidedAFile_ReturnsFalse(self): + os.path.isdir = lambda name: False + self.assertFalse(self.shell.isDirectory("some-file")) + + def test_isFile_whenProvidedAFile_ReturnsTrue(self): + os.path.isfile = lambda name: True + self.assertTrue(self.shell.isFile("some-file")) + + def test_isFile_whenProvidedAFile_ReturnsFalse(self): + os.path.isfile = lambda name: False + self.assertFalse(self.shell.isFile("some-directory")) diff --git a/tests/app/test_ModelHelpers.py b/tests/app/test_ModelHelpers.py new file mode 100644 index 00000000..e5513d81 --- /dev/null +++ b/tests/app/test_ModelHelpers.py @@ -0,0 +1,49 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2019 Hackman238 + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from PyQt6 import QtCore + +from app.ModelHelpers import resolveHeaders, itemInteractive, itemSelectable + + +class ModelHelpersTest(unittest.TestCase): + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsWithinBound_ReturnsHeaders(self): + expectedHeaders = ["header1", "header2", "header3"] + headers = [[], expectedHeaders, []] + actualHeaders = resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Horizontal, 1, headers) + self.assertEqual(expectedHeaders, actualHeaders) + + def test_resolveHeaders_WhenRoleIsNotDisplay_ReturnsNone(self): + self.assertIsNone(resolveHeaders(QtCore.Qt.ItemDataRole.BackgroundRole, QtCore.Qt.Orientation.Horizontal, 1, [])) + + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsNotHz_ReturnsNone(self): + self.assertIsNone(resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Vertical, 1, [])) + + def test_resolveHeaders_WhenRoleIsDisplayAndOrientationIsHzAndSectionIsOutOfBound_ReturnsStringMessage(self): + expectedMessage = "not implemented in view model" + actualMessage = resolveHeaders(QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.Orientation.Horizontal, 100, []) + self.assertEqual(expectedMessage, actualMessage) + + def test_itemInteractive_ReturnsItemFlagForEnabledSelectableEditableItem(self): + expectedFlags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable + self.assertEqual(expectedFlags, itemInteractive()) + + def test_itemSelectable_ReturnItemFlagForEnabledSelectableItem(self): + expectedFlags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable + self.assertEqual(expectedFlags, itemSelectable()) diff --git a/tests/app/test_ProjectManager.py b/tests/app/test_ProjectManager.py new file mode 100644 index 00000000..9400359e --- /dev/null +++ b/tests/app/test_ProjectManager.py @@ -0,0 +1,136 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + +from app.Project import Project + + +class ProjectManagerTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + @patch('db.repositories.HostRepository') + @patch('app.auxiliary.Wordlist') + @patch('db.SqliteDbAdapter.Database') + def setUp(self, getLogger, hostRepository, wordlist, database) -> None: + from app.ProjectManager import ProjectManager + self.mockShell = MagicMock() + self.mockRepositoryFactory = MagicMock() + self.project = MagicMock() + self.mockDatabase = database + self.projectManager = ProjectManager(self.mockShell, self.mockRepositoryFactory, getLogger) + + def test_createNewProject_WhenProvidedProjectDetails_ReturnsANewProject(self): + projectType = "legion" + isTemporary = True + self.mockDatabase.name = "newTemporaryProject" + self.mockShell.get_current_working_directory.return_value = "workingDir/" + self.mockShell.create_temporary_directory.side_effect = ["/outputFolder", "/runningFolder"] + + project = self.projectManager.createNewProject(projectType, isTemporary) + self.mockShell.create_named_temporary_file.assert_called_once_with( + suffix=".legion", prefix="legion-", directory="./tmp/", delete_on_close=False + ) + self.mockShell.create_temporary_directory.assert_has_calls([ + mock.call(prefix="legion-", suffix="-tool-output", directory="./tmp/"), + mock.call(prefix="legion-", suffix="-running", directory="./tmp/"), + ]) + self.mockShell.create_directory_recursively.assert_has_calls([ + mock.call("/outputFolder/screenshots"), + mock.call("/runningFolder/nmap"), + mock.call("/runningFolder/hydra"), + mock.call("/runningFolder/dnsmap"), + ]) + self.assertIsNotNone(project.properties.projectName) + self.assertEqual(project.properties.projectType, "legion") + self.assertEqual(project.properties.workingDirectory, "workingDir/") + self.assertEqual(project.properties.isTemporary, True) + self.assertEqual(project.properties.outputFolder, "/outputFolder") + self.assertEqual(project.properties.runningFolder, "/runningFolder") + self.assertEqual(project.properties.storeWordListsOnExit, True) + self.mockRepositoryFactory.buildRepositories.assert_called_once_with(mock.ANY) + + def test_closeProject_WhenProvidedAnOpenTemporaryProject_ClosesTheProject(self): + self.project.properties.isTemporary = True + self.project.properties.storeWordListsOnExit = True + self.project.properties.projectName = "project-name" + self.project.properties.runningFolder = "./running/folder" + self.project.properties.outputFolder = "./output/folder" + + self.projectManager.closeProject(self.project) + self.mockShell.remove_file.assert_called_once_with("project-name") + self.mockShell.remove_directory.assert_has_calls([mock.call("./output/folder"), mock.call("./running/folder")]) + + def test_closeProject_WhenProvidedAnOpenNonTemporaryProject_ClosesTheProject(self): + self.project.properties.isTemporary = False + self.project.properties.storeWordListsOnExit = False + self.project.properties.runningFolder = "./running/folder" + self.project.properties.usernamesWordList = MagicMock() + self.project.properties.usernamesWordList.filename = "UsernamesList.txt" + self.project.properties.passwordWordList = MagicMock() + self.project.properties.passwordWordList.filename = "PasswordsList.txt" + + self.projectManager.closeProject(self.project) + self.mockShell.remove_file.assert_has_calls([mock.call("UsernamesList.txt"), mock.call("PasswordsList.txt")]) + self.mockShell.remove_directory.assert_called_once_with("./running/folder") + + def test_openExistingProject_WhenProvidedProjectNameAndType_OpensAnExistingProjectSuccessfully(self): + from app.Project import Project + + projectName = "some-existing-project" + self.mockShell.create_temporary_directory.return_value = "/running/folder" + openedExistingProject: Project = self.projectManager.openExistingProject(projectName, "legion") + self.assertFalse(openedExistingProject.properties.isTemporary) + self.assertEqual(openedExistingProject.properties.projectName, "some-existing-project") + self.assertEqual(openedExistingProject.properties.workingDirectory, "/") + self.assertEqual(openedExistingProject.properties.projectType, "legion") + self.assertFalse(openedExistingProject.properties.isTemporary) + self.assertEqual(openedExistingProject.properties.outputFolder, "some-existing-project-tool-output") + self.assertEqual(openedExistingProject.properties.runningFolder, "/running/folder") + self.assertIsNotNone(openedExistingProject.properties.usernamesWordList) + self.assertIsNotNone(openedExistingProject.properties.passwordWordList) + self.assertTrue(openedExistingProject.properties.storeWordListsOnExit) + self.mockShell.create_temporary_directory.assert_called_once_with(suffix="-running", prefix="legion-", + directory="./tmp/") + self.mockRepositoryFactory.buildRepositories.assert_called_once() + + @patch('os.system') + def test_saveProjectAs_WhenProvidedAnActiveTemporaryProjectAndASaveFileName_SavesProjectSuccessfully(self, + osSystem): + expectedFileName = "my-test-project" + self.project.properties.projectName = "some-running-temporary-project" + self.project.properties.outputFolder = "some-temporary-output-folder" + self.project.properties.isTemporary = True + self.mockShell.directoryOrFileExists.return_value = False + + savedProject: Project = self.projectManager.saveProjectAs(self.project, expectedFileName, replace=1, + projectType="legion") + self.mockShell.copy.assert_called_once_with(source="some-running-temporary-project", + destination="my-test-project.legion") + osSystem.assert_called_once() + self.mockShell.remove_file.assert_called_once_with("some-running-temporary-project") + self.mockShell.remove_directory.assert_called_once_with("some-temporary-output-folder") + self.assertEqual(savedProject.properties.projectName, "my-test-project.legion") + self.assertEqual(savedProject.properties.projectType, "legion") + + @patch('os.system') + def test_saveProjectAs_WhenReplaceFlagIsFalse_DoesNotSaveProject(self, osSystem): + self.projectManager.saveProjectAs(self.project, "some-project-that-cannot-be-replaced", replace=0, + projectType="legion") + self.mockShell.copy.assert_not_called() + osSystem.assert_not_called() diff --git a/tests/app/test_Timing.py b/tests/app/test_Timing.py new file mode 100644 index 00000000..a68b6a81 --- /dev/null +++ b/tests/app/test_Timing.py @@ -0,0 +1,37 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from datetime import datetime +from time import time +from unittest.mock import patch + +from app.timing import getTimestamp + + +class TimingTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def test_getTimestamp_WhenInvokedWithNoParameters_ReturnsStandardFormattedTimestamp(self, getLogger): + expectedStandardTimestampFormat = "%Y%m%d%H%M%S%f" + currentTime = datetime.fromtimestamp(time()) + self.assertEqual(getTimestamp()[:-3], currentTime.strftime(expectedStandardTimestampFormat)[:-3]) + + @patch('utilities.stenoLogging.get_logger') + def test_getTimestamp_WhenInvokedWithHumanParameter_ReturnsHumanFormattedTimestamp(self, getLogger): + expectedHumanTimestampFormat = "%d %b %Y %H:%M:%S.%f" + currentTime = datetime.fromtimestamp(time()) + self.assertEqual(getTimestamp(human=True)[:-3], currentTime.strftime(expectedHumanTimestampFormat)[:-3]) diff --git a/tests/app/tools/__init__.py b/tests/app/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/tools/nmap/__init__.py b/tests/app/tools/nmap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app/tools/nmap/test_DefaultNmapExporter.py b/tests/app/tools/nmap/test_DefaultNmapExporter.py new file mode 100644 index 00000000..48ea0453 --- /dev/null +++ b/tests/app/tools/nmap/test_DefaultNmapExporter.py @@ -0,0 +1,44 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + + +class DefaultNmapExporterTest(unittest.TestCase): + def setUp(self) -> None: + from app.tools.nmap.DefaultNmapExporter import DefaultNmapExporter + self.log = MagicMock() + self.mockShell = MagicMock() + self.nmapExporter = DefaultNmapExporter(self.mockShell, self.log) + + @patch("subprocess.Popen") + def test_exportOutputToHtml_WhenProvidedFileNameAndOutputFolder_ExportsOutputSuccessfully(self, processOpen): + exportCommand = f"xsltproc -o some-file.html nmap.xsl some-file.xml" + self.nmapExporter.exportOutputToHtml("some-file", "some-folder/") + processOpen.assert_called_once_with(exportCommand, shell=True) + self.mockShell.move.assert_called_once_with("some-file.html", "some-folder/") + + @patch("subprocess.Popen") + def test_exportOutputToHtml_WhenExportFailsDueToProcessError_DoesNotMoveAnyFilesToOutputFolder(self, processOpen): + exportCommand = f"xsltproc -o some-bad-file.html nmap.xsl some-bad-file.xml" + processOpen.side_effect = Exception("something went wrong") + self.nmapExporter.exportOutputToHtml("some-bad-file", "some-folder/") + processOpen.assert_called_once_with(exportCommand, shell=True) + self.mockShell.move.assert_not_called() + self.log.error.assert_called() + diff --git a/tests/app/tools/nmap/test_NmapHelpers.py b/tests/app/tools/nmap/test_NmapHelpers.py new file mode 100644 index 00000000..8c5f1584 --- /dev/null +++ b/tests/app/tools/nmap/test_NmapHelpers.py @@ -0,0 +1,42 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock + +from app.tools.nmap.NmapHelpers import nmapFileExists + + +class NmapHelpersTest(unittest.TestCase): + def setUp(self) -> None: + self.mockShell = MagicMock() + + def test_nmapFileExists_WhenNecessaryNmapFilesExistOnFilesystem_ReturnsTrue(self): + def makeAllNecessaryNmapFilesExist(): + self.mockShell.directoryOrFileExists.side_effect = [True, True, True] + self.mockShell.isFile.side_effect = [True, True, True] + + makeAllNecessaryNmapFilesExist() + self.assertTrue(nmapFileExists(self.mockShell, "some-nmap-session")) + + def test_nmapFileExists_WhenAtLeastOneNmapFileDoesNotExist_ReturnsFalse(self): + def makeAtLeastOneNmapFileNotPresent(): + self.mockShell.directoryOrFileExists.side_effect = [True, True, True] + self.mockShell.isFile.side_effect = [True, True, False] + + makeAtLeastOneNmapFileNotPresent() + self.assertFalse(nmapFileExists(self.mockShell, "some-nmap-session")) diff --git a/tests/app/tools/nmap/test_NmapPaths.py b/tests/app/tools/nmap/test_NmapPaths.py new file mode 100644 index 00000000..a86c727d --- /dev/null +++ b/tests/app/tools/nmap/test_NmapPaths.py @@ -0,0 +1,11 @@ +import unittest + +from app.tools.nmap.NmapPaths import getNmapRunningFolder, getNmapOutputFolder + + +class NmapPathsTest(unittest.TestCase): + def test_getNmapRunningFolder_ReturnsProperNmapPathWithinAnActiveProject(self): + self.assertEqual(getNmapRunningFolder("some-folder"), "some-folder/nmap") + + def test_getNmapOutputFolder_ReturnsProperNmapPathWithinAnActiveProject(self): + self.assertEqual(getNmapOutputFolder("some-folder"), "some-folder/nmap") diff --git a/tests/app/tools/test_ToolCoordinator.py b/tests/app/tools/test_ToolCoordinator.py new file mode 100644 index 00000000..24d999ef --- /dev/null +++ b/tests/app/tools/test_ToolCoordinator.py @@ -0,0 +1,82 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + + +class ToolCoordinatorTest(unittest.TestCase): + @patch("app.tools.nmap.NmapHelpers.nmapFileExists") + def setUp(self, nmapFileExists) -> None: + from app.tools.ToolCoordinator import ToolCoordinator + self.mockShell = MagicMock() + self.mockNmapExporter = MagicMock() + self.nmapFileExists = nmapFileExists + self.outputFolder = "some-output-folder" + self.toolCoordinator = ToolCoordinator(self.mockShell, self.mockNmapExporter) + + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndNmapFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename): + fileName = "some-output-nmap-file" + + basename.return_value = "nmap" + self.mockShell.directoryOrFileExists.side_effect = [True, False, True, True, True] + self.nmapFileExists.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockNmapExporter.exportOutputToHtml.assert_called_once_with("some-output-nmap-file", + "some-output-folder/nmap") + self.mockShell.move.assert_has_calls([ + mock.call("some-output-nmap-file.xml", "some-output-folder/nmap"), + mock.call("some-output-nmap-file.nmap", "some-output-folder/nmap"), + mock.call("some-output-nmap-file.gnmap", "some-output-folder/nmap"), + ]) + + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputDirAndGenericFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename): + fileName = "some-output-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-file", "some-output-folder/some-tool") + + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndXmlFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename): + fileName = "some-output-xml-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, False, False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-xml-file.xml", "some-output-folder/some-tool") + + @patch("ntpath.basename") + def test_saveToolOutput_WhenGivenProjectOutputFolderAndTxtFileNameToSaveOutputIn_SavesOutputSuccessfully(self, + basename): + fileName = "some-output-txt-file" + basename.return_value = "some-tool" + self.mockShell.directoryOrFileExists.side_effect = [False, False, False, False, True] + self.mockShell.isFile.return_value = True + + self.toolCoordinator.saveToolOutput(self.outputFolder, fileName) + self.mockShell.move.assert_called_once_with("some-output-txt-file.txt", "some-output-folder/some-tool") diff --git a/tests/db/__init__.py b/tests/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/db/helpers/db_helpers.py b/tests/db/helpers/db_helpers.py new file mode 100644 index 00000000..66f93be3 --- /dev/null +++ b/tests/db/helpers/db_helpers.py @@ -0,0 +1,31 @@ +from unittest.mock import MagicMock + + +def mockExecuteFetchAll(return_value): + mock_db_execute = MagicMock() + mock_db_execute.fetchall.return_value = return_value + return mock_db_execute + + +def mockExecuteAll(return_value): + mock_db_execute = MagicMock() + mock_db_execute.all.return_value = return_value + return mock_db_execute + + +def mockFirstBySideEffect(return_value): + mock_filter_by = MagicMock() + mock_filter_by.first.side_effect = return_value + return mock_filter_by + + +def mockFirstByReturnValue(return_value): + mock_filter_by = MagicMock() + mock_filter_by.first.return_value = return_value + return mock_filter_by + + +def mockQueryWithFilterBy(return_value): + mock_query = MagicMock() + mock_query.filter_by.return_value = return_value + return mock_query diff --git a/tests/db/repositories/__init__.py b/tests/db/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/db/repositories/test_CVERepository.py b/tests/db/repositories/test_CVERepository.py new file mode 100644 index 00000000..b1f1bcf9 --- /dev/null +++ b/tests/db/repositories/test_CVERepository.py @@ -0,0 +1,38 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch, MagicMock + +from tests.db.helpers.db_helpers import mockExecuteFetchAll + + +class CVERepositoryTest(unittest.TestCase): + def setUp(self) -> None: + self.mock_db_adapter = MagicMock() + + def test_getCVEsByHostIP_WhenProvidedAHostIp_ReturnsCVEs(self): + from db.repositories.CVERepository import CVERepository + self.mock_db_adapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['cve1'], ['cve2']]) + expected_query = ("SELECT cves.name, cves.severity, cves.product, cves.version, cves.url, cves.source, " + "cves.exploitId, cves.exploit, cves.exploitUrl FROM cve AS cves " + "INNER JOIN hostObj AS hosts ON hosts.id = cves.hostId " + "WHERE hosts.ip = ?") + cveRepository = CVERepository(self.mock_db_adapter) + result = cveRepository.getCVEsByHostIP("some_host") + self.assertEqual([['cve1'], ['cve2']], result) + self.mock_db_adapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host") diff --git a/tests/db/repositories/test_HostRepository.py b/tests/db/repositories/test_HostRepository.py new file mode 100644 index 00000000..19f10387 --- /dev/null +++ b/tests/db/repositories/test_HostRepository.py @@ -0,0 +1,139 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockQueryWithFilterBy, mockFirstByReturnValue + +existsQuery = 'SELECT host.ip FROM hostObj AS host WHERE host.ip == ? OR host.hostname == ?' + + +def expectedGetHostsAndPortsQuery(with_filter: str = "") -> str: + query = ( + "SELECT hosts.ip,ports.portId,ports.protocol,ports.state,ports.hostId,ports.serviceId,services.name," + "services.product,services.version,services.extrainfo,services.fingerprint FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id=ports.serviceId " + "WHERE services.name=?") + query += with_filter + return query + + +class HostRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.HostRepository import HostRepository + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.mockProcess = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.hostRepository = HostRepository(self.mockDbAdapter) + + def getHostsAndPortsTestCase(self, filters, service_name, expectedQuery): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [{'name': 'service_name1'}, {'name': 'service_name2'}]) + service_names = self.hostRepository.getHostsAndPortsByServiceName(service_name, filters) + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, service_name) + self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) + + def test_exists_WhenProvidedAExistingHosts_ReturnsTrue(self): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['some-ip']]) + self.assertTrue(self.hostRepository.exists("some_host")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(existsQuery, "some_host", "some_host") + + def test_exists_WhenProvidedANonExistingHosts_ReturnsFalse(self): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([]) + + self.assertFalse(self.hostRepository.exists("some_host")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(existsQuery, "some_host", "some_host") + + def test_getHosts_InvokedWithNoFilters_ReturnsHosts(self): + from app.auxiliary import Filters + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['host1'], ['host2']]) + expectedQuery = "SELECT * FROM hostObj AS hosts WHERE 1=1" + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = self.hostRepository.getHosts(filters) + self.assertEqual([['host1'], ['host2']], result) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) + + def test_getHosts_InvokedWithAFewFilters_ReturnsFilteredHosts(self): + from app.auxiliary import Filters + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['host1'], ['host2']]) + expectedQuery = ("SELECT * FROM hostObj AS hosts WHERE 1=1" + " AND hosts.status != 'down' AND hosts.checked != 'True'") + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=False, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = self.hostRepository.getHosts(filters) + self.assertEqual([['host1'], ['host2']], result) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) + + def test_getHostInfo_WhenProvidedHostIpAddress_FetchesHostInformation(self): + from db.entities.host import hostObj + + expected_host_info: hostObj = MagicMock() + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(expected_host_info)) + + actual_host_info = self.hostRepository.getHostInformation("127.0.0.1") + self.assertEqual(actual_host_info, expected_host_info) + + def test_getHostsAndPorts_InvokedWithNoFilters_FetchesHostsAndPortsMatchingKeywords(self): + from app.auxiliary import Filters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + expectedQuery = expectedGetHostsAndPortsQuery() + self.getHostsAndPortsTestCase(filters=filters, + service_name="some_service_name", expectedQuery=expectedQuery) + + def test_getHostsAndPorts_InvokedWithFewFilters_FetchesHostsAndPortsWithFiltersApplied(self): + from app.auxiliary import Filters + + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + expectedQuery = expectedGetHostsAndPortsQuery( + with_filter=" AND hosts.status != 'down' AND ports.protocol != 'udp'") + self.getHostsAndPortsTestCase(filters=filters, + service_name="some_service_name", expectedQuery=expectedQuery) + + def test_deleteHost_InvokedWithAHostId_DeletesProcess(self): + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + + self.hostRepository.deleteHost("some-host-id") + self.mockDbSession.delete.assert_called_once_with(self.mockProcess) + self.mockDbSession.commit.assert_called_once() + + def test_toggleHostCheckStatus_WhenHostIsSetToTrue_TogglesToFalse(self): + self.mockProcess.checked = 'True' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.hostRepository.toggleHostCheckStatus("some-ip-address") + self.assertEqual('False', self.mockProcess.checked) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def test_toggleHostCheckStatus_WhenHostIsSetToFalse_TogglesToTrue(self): + self.mockProcess.checked = 'False' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.hostRepository.toggleHostCheckStatus("some-ip-address") + self.assertEqual('True', self.mockProcess.checked) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_NoteRepository.py b/tests/db/repositories/test_NoteRepository.py new file mode 100644 index 00000000..388302a3 --- /dev/null +++ b/tests/db/repositories/test_NoteRepository.py @@ -0,0 +1,53 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from db.repositories.NoteRepository import NoteRepository +from tests.db.helpers.db_helpers import mockQueryWithFilterBy, mockFirstByReturnValue + + +class NoteRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.entities.note import note + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.someNote: note = MagicMock() + self.mockLog = MagicMock() + self.noteRepository: NoteRepository = NoteRepository(self.mockDbAdapter, self.mockLog) + + def test_getNoteByHostId_WhenProvidedHostId_ReturnsNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue("some-note")) + + note = self.noteRepository.getNoteByHostId("some-host-id") + self.assertEqual("some-note", note) + + def test_storeNotes_WhenProvidedHostIdAndNoteAndNoteAlreadyExists_UpdatesNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.someNote)) + self.noteRepository.storeNotes("some-host-id", "some-note") + self.mockDbSession.add.assert_called_once_with(self.someNote) + self.mockDbAdapter.commit.assert_called_once() + + def test_storeNotes_WhenProvidedHostIdAndNoteAndNoteDoesNotExist_SavesNewNote(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(None)) + self.noteRepository.storeNotes("some-host-id", "some-note") + self.mockDbSession.add.assert_called_once() + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_PortRepository.py b/tests/db/repositories/test_PortRepository.py new file mode 100644 index 00000000..550bea8f --- /dev/null +++ b/tests/db/repositories/test_PortRepository.py @@ -0,0 +1,108 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2018-2019 Hackman238 + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import patch, MagicMock + +from tests.db.helpers.db_helpers import mockFirstByReturnValue, mockExecuteFetchAll + + +class PortRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.PortRepository import PortRepository + self.mockDbAdapter = MagicMock() + self.mockDbSession = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.repository = PortRepository(self.mockDbAdapter) + + def test_getPortsByIPAndProtocol_ReturnsPorts(self): + expected_query = ("SELECT ports.portId FROM portObj AS ports " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "WHERE hosts.ip = ? and ports.protocol = ?") + self.mockDbAdapter.metadata.bind.execute.return_value = mockFirstByReturnValue( + [['port-id1'], ['port-id2']]) + ports = self.repository.getPortsByIPAndProtocol("some_host_ip", "tcp") + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip", "tcp") + self.assertEqual([['port-id1'], ['port-id2']], ports) + + def test_getPortStatesByHostId_ReturnsPortsStates(self): + expected_query = 'SELECT port.state FROM portObj as port WHERE port.hostId = ?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['port-state1'], ['port-state2']]) + port_states = self.repository.getPortStatesByHostId("some_host_id") + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_id") + self.assertEqual([['port-state1'], ['port-state2']], port_states) + + def test_getPortsAndServicesByHostIP_InvokedWithNoFilters_ReturnsPortsAndServices(self): + from app.auxiliary import Filters + + expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " + "WHERE hosts.ip = ?") + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['ip1'], ['ip2']]) + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + results = self.repository.getPortsAndServicesByHostIP("some_host_ip", filters) + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.assertEqual([['ip1'], ['ip2']], results) + + def test_getPortsAndServicesByHostIP_InvokedWithFewFilters_ReturnsPortsAndServices(self): + from app.auxiliary import Filters + + expected_query = ("SELECT hosts.ip, ports.portId, ports.protocol, ports.state, ports.hostId, ports.serviceId, " + "services.name, services.product, services.version, services.extrainfo, services.fingerprint " + "FROM portObj AS ports INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "LEFT OUTER JOIN serviceObj AS services ON services.id = ports.serviceId " + "WHERE hosts.ip = ? AND ports.protocol != 'tcp' AND ports.protocol != 'udp'") + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['ip1'], ['ip2']]) + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=False, udp=False) + results = self.repository.getPortsAndServicesByHostIP("some_host_ip", filters) + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expected_query, "some_host_ip") + self.assertEqual([['ip1'], ['ip2']], results) + + def test_deleteAllPortsAndScriptsByHostId_WhenProvidedByHostIDAndProtocol_DeletesAllPortsAndScripts(self): + mockFilterHost = mockProtocolFilter = mockReturnAll = MagicMock() + mockPort1 = mockPort2 = MagicMock() + mockReturnAll.all.return_value = [mockPort1, mockPort2] + mockProtocolFilter.filter.return_value = mockReturnAll + mockFilterHost.filter.return_value = mockProtocolFilter + + mockFilterScript = mockReturnAllScripts = MagicMock() + mockReturnAllScripts.all.return_value = ['some-script1', 'some-script2'] + mockFilterScript.filter.return_value = mockReturnAllScripts + + self.mockDbSession.query.side_effect = [mockFilterHost, mockFilterScript, mockFilterScript] + + self.repository.deleteAllPortsAndScriptsByHostId("some-host-id", "some-protocol") + self.mockDbSession.delete.assert_has_calls([ + mock.call('some-script1'), mock.call('some-script2'), + mock.call('some-script1'), mock.call('some-script2'), + mock.call(mockPort1), mock.call(mockPort2) + ]) + self.mockDbSession.commit.assert_called_once() diff --git a/tests/db/repositories/test_ProcessRepository.py b/tests/db/repositories/test_ProcessRepository.py new file mode 100644 index 00000000..949849fb --- /dev/null +++ b/tests/db/repositories/test_ProcessRepository.py @@ -0,0 +1,306 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstBySideEffect, mockFirstByReturnValue, \ + mockQueryWithFilterBy + + +def build_mock_process(status: str, display: str) -> MagicMock: + process = MagicMock() + process.status = status + process.display = display + return process + + +class ProcessRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.ProcessRepository import ProcessRepository + self.mockProcess = MagicMock() + self.mockDbSession = MagicMock() + self.mockDbAdapter = MagicMock() + self.mockLogger = MagicMock() + self.mockFilters = MagicMock() + self.mockDbAdapter.session.return_value = self.mockDbSession + self.processRepository = ProcessRepository(self.mockDbAdapter, self.mockLogger) + + def test_getProcesses_WhenProvidedShowProcessesWithNoNmapFlag_ReturnsProcesses(self): + expectedQuery = ('SELECT "0", "0", "0", process.name, "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ' + 'FROM process AS process WHERE process.closed = "False" AND process.name != "nmap" ' + 'GROUP BY process.name') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, showProcesses='noNmap') + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) + + def test_getProcesses_WhenProvidedShowProcessesWithFlagFalse_ReturnsProcesses(self): + expectedQuery = ('SELECT process.id, process.hostIp, process.tabTitle, process.outputfile, output.output ' + 'FROM process AS process INNER JOIN process_output AS output ON process.id = output.processId ' + 'WHERE process.display = ? AND process.closed = "False" order by process.id desc') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, showProcesses=False) + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'False') + + def test_getProcesses_WhenProvidedShowProcessesWithNoFlag_ReturnsProcesses(self): + expectedQuery = "SELECT * FROM process AS process WHERE process.display=? order by id asc" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-process'], ['some-process2']]) + processes = self.processRepository.getProcesses(self.mockFilters, "True", sort='asc', ncol='id') + self.assertEqual(processes, [['some-process'], ['some-process2']]) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, 'True') + + def test_storeProcess_WhenProvidedAProcess_StoreProcess(self): + self.processRepository.storeProcess(self.mockProcess) + + self.mockDbSession.add.assert_called_once() + self.mockDbAdapter.commit.assert_called_once() + + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutput_StoresProcessOutput(self): + from db.entities.process import process + from db.entities.processOutput import process_output + + expected_process: process = MagicMock() + process.status = 'Running' + expected_process_output: process_output = MagicMock() + mock_query = MagicMock() + mock_query.filter_by.return_value = mockFirstBySideEffect([expected_process, expected_process_output]) + self.mockDbSession.query.return_value = mock_query + + self.processRepository.storeProcessOutput("some_process_id", "this is some cool output") + + self.mockDbSession.add.assert_has_calls([ + mock.call(expected_process_output), + mock.call(expected_process) + ]) + self.mockDbAdapter.commit.assert_called_once() + + def test_storeProcessOutput_WhenProvidedProcessIdDoesNotExist_DoesNotPerformAnyUpdate(self): + self.mockDbAdapter.session.return_value = self.mockDbSession + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(False)) + + self.processRepository.storeProcessOutput("some_non_existent_process_id", "this is some cool output") + + self.mockDbSession.add.assert_not_called() + self.mockDbAdapter.commit.assert_not_called() + + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcKilled_StoresOutputButStatusNotUpdated( + self): + self.whenProcessDoesNotFinishGracefully("Killed") + + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcCancelled_StoresOutputButStatusNotUpdated( + self): + self.whenProcessDoesNotFinishGracefully("Cancelled") + + def test_storeProcessOutput_WhenProvidedExistingProcessIdAndOutputButProcCrashed_StoresOutputButStatusNotUpdated( + self): + self.whenProcessDoesNotFinishGracefully("Crashed") + + def test_getStatusByProcessId_WhenGivenProcId_FetchesProcessStatus(self): + expectedQuery = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['Running']]) + + actual_status = self.processRepository.getStatusByProcessId("some_process_id") + + self.assertEqual(actual_status, 'Running') + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_getStatusByProcessId_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + expectedQuery = 'SELECT process.status FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll(False) + + actual_status = self.processRepository.getStatusByProcessId("some_process_id") + + self.assertEqual(actual_status, -1) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_getPIDByProcessId_WhenGivenProcId_FetchesProcessId(self): + expectedQuery = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([['1234']]) + + actual_status = self.processRepository.getPIDByProcessId("some_process_id") + + self.assertEqual(actual_status, '1234') + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_getPIDByProcessId_WhenProcIdDoesNotExist_ReturnsNegativeOne(self): + expectedQuery = 'SELECT process.pid FROM process AS process WHERE process.id=?' + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll(False) + + actual_status = self.processRepository.getPIDByProcessId("some_process_id") + + self.assertEqual(actual_status, -1) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_isKilledProcess_WhenProvidedKilledProcessId_ReturnsTrue(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Killed"]]) + + self.assertTrue(self.processRepository.isKilledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_isKilledProcess_WhenProvidedNonKilledProcessId_ReturnsFalse(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Running"]]) + + self.assertFalse(self.processRepository.isKilledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_isCancelledProcess_WhenProvidedCancelledProcessId_ReturnsTrue(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Cancelled"]]) + + self.assertTrue(self.processRepository.isCancelledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_isCancelledProcess_WhenProvidedNonCancelledProcessId_ReturnsFalse(self): + expectedQuery = "SELECT process.status FROM process AS process WHERE process.id=?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["Running"]]) + + self.assertFalse(self.processRepository.isCancelledProcess("some_process_id")) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_process_id") + + def test_storeProcessCrashStatus_WhenProvidedProcessId_StoresProcessCrashStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessCrashStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Crashed") + + def test_storeProcessCancelledStatus_WhenProvidedProcessId_StoresProcessCancelledStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessCancelStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Cancelled") + + def test_storeProcessRunningStatus_WhenProvidedProcessId_StoresProcessRunningStatus(self): + self.mockProcessStatusAndReturnSingle("Waiting") + self.processRepository.storeProcessRunningStatus("some-process-id", "3123") + self.assertProcessStatusUpdatedTo("Running") + + def test_storeProcessKillStatus_WhenProvidedProcessId_StoresProcessKillStatus(self): + self.mockProcessStatusAndReturnSingle("Running") + self.processRepository.storeProcessKillStatus("some-process-id") + self.assertProcessStatusUpdatedTo("Killed") + + def test_storeProcessRunningElapsedTime_WhenProvidedProcessId_StoresProcessRunningElapsedTime(self): + self.mockProcess.elapsed = "some-time" + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + + self.processRepository.storeProcessRunningElapsedTime("some-process-id", "another-time") + self.assertEqual("another-time", self.mockProcess.elapsed) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def test_getHostsByToolName_WhenProvidedToolNameAndClosedFalse_StoresProcessRunningElapsedTime(self): + expectedQuery = ('SELECT process.id, "0", "0", "0", "0", "0", "0", process.hostIp, process.port, ' + 'process.protocol, "0", "0", process.outputfile, "0", "0", "0" FROM process AS process ' + 'WHERE process.name=? and process.closed="False"') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["some-host1"], ["some-host2"]]) + + hosts = self.processRepository.getHostsByToolName("some-toolname", "False") + self.assertEqual([["some-host1"], ["some-host2"]], hosts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-toolname") + + def test_getHostsByToolName_WhenProvidedToolNameAndClosedAsFetchAll_StoresProcessRunningElapsedTime(self): + expectedQuery = ('SELECT "0", "0", "0", "0", "0", process.hostIp, process.port, process.protocol, "0", "0", ' + 'process.outputfile, "0", "0", "0" FROM process AS process WHERE process.name=?') + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll([["some-host1"], ["some-host2"]]) + + hosts = self.processRepository.getHostsByToolName("some-toolname", "FetchAll") + self.assertEqual([["some-host1"], ["some-host2"]], hosts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-toolname") + + def test_storeCloseStatus_WhenProvidedProcessId_StoresCloseStatus(self): + self.mockProcess.closed = 'False' + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + self.processRepository.storeCloseStatus("some-process-id") + + self.assertEqual('True', self.mockProcess.closed) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def test_storeScreenshot_WhenProvidedIPAndPortAndFileName_StoresScreenshot(self): + self.processRepository.storeScreenshot("some-ip", "some-port", "some-filename") + self.mockDbSession.add.assert_called_once() + self.mockDbSession.commit.assert_called_once() + + def test_toggleProcessDisplayStatus_whenResetAllIsTrue_setDisplayToFalseForAllProcessesThatAreNotRunning( + self): + process1 = build_mock_process(status="Waiting", display="True") + process2 = build_mock_process(status="Waiting", display="True") + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mockDbSession.query.return_value = mock_query_response + self.processRepository.toggleProcessDisplayStatus(resetAll=True) + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.mockDbSession.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + self.mockDbAdapter.commit.assert_called_once() + + def test_toggleProcessDisplayStatus_whenResetAllIFalse_setDisplayToFalseForAllProcessesThatAreNotRunningOrWaiting( + self): + process1 = build_mock_process(status="Random Status", display="True") + process2 = build_mock_process(status="Another Random Status", display="True") + process3 = build_mock_process(status="Running", display="True") + mock_query_response = MagicMock() + mock_filtered_response = MagicMock() + mock_filtered_response.all.return_value = [process1, process2] + mock_query_response.filter_by.return_value = mock_filtered_response + self.mockDbSession.query.return_value = mock_query_response + self.processRepository.toggleProcessDisplayStatus() + + self.assertEqual("False", process1.display) + self.assertEqual("False", process2.display) + self.assertEqual("True", process3.display) + self.mockDbSession.add.assert_has_calls([ + mock.call(process1), + mock.call(process2), + ]) + self.mockDbAdapter.commit.assert_called_once() + + def mockProcessStatusAndReturnSingle(self, processStatus: str): + self.mockProcess.status = processStatus + self.mockDbSession.query.return_value = mockQueryWithFilterBy(mockFirstByReturnValue(self.mockProcess)) + + def assertProcessStatusUpdatedTo(self, expected_status: str): + self.assertEqual(expected_status, self.mockProcess.status) + self.mockDbSession.add.assert_called_once_with(self.mockProcess) + self.mockDbAdapter.commit.assert_called_once() + + def whenProcessDoesNotFinishGracefully(self, process_status: str): + from db.entities.process import process + from db.entities.processOutput import process_output + + expected_process: process = MagicMock() + expected_process.status = process_status + expected_process_output: process_output = MagicMock() + self.mockDbSession.query.return_value = mockQueryWithFilterBy( + mockFirstBySideEffect([expected_process, expected_process_output])) + + self.processRepository.storeProcessOutput("some_process_id", "this is some cool output") + + self.mockDbSession.add.assert_called_once_with(expected_process_output) + self.mockDbAdapter.commit.assert_called_once() diff --git a/tests/db/repositories/test_ScriptRepository.py b/tests/db/repositories/test_ScriptRepository.py new file mode 100644 index 00000000..e3d56e11 --- /dev/null +++ b/tests/db/repositories/test_ScriptRepository.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import MagicMock + +from tests.db.helpers.db_helpers import mockExecuteFetchAll + + +class ScriptRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.ScriptRepository import ScriptRepository + self.mockDbAdapter = MagicMock() + self.scriptRepository = ScriptRepository(self.mockDbAdapter) + + def test_getScriptsByHostIP_WhenProvidedAHostIP_ReturnsAllScripts(self): + expectedQuery = ("SELECT host.id, host.scriptId, port.portId, port.protocol FROM l1ScriptObj AS host " + "INNER JOIN hostObj AS hosts ON hosts.id = host.hostId " + "LEFT OUTER JOIN portObj AS port ON port.id = host.portId WHERE hosts.ip=?") + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-script1'], ['some-script2']]) + scripts = self.scriptRepository.getScriptsByHostIP("some-host-ip") + self.assertEqual([['some-script1'], ['some-script2']], scripts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-host-ip") + + def test_getScriptOutputById_WhenProvidedAScriptId_ReturnsScriptOutput(self): + expectedQuery = "SELECT script.output FROM l1ScriptObj as script WHERE script.id = ?" + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [['some-script-output1'], ['some-script-output2']]) + + scripts = self.scriptRepository.getScriptOutputById("some-id") + self.assertEqual([['some-script-output1'], ['some-script-output2']], scripts) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some-id") diff --git a/tests/db/repositories/test_ServiceRepository.py b/tests/db/repositories/test_ServiceRepository.py new file mode 100644 index 00000000..2006897e --- /dev/null +++ b/tests/db/repositories/test_ServiceRepository.py @@ -0,0 +1,74 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import MagicMock, patch + +from tests.db.helpers.db_helpers import mockExecuteFetchAll, mockFirstByReturnValue + + +class ServiceRepositoryTest(unittest.TestCase): + def setUp(self) -> None: + from db.repositories.ServiceRepository import ServiceRepository + self.mockDbAdapter = MagicMock() + self.repository = ServiceRepository(self.mockDbAdapter) + + def getServiceNamesTestCase(self, filters, expectedQuery): + self.mockDbAdapter.metadata.bind.execute.return_value = mockExecuteFetchAll( + [{'name': 'service_name1'}, {'name': 'service_name2'}]) + service_names = self.repository.getServiceNames(filters) + + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery) + self.assertEqual([{'name': 'service_name1'}, {'name': 'service_name2'}], service_names) + + def test_getServiceNames_InvokedWithNoFilters_FetchesAllServiceNames(self): + from app.auxiliary import Filters + + expectedQuery = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "ORDER BY service.name ASC") + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + self.getServiceNamesTestCase(filters=filters, expectedQuery=expectedQuery) + + def test_getServiceNames_InvokedWithFewFilters_FetchesAllServiceNamesWithFiltersApplied(self): + from app.auxiliary import Filters + + expectedQuery = ("SELECT DISTINCT service.name FROM serviceObj as service " + "INNER JOIN portObj as ports " + "INNER JOIN hostObj AS hosts " + "ON hosts.id = ports.hostId AND service.id=ports.serviceId WHERE 1=1 " + "AND hosts.status != 'down' AND ports.protocol != 'udp' " + "ORDER BY service.name ASC") + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + self.getServiceNamesTestCase(filters=filters, expectedQuery=expectedQuery) + + def test_getServiceNamesByHostIPAndPort_WhenProvidedWithHostIpAndPort_ReturnsServiceNames(self): + self.mockDbAdapter.metadata.bind.execute.return_value = mockFirstByReturnValue( + [['service-name1'], ['service-name2']]) + expectedQuery = ("SELECT services.name FROM serviceObj AS services " + "INNER JOIN hostObj AS hosts ON hosts.id = ports.hostId " + "INNER JOIN portObj AS ports ON services.id=ports.serviceId " + "WHERE hosts.ip=? and ports.portId = ?") + result = self.repository.getServiceNamesByHostIPAndPort("some_host", "1234") + self.assertEqual([['service-name1'], ['service-name2']], result) + self.mockDbAdapter.metadata.bind.execute.assert_called_once_with(expectedQuery, "some_host", "1234") diff --git a/tests/db/test_filters.py b/tests/db/test_filters.py new file mode 100644 index 00000000..ee8ba4ae --- /dev/null +++ b/tests/db/test_filters.py @@ -0,0 +1,127 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch + + +class FiltersTest(unittest.TestCase): + @patch('utilities.stenoLogging.get_logger') + def setUp(self, get_logger) -> None: + return + + def test_applyFilters_InvokedWithNoFilters_ReturnsEmptyString(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual("", result) + + def test_applyFilters_InvokedWithHostDownFilter_ReturnsQueryFilterWithHostsDown(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=False, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND hosts.status != 'down'", result) + + def test_applyFilters_InvokedWithHostUpFilter_ReturnsQueryFilterWithHostsUp(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=False, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND hosts.status != 'up'", result) + + def test_applyFilters_InvokedWithHostCheckedFilter_ReturnsQueryFilterWithHostsChecked(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=False, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND hosts.checked != 'True'", result) + + def test_applyFilters_InvokedWithPortOpenFilter_ReturnsQueryFilterWithPortOpen(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=False, portfiltered=True, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND ports.state != 'open' AND ports.state != 'open|filtered'", result) + + def test_applyFilters_InvokedWithPortClosedFilter_ReturnsQueryFilterWithPortClosed(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=False, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND ports.state != 'closed'", result) + + def test_applyFilters_InvokedWithPortFilteredFilter_ReturnsQueryFilterWithPortFiltered(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=False, portclosed=True, + tcp=True, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND ports.state != 'filtered' AND ports.state != 'open|filtered'", result) + + def test_applyFilters_InvokedWithTcpProtocolFilter_ReturnsQueryFilterWithTcp(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=False, udp=True) + result = applyFilters(filters) + self.assertEqual(" AND ports.protocol != 'tcp'", result) + + def test_applyFilters_InvokedWithUdpProtocolFilter_ReturnsQueryFilterWithUdp(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=False) + result = applyFilters(filters) + self.assertEqual(" AND ports.protocol != 'udp'", result) + + def test_applyFilters_InvokedWithKeywordsFilter_ReturnsQueryFilterWithKeywords(self): + from app.auxiliary import Filters + from db.filters import applyFilters + + filters: Filters = Filters() + keyword = "some-keyword" + filters.apply(up=True, down=True, checked=True, portopen=True, portfiltered=True, portclosed=True, + tcp=True, udp=True, keywords=[keyword]) + result = applyFilters(filters) + self.assertEqual(" AND (hosts.ip LIKE '%some-keyword%' OR hosts.osMatch LIKE '%some-keyword%'" + " OR hosts.hostname LIKE '%some-keyword%')", result) diff --git a/tests/db/test_validation.py b/tests/db/test_validation.py new file mode 100644 index 00000000..587486bb --- /dev/null +++ b/tests/db/test_validation.py @@ -0,0 +1,28 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest + +from db.validation import sanitise + + +class validationTests(unittest.TestCase): + def test_sanitise_whenGivenAStringIncludingSingleQuotes_EscapesSingleQuotes(self): + self.assertEqual(sanitise("my' escaped ' string"), "my'' escaped '' string") + + def test_sanitise_whenGivenAStringWithNoSingleQuotes_ReturnsSameString(self): + self.assertEqual("my string", sanitise("my string")) diff --git a/tests/parsers/__init__.py b/tests/parsers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/parsers/nmap-fixtures/malformed-nmap-report.xml b/tests/parsers/nmap-fixtures/malformed-nmap-report.xml new file mode 100644 index 00000000..fe3d655e --- /dev/null +++ b/tests/parsers/nmap-fixtures/malformed-nmap-report.xml @@ -0,0 +1,39 @@ + + + +<> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/parsers/nmap-fixtures/valid-nmap-report.xml b/tests/parsers/nmap-fixtures/valid-nmap-report.xml new file mode 100644 index 00000000..17bae094 --- /dev/null +++ b/tests/parsers/nmap-fixtures/valid-nmap-report.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/parsers/test_Parser.py b/tests/parsers/test_Parser.py new file mode 100644 index 00000000..c5ae0a77 --- /dev/null +++ b/tests/parsers/test_Parser.py @@ -0,0 +1,111 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from os.path import dirname, join +from typing import Optional + +from parsers.Host import Host +from parsers.OS import OS +from parsers.Parser import Parser, parseNmapReport, MalformedXmlDocumentException +from parsers.Port import Port +from parsers.Session import Session + + +def givenAnXmlFile(xmlFile: str) -> Parser: + return parseNmapReport(join(dirname(__file__), xmlFile)) + + +class ParserTest(unittest.TestCase): + def test_parseNmapReport_givenAValidXml_ReturnsAValidParser(self): + self.assertIsNotNone(givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml")) + + def test_parseNmapReport_givenAnInvalidXml_RaisesException(self): + with self.assertRaises(MalformedXmlDocumentException): + givenAnXmlFile("nmap-fixtures/malformed-nmap-report.xml") + + def test_parser_givenValidXmlWithHosts_ReturnsServicesParsed(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + hosts = list(parser.getAllHosts()) + self.assertEqual(1, len(hosts)) + + allPorts: [Port] = hosts[0].all_ports() + allServiceNames: [str] = list(map(lambda port: port.getService().name, allPorts)) + allServiceVersions: [str] = list(map(lambda port: port.getService().version, allPorts)) + allServiceFingerprints: [str] = list(map(lambda port: port.getService().fingerprint, allPorts)) + allServiceProducts: [str] = list(map(lambda port: port.getService().product, allPorts)) + allServiceExtraInfos: [str] = list(map(lambda port: port.getService().extrainfo, allPorts)) + self.assertEqual(['domain', 'http', 'netbios-ssn', 'https', 'msft'], allServiceNames) + self.assertEqual(['0.0', '0.0', '0.0', '1.0', '0.0'], allServiceVersions) + self.assertEqual(['p1', 'p2', 'p3', 'p4', 'p5'], allServiceFingerprints) + self.assertEqual(['table', 'table', 'table', 'table', 'table'], allServiceProducts) + self.assertEqual(['exinfo', 'exinfo', 'exinfo', 'exinfo', 'exinfo'], allServiceExtraInfos) + + def test_parser_givenAValidXmlWithIpAddresses_ReturnsAllIpAddresses(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual(['192.168.1.1'], parser.getAllIps()) + + def test_parser_givenAValidXmlWithIpAddressesAndFilterByStatus_ReturnsAllIpAddresses(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual(['192.168.1.1'], parser.getAllIps('up')) + + def test_parser_givenAValidXmlWithIpAddressesAndFilterByStatusWithNoResults_ReturnsAnEmptyList(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertEqual([], parser.getAllIps('down')) + + def test_parser_givenAValidXmlWithHosts_ReturnsHostByIpAddress(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + host: Optional[Host.Host] = parser.getHost("192.168.1.1") + self.assertIsNotNone(host) + + self.assertEqual("192.168.1.1", host.ip) + self.assertEqual("192.168.1.1", host.ipv4) + self.assertEqual("up", host.status) + self.assertEqual("coolhost", host.hostname) + self.assertEqual("closed", host.state) + + ports: [Port.Port] = host.all_ports() + allPortIds: [str] = list(map(lambda port: port.portId, ports)) + allPortProtocols: [str] = list(map(lambda port: port.protocol, ports)) + allPortStates: [str] = list(map(lambda port: port.state, ports)) + self.assertEqual(['53', '80', '139', '443', '445'], allPortIds) + self.assertEqual(['tcp', 'tcp', 'tcp', 'tcp', 'tcp'], allPortProtocols) + self.assertEqual(['open', 'open', 'open', 'open', 'open'], allPortStates) + + self.assertEqual(1, len(host.getOs())) + operatingSystem: OS = host.getOs()[0] + self.assertEqual("macos", operatingSystem.name) + self.assertEqual("darwin", operatingSystem.family) + self.assertEqual("x64", operatingSystem.osType) + self.assertEqual("5", operatingSystem.generation) + self.assertEqual("apple", operatingSystem.vendor) + self.assertEqual("98%", operatingSystem.accuracy) + + def test_parser_givenAValidXmlButInvalidHostIp_ReturnsNone(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + self.assertIsNone(parser.getHost("192.168.1.2")) + + def test_parser_givenAValidXml_ReturnsSessionInfo(self): + parser = givenAnXmlFile("nmap-fixtures/valid-nmap-report.xml") + session: Session.Session = parser.getSession() + self.assertEqual("Wed Apr 15 20:34:59 2020", session.finish_time) + self.assertEqual("7.80", session.nmapVersion) + self.assertEqual("nmap -oX output.xml --stylesheet nmap.xsl -vv -p1-1024 -sT 192.168.1.1", session.scanArgs) + self.assertEqual("Wed Apr 15 20:34:59 2020", session.startTime) + self.assertEqual("1", session.totalHosts) + self.assertEqual("1", session.upHosts) + self.assertEqual("0", session.downHosts) diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 00000000..f9d2a120 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,31 @@ +try: + from sqlalchemy.orm.scoping import ScopedSession as scoped_session + print("SQL Alchemy library OK") +except ImportError: + print("Import failed. SQL Alchemy library not found.") + exit(1) + +try: + from PyQt6 import QtWidgets, QtGui, QtCore + print("PyQt6 library OK.") +except ImportError: + print("Import failed. PyQt6 library not found.") + exit(1) + +try: + import quamash + import asyncio + print("Quamash and asyncio libraries OK.") +except ImportError: + print("Import failed. quamash or asyncio not found.") + exit(1) + +try: + from app.logic import * + from ui.gui import * + from ui.view import * + from controller.controller import * + print("Legion class imports OK.") +except ImportError: + print("Import failed. One or more modules failed to import correctly.") + exit(1) diff --git a/tests/ui/__init__.py b/tests/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ui/observers/__init__.py b/tests/ui/observers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ui/observers/test_QtUpdateProgressObserver.py b/tests/ui/observers/test_QtUpdateProgressObserver.py new file mode 100644 index 00000000..7a6df5d2 --- /dev/null +++ b/tests/ui/observers/test_QtUpdateProgressObserver.py @@ -0,0 +1,41 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest.mock import patch + + +class QtUpdateProgressObserverTest(unittest.TestCase): + @patch("ui.ancillaryDialog.ProgressWidget") + @patch('utilities.stenoLogging.get_logger') + def setUp(self, mockProgressWidget, mockLogging) -> None: + from ui.observers.QtUpdateProgressObserver import QtUpdateProgressObserver + self.mockProgressWidget = mockProgressWidget + self.qtUpdateProgressObserver = QtUpdateProgressObserver(self.mockProgressWidget) + + def test_onStart_callsShowOnProgressWidget(self): + self.qtUpdateProgressObserver.onStart() + self.mockProgressWidget.show.assert_called_once() + + def test_onFinished_callsHideOnProgressWidget(self): + self.qtUpdateProgressObserver.onFinished() + self.mockProgressWidget.hide.assert_called_once() + + def test_onProgressUpdate_callsSetProgressAndShow(self): + self.qtUpdateProgressObserver.onProgressUpdate(25) + self.mockProgressWidget.setProgress.assert_called_once_with(25) + self.mockProgressWidget.show.assert_called_once() diff --git a/tests/ui/test_eventfilter.py b/tests/ui/test_eventfilter.py new file mode 100644 index 00000000..3170f728 --- /dev/null +++ b/tests/ui/test_eventfilter.py @@ -0,0 +1,111 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +import unittest +from unittest import mock +from unittest.mock import MagicMock, Mock, patch + +from PyQt6.QtCore import QEvent, Qt, QObject +from PyQt6.QtWidgets import QApplication + +from ui.eventfilter import MyEventFilter + + +class MyEventFilterTestCase(unittest.TestCase): + def setUp(self) -> None: + self.mock_view = MagicMock() + self.mock_main_window = MagicMock() + self.mock_event = MagicMock() + self.mock_receiver = MagicMock() + + def test_eventFilter_whenKeyPressedIsClose_InvokesAppExit(self): + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.mock_event.type = Mock(return_value=QEvent.Type.Close) + + result = event_filter.eventFilter(self.mock_main_window, self.mock_event) + self.assertTrue(result) + self.mock_event.ignore.assert_called_once() + self.mock_view.appExit.assert_called_once() + + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') + def test_eventFilter_whenKeyDownPressed_SelectsNextRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row): + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key.Key_Down) + self.mock_receiver = hosts_table_view + selected_row.row = Mock(return_value=0) + selection_model.selectedRows = Mock(return_value=[selected_row]) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + self.mock_receiver.selectRow.assert_called_once_with(1) + self.mock_receiver.clicked.emit.assert_called_with(selected_row) + + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') + def test_eventFilter_whenKeyUpPressed_SelectsPreviousRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row): + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key.Key_Up) + self.mock_receiver = hosts_table_view + selected_row.row = Mock(return_value=1) + selection_model.selectedRows = Mock(return_value=[selected_row]) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + self.mock_receiver.selectRow.assert_called_once_with(0) + self.mock_receiver.clicked.emit.assert_called_with(selected_row) + + @patch('PyQt6.QtWidgets.QTableView') + @patch('PyQt6.QtWidgets.QAbstractItemView') + @patch('PyQt6.QtCore.QModelIndex') + @patch('PyQt6.QtGui.QClipboard') + def test_eventFilter_whenKeyCPressed_SelectsPreviousRowAndEmitsClickEvent( + self, hosts_table_view, selection_model, selected_row, mock_clipboard): + expected_data = MagicMock() + expected_data.toString = Mock(return_value="some clipboard data") + control_modifier = mock.patch.object(QApplication, 'keyboardModifiers', return_value=Qt.KeyboardModifier.ControlModifier) + clipboard = mock.patch.object(QApplication, 'clipboard', return_value=mock_clipboard) + + self.mock_view.ui.HostsTableView = hosts_table_view + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + self.simulateKeyPress(Qt.Key.Key_C) + self.mock_receiver = hosts_table_view + selected_row.data = Mock(return_value=expected_data) + selection_model.currentIndex = Mock(return_value=selected_row) + self.mock_receiver.selectionModel = Mock(return_value=selection_model) + + with control_modifier, clipboard: + result = event_filter.eventFilter(self.mock_receiver, self.mock_event) + self.assertTrue(result) + mock_clipboard.setText.assert_called_once_with("some clipboard data") + + def test_eventFilter_onDefaultAction_CallsParentEventFilter(self): + event_filter = MyEventFilter(self.mock_view, self.mock_main_window) + result = event_filter.eventFilter(QObject(), QEvent(QEvent.Type.Scroll)) + self.assertFalse(result) + + def simulateKeyPress(self, key_event): + self.mock_event.type = Mock(return_value=QEvent.Type.KeyPress) + self.mock_event.key = Mock(return_value=key_event) diff --git a/ui/ViewHeaders.py b/ui/ViewHeaders.py new file mode 100644 index 00000000..d2fc21fd --- /dev/null +++ b/ui/ViewHeaders.py @@ -0,0 +1,39 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" + +hostTableHeaders = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "State", "Count", "Closed"] + +serviceTableHeaders = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + +serviceNamesTableHeaders = ["Name"] + +scriptsTableHeaders = ["Id", "Script", "Port", "Protocol"] + +cvesTableHeaders = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] + +processTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + +toolsTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + +toolHostsTableHeaders = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] diff --git a/ui/ViewState.py b/ui/ViewState.py new file mode 100644 index 00000000..3f77892b --- /dev/null +++ b/ui/ViewState.py @@ -0,0 +1,53 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.auxiliary import Filters + + +# Defines the state of the UI at any given moment +# Defaults are the initial state of the UI +class ViewState: + # Indicator if any changes have happened since last save (default: False [no changes]) + dirty: bool = False + # Indicator if 'Save As..' dialog should be used (default: True) + firstSave = True + # Indicator of which tabs should be displayed for each host (default: empty dictionary) + hostTabs = dict() + # Indicator of the numbering of the bruteforce tabs, incremented when a new tab is added (default: 1) + bruteTabCount = 1 + # to choose what to display in each panel (default: base filters) + filters = Filters() + # Indicator of which host was clicked last (default: None) + lastHostIdClicked = '' + # Indicator of which IP Address was clicked on last (default: None) + ip_clicked = '' + # Indicator of which Service was clicked on last (default: None) + service_clicked = '' + # Indicator of which Tool was clicked on last (default: None) + tool_clicked = '' + # Indicator of which script was clicked on last (default: None) + script_clicked = '' + # Indicator of which tool host was clicked on last (default: None) + tool_host_clicked = '' + # these variables indicate that the corresponding table needs to be updated. + # 'lazy' means we only update a table at the last possible minute - before the user needs to see it + lazy_update_hosts = False + lazy_update_services = False + lazy_update_tools = False + # Indicator if a context menu is showing (important to avoid disrupting the user) (default: False) + menuVisible = False + diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/addHostDialog.py b/ui/addHostDialog.py new file mode 100644 index 00000000..9e06b1fc --- /dev/null +++ b/ui/addHostDialog.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode +from ui.ancillaryDialog import flipState + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +# dialog shown when the user selects "Add host(s)" from the menu +class AddHostsDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + self.setupLayout() + + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Add host(s) to scan seperated by semicolons') + flags = Qt.WindowType.Window | Qt.WindowType.WindowSystemMenuHint | Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.WindowMaximizeButtonHint | \ + Qt.WindowType.WindowCloseButtonHint + self.setWindowFlags(flags) + + self.resize(700, 700) + + self.formLayout = QtWidgets.QVBoxLayout() + + self.lblHost = QtWidgets.QLabel(self) + self.lblHost.setText('IP(s), Range(s), and Host(s)') + self.txtHostList = QtWidgets.QPlainTextEdit(self) + + self.hlayout = QtWidgets.QHBoxLayout() + self.hlayout.addWidget(self.lblHost) + self.hlayout.addWidget(self.txtHostList) + + self.lblHostExample = QtWidgets.QLabel(self) + self.lblHostExample.setText('Ex: 192.168.1.0/24; 10.10.10.10-20; 1.2.3.4; bing.com') + self.font = QtGui.QFont('Calibri', 10) + self.lblHostExample.setFont(self.font) + self.lblHostExample.setAlignment(Qt.AlignmentFlag.AlignRight) + self.spacer = QSpacerItem(15,15) + + self.validationLabel = QtWidgets.QLabel(self) + self.validationLabel.setText('Invalid input. Please try again!') + self.validationLabel.setStyleSheet('QLabel { color: red }') + + self.spacer2 = QSpacerItem(5,5) + + # Mode + self.grpMode = QtWidgets.QGroupBox() + self.grpModeWidgets = QtWidgets.QHBoxLayout() + self.grpMode.setTitle('Mode Selection') + self.rdoModeOptEasy = QtWidgets.QRadioButton(self) + self.rdoModeOptEasy.setText('Easy') + self.rdoModeOptEasy.setToolTip('Easy mode [--lame]') + self.rdoModeOptHard = QtWidgets.QRadioButton(self) + self.rdoModeOptHard.setText('Hard') + self.rdoModeOptHard.setToolTip('Hard mode') + self.grpModeWidgets.addWidget(self.rdoModeOptEasy) + self.grpModeWidgets.addWidget(self.rdoModeOptHard) + self.grpMode.setLayout(self.grpModeWidgets) + self.rdoModeOptEasy.toggle() + + # Easy mode options + self.grpEasyMode = QtWidgets.QGroupBox() + self.grpEasyModeWidgets = QtWidgets.QHBoxLayout() + self.grpEasyMode.setTitle('Easy Mode Options') + self.chkDiscovery = QtWidgets.QCheckBox(self) + self.chkDiscovery.setText('Run nmap host discovery') + self.chkDiscovery.setToolTip('Typical host discovery options') + self.chkDiscovery.toggle() + self.chkNmapStaging = QtWidgets.QCheckBox(self) + self.chkNmapStaging.setText('Run staged nmap scan') + self.chkNmapStaging.setToolTip('Scan ports in stages with typical options') + self.chkNmapStaging.toggle() + self.grpEasyModeWidgets.addWidget(self.chkDiscovery) + self.grpEasyModeWidgets.addWidget(self.chkNmapStaging) + self.grpEasyMode.setLayout(self.grpEasyModeWidgets) + self.grpEasyMode.setEnabled(True) + + self.spacer3 = QSpacerItem(5,5) + + # Timing and performance options + self.grpScanTiming = QtWidgets.QGroupBox() + self.grpScanTimingWidgets = QtWidgets.QVBoxLayout() + self.grpScanTimingControlWidgets = QtWidgets.QHBoxLayout() + self.grpScanTimingLabelWidgets = QtWidgets.QHBoxLayout() + self.grpScanTiming.setTitle('Timing and Performance Options') + self.grpScanTimingSpacer = QSpacerItem(5,5) + self.lblScanTimingLabel0 = QtWidgets.QLabel() + self.lblScanTimingLabel1 = QtWidgets.QLabel() + self.lblScanTimingLabel2 = QtWidgets.QLabel() + self.lblScanTimingLabel3 = QtWidgets.QLabel() + self.lblScanTimingLabel4 = QtWidgets.QLabel() + self.lblScanTimingLabel5 = QtWidgets.QLabel() + self.lblScanTimingLabel0.setText("Paranoid") + self.lblScanTimingLabel0.setToolTip( + 'Serialize every scan operation with a 5 minute wait between each. Useful for evading IDS detection [-T0]') + self.lblScanTimingLabel1.setText("Sneaky") + self.lblScanTimingLabel1.setToolTip( + 'Serialize every scan operation with a 15 second wait between each. Useful for evading IDS detection [-T1]') + self.lblScanTimingLabel2.setText("Polite") + self.lblScanTimingLabel2.setToolTip( + 'Serialize every scan operation with a 0.4 second wait between each. Useful for evading IDS detection [-T2]' + ) + self.lblScanTimingLabel3.setText("Normal") + self.lblScanTimingLabel3.setToolTip('NMAP defaults including parallelization [-T3]') + self.lblScanTimingLabel4.setText("Aggressive") + self.lblScanTimingLabel4.setToolTip( + 'Sets the following options: --max-rtt-timeout 1250ms --min-rtt-timeout 100ms ' + + '--initial-rtt-timeout 500ms --max-retries 6 with a 10ms delay between operations [-T4]') + self.lblScanTimingLabel5.setText("Insane") + self.lblScanTimingLabel5.setToolTip('Sets the following options: --max-rtt-timeout 300ms ' + + '--min-rtt-timeout 50ms --initial-rtt-timeout 250ms --max-retries 2 ' + + '--host-timeout 15m --script-timeout 10m with a 5ms delay between ' + + 'operations [-T5]') + self.sldScanTimingSlider = QtWidgets.QSlider(Qt.Orientation.Horizontal) + self.sldScanTimingSlider.setRange(0, 5) + self.sldScanTimingSlider.setSingleStep(1) + self.sldScanTimingSlider.setValue(4) + self.grpScanTimingControlWidgets.addWidget(self.sldScanTimingSlider) + self.grpScanTimingControlWidgets.addItem(self.grpScanTimingSpacer) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel0) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel1) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel2) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel3) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel4) + self.grpScanTimingLabelWidgets.addWidget(self.lblScanTimingLabel5) + self.grpScanTimingLabelWidgets.setSpacing(45) + self.grpScanTimingWidgets.addLayout(self.grpScanTimingControlWidgets) + self.grpScanTimingWidgets.addLayout(self.grpScanTimingLabelWidgets) + self.grpScanTiming.setLayout(self.grpScanTimingWidgets) + + self.spacer3_5 = QSpacerItem(5,5) + + # Port scan options + self.rdoScanOptTcpConnect = QtWidgets.QRadioButton(self) + self.rdoScanOptTcpConnect.setText('TCP') + self.rdoScanOptTcpConnect.setToolTip('TCP connect() scanning [-sT]') + self.rdoScanOptObfuscated = QtWidgets.QRadioButton(self) + self.rdoScanOptObfuscated.setText('Obfuscated') + self.rdoScanOptObfuscated.setToolTip('Obfuscated scanning for avoiding Firewall and WAF detection [--data-length 5 --max-retries 2 --randomize-hosts]') + self.rdoScanOptFin = QtWidgets.QRadioButton(self) + self.rdoScanOptFin.setText('FIN') + self.rdoScanOptFin.setToolTip('FIN scanning sends a packet with only the FIN flag set [-sF]') + self.rdoScanOptNull = QtWidgets.QRadioButton(self) + self.rdoScanOptNull.setText('NULL') + self.rdoScanOptNull.setToolTip('Null scanning sends a packet with no flags switched on [-sN]') + self.rdoScanOptXmas = QtWidgets.QRadioButton(self) + self.rdoScanOptXmas.setText('Xmas') + self.rdoScanOptXmas.setToolTip('Xmas Tree scanning sets the FIN, URG and PUSH flags [-sX]') + self.rdoScanOptPingTcp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingTcp.setText('TCP Ping') + self.rdoScanOptPingTcp.setToolTip('TCP Ping scanning sends either a SYN or an ACK packet to any port '+ + '(80 is the default) on the remote system [-sP]') + self.rdoScanOptPingUdp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingUdp.setText('UDP Ping') + self.rdoScanOptPingUdp.setToolTip('UDP Ping scanning sends 0-byte UDP packets to each target port on the '+ + 'victim. Receipt of an ICMP Port Unreachable message signifies the port '+ + 'is closed, otherwise it is assumed open [-sU]') + + # Fragmentation option + self.chkScanOptFragmentation = QtWidgets.QCheckBox(self) + self.chkScanOptFragmentation.setText('Fragment') + self.chkScanOptFragmentation.setToolTip('Enable packet fragmentation [-f]') + self.chkScanOptFragmentation.toggle() + + # Port scan options + self.grpScanOpt = QtWidgets.QGroupBox() + self.grpScanOptWidgets = QtWidgets.QHBoxLayout() + self.grpScanOpt.setTitle('Port Scan Options') + self.grpScanOptWidgets.addWidget(self.rdoScanOptTcpConnect) + self.grpScanOptWidgets.addWidget(self.rdoScanOptObfuscated) + self.grpScanOptWidgets.addWidget(self.rdoScanOptFin) + self.grpScanOptWidgets.addWidget(self.rdoScanOptNull) + self.grpScanOptWidgets.addWidget(self.rdoScanOptXmas) + self.grpScanOptWidgets.addWidget(self.rdoScanOptPingTcp) + self.grpScanOptWidgets.addWidget(self.rdoScanOptPingUdp) + self.grpScanOptWidgets.addWidget(self.chkScanOptFragmentation) + self.grpScanOpt.setLayout(self.grpScanOptWidgets) + self.rdoScanOptObfuscated.toggle() + self.grpScanOpt.setEnabled(False) + + self.spacer4 = QSpacerItem(5,5) + + self.rdoScanOptPingDisable = QtWidgets.QRadioButton(self) + self.rdoScanOptPingDisable.setText('Disable') + self.rdoScanOptPingDisable.setToolTip('Disable Ping entirely [-Pn]') + self.rdoScanOptPingDefault = QtWidgets.QRadioButton(self) + self.rdoScanOptPingDefault.setText('Default') + self.rdoScanOptPingDefault.setToolTip('ICMP Echo Request and TCP ping, with ACK packets [-PB]') + self.rdoScanOptPingRegular = QtWidgets.QRadioButton(self) + self.rdoScanOptPingRegular.setText('ICMP') + self.rdoScanOptPingRegular.setToolTip('Standard ICMP Echo Request [-PE]') + self.rdoScanOptPingSyn = QtWidgets.QRadioButton(self) + self.rdoScanOptPingSyn.setText('TCP SYN') + self.rdoScanOptPingSyn.setToolTip('TCP Ping that sends SYN packets instead of ACK packets [-PT -PS]') + self.rdoScanOptPingAck = QtWidgets.QRadioButton(self) + self.rdoScanOptPingAck.setText('TCP ACK') + self.rdoScanOptPingAck.setToolTip('TCP Ping that sends ACK packets instead of SYN packets [-PT]') + self.rdoScanOptPingTimeStamp = QtWidgets.QRadioButton(self) + self.rdoScanOptPingTimeStamp.setText('Timestamp') + self.rdoScanOptPingTimeStamp.setToolTip('ICMP Timestamp Request [-PP]') + self.rdoScanOptPingNetmask = QtWidgets.QRadioButton(self) + self.rdoScanOptPingNetmask.setText('Netmask') + self.rdoScanOptPingNetmask.setToolTip('ICMP Netmask Request [-PM]') + + self.grpScanOptPing = QtWidgets.QGroupBox() + self.grpScanOptPingWidgets = QtWidgets.QHBoxLayout() + self.grpScanOptPing.setTitle('Host Discovery Options') + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingDisable) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingDefault) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingRegular) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingSyn) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingAck) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingTimeStamp) + self.grpScanOptPingWidgets.addWidget(self.rdoScanOptPingNetmask) + self.grpScanOptPing.setLayout(self.grpScanOptPingWidgets) + self.rdoScanOptPingDisable.toggle() + self.grpScanOptPing.setEnabled(False) + + self.spacer6 = QSpacerItem(5,5) + + # Custom scan options + self.scanOptCustomGroup = QtWidgets.QGroupBox() + self.scanOptCustomGroupWidgets = QtWidgets.QHBoxLayout() + self.scanOptCustomGroup.setTitle('Custom Options') + self.lblCustomOpt = QtWidgets.QLabel(self) + self.lblCustomOpt.setText('Additional arguments') + self.txtCustomOptList = QtWidgets.QLineEdit(self) + self.txtCustomOptList.setText("") #"-sV -O") + self.scanOptCustomGroupWidgets.addWidget(self.lblCustomOpt) + self.scanOptCustomGroupWidgets.addWidget(self.txtCustomOptList) + self.scanOptCustomGroup.setLayout(self.scanOptCustomGroupWidgets) + self.scanOptCustomGroup.setEnabled(False) + + self.cmdAddButton = QPushButton('Submit', self) + self.cmdAddButton.setMaximumSize(160, 70) + self.addIcon = QtGui.QIcon() + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.cmdAddButton.setIconSize(QtCore.QSize(19, 19)) + self.cmdAddButton.setIcon(self.addIcon) + + self.cmdCancelButton = QPushButton('Cancel', self) + self.cmdCancelButton.setMaximumSize(110, 30) + self.cancelIcon = QtGui.QIcon() + self.cancelIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/minus-black.png")), QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off) + self.cmdCancelButton.setIconSize(QtCore.QSize(19, 19)) + self.cmdCancelButton.setIcon(self.cancelIcon) + + self.cmdAddButton.setDefault(True) + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.cmdAddButton) + self.hlayout2.addWidget(self.cmdCancelButton) + self.formLayout.addLayout(self.hlayout) + self.formLayout.addWidget(self.lblHostExample) + + self.formLayout.addWidget(self.validationLabel) + self.validationLabel.hide() + + self.formLayout.addWidget(self.grpMode) + self.formLayout.addItem(self.spacer) + self.formLayout.addWidget(self.grpEasyMode) + self.formLayout.addItem(self.spacer2) + self.formLayout.addWidget(self.grpScanTiming) + self.formLayout.addItem(self.spacer3_5) + self.formLayout.addWidget(self.grpScanOpt) + self.formLayout.addItem(self.spacer3) + self.formLayout.addWidget(self.grpScanOptPing) + self.formLayout.addItem(self.spacer6) + self.formLayout.addWidget(self.scanOptCustomGroup) + self.formLayout.addItem(self.spacer4) + self.formLayout.addLayout(self.hlayout2) + self.setLayout(self.formLayout) + + + easyModeControls = [self.grpEasyMode] + hardModeControls = [self.grpScanOpt, self.grpScanOptPing, self.scanOptCustomGroup] + + self.rdoModeOptHard.clicked.connect(lambda: flipState(targetState = self.rdoModeOptHard.isChecked(), + widgetsToFlipOn = hardModeControls, + widgetsToFlipOff = easyModeControls)) + self.rdoModeOptEasy.clicked.connect(lambda: flipState(targetState = self.rdoModeOptEasy.isChecked(), + widgetsToFlipOn = easyModeControls, + widgetsToFlipOff = hardModeControls)) diff --git a/ui/ancillaryDialog.py b/ui/ancillaryDialog.py new file mode 100644 index 00000000..419a5cdc --- /dev/null +++ b/ui/ancillaryDialog.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode + +def flipState(targetState, widgetsToFlipOn, widgetsToFlipOff): + for widgetToFlipOn in widgetsToFlipOn: + widgetToFlipOn.setEnabled(targetState) + for widgetToFlipOff in widgetsToFlipOff: + widgetToFlipOff.setEnabled(not targetState) + +# Progress bar primative +class ProgressWidget(QtWidgets.QDialog): + def __init__(self, text, parent=None): + QtWidgets.QDialog.__init__(self, parent) + self.text = text + self.setWindowTitle(text) + self.setupLayout() + + def setupLayout(self): + vbox = QtWidgets.QVBoxLayout() + self.label = QtWidgets.QLabel(self.text) + self.progressBar = QtWidgets.QProgressBar() + vbox.addWidget(self.label) + vbox.addWidget(self.progressBar) + hbox = QtWidgets.QHBoxLayout() + hbox.addStretch(1) + vbox.addLayout(hbox) + self.setLayout(vbox) + + def setProgress(self, progress): + if progress > 100: + progress = 100 + self.progressBar.setValue(int(progress)) + + def setText(self, text): + self.text = text + self.setWindowTitle(text) + + def reset(self, text): + self.text = text + self.setWindowTitle(text) + self.setProgress(0) + +# Image display primative +class ImageViewer(QtWidgets.QWidget): + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + + self.scaleFactor = 0.0 + + self.imageLabel = QtWidgets.QLabel() + self.imageLabel.setBackgroundRole(QtGui.QPalette.ColorRole.Base) + self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Ignored) + self.imageLabel.setScaledContents(True) + + self.scrollArea = QtWidgets.QScrollArea() + self.scrollArea.setBackgroundRole(QtGui.QPalette.ColorRole.Dark) + self.scrollArea.setWidget(self.imageLabel) + + def open(self, fileName): + if fileName: + image = QtGui.QImage(fileName) + if image.isNull(): + QtWidgets.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName) + return + + self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image)) + self.scaleFactor = 1.0 + self.fitToWindow() + + def zoomIn(self): + self.scaleImage(1.25) + + def zoomOut(self): + self.scaleImage(0.8) + + def normalSize(self): + self.fitToWindow(False) + self.imageLabel.adjustSize() + self.scaleFactor = 1.0 + + def fitToWindow(self, fit=True): + self.scrollArea.setWidgetResizable(fit) + + def scaleImage(self, factor): + self.fitToWindow(False) + self.scaleFactor *= factor + self.imageLabel.resize(self.scaleFactor * self.imageLabel.pixmap().size()) + + self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor) + self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor) + + def adjustScrollBar(self, scrollBar, factor): + scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2))) + +# Gif and supported video display primative +class ImagePlayer(QtWidgets.QWidget): + def __init__(self, filename, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self.movie = QtGui.QMovie(filename) + self.movie_screen = QtWidgets.QLabel() + self.movie_screen.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + main_layout = QtWidgets.QVBoxLayout() + main_layout.addWidget(self.movie_screen) + self.setLayout(main_layout) + self.movie.setCacheMode(QtGui.QMovie.CacheMode.CacheAll) + self.movie.setSpeed(100) + self.movie_screen.setMovie(self.movie) + self.movie.start() diff --git a/ui/configDialog.py b/ui/configDialog.py new file mode 100644 index 00000000..ff83c50f --- /dev/null +++ b/ui/configDialog.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode +from ui.ancillaryDialog import flipState + +class Config(QtWidgets.QPlainTextEdit): + def __init__(self, qss, parent = None): + super(Config, self).__init__(parent) + self.setMinimumHeight(550) + self.setStyleSheet(qss) + self.setPlainText(open(os.path.expanduser('~/.local/share/legion/legion.conf'),'r').read()) + self.setReadOnly(False) + + def getText(self): + return self.toPlainText() + +class ConfigDialog(QtWidgets.QDialog): + def __init__(self, controller, qss, parent = None): + super(ConfigDialog, self).__init__(parent) + self.controller = controller + self.qss = qss + self.setWindowTitle("Config") + self.Main = QtWidgets.QVBoxLayout() + self.frm = QtWidgets.QFormLayout() + self.setGeometry(0, 0, 800, 600) + self.center() + self.Qui_update() + self.setStyleSheet(self.qss) + + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + + def Qui_update(self): + self.form = QtWidgets.QFormLayout() + self.form2 = QtWidgets.QVBoxLayout() + self.tabwid = QtWidgets.QTabWidget(self) + self.TabConfig = QtWidgets.QWidget(self) + self.cmdSave = QtWidgets.QPushButton("Save") + self.cmdSave.setFixedWidth(90) + self.cmdSave.setIcon(QtGui.QIcon('images/save.png')) + self.cmdSave.clicked.connect(self.save) + self.cmdClose = QtWidgets.QPushButton("Close") + self.cmdClose.setFixedWidth(90) + self.cmdClose.setIcon(QtGui.QIcon('images/close.png')) + self.cmdClose.clicked.connect(self.close) + + self.formConfig = QtWidgets.QFormLayout() + + # Config Section + self.configObj = Config(qss = self.qss) + self.formConfig.addRow(self.configObj) + self.TabConfig.setLayout(self.formConfig) + + self.tabwid.addTab(self.TabConfig,'Config') + self.form.addRow(self.tabwid) + self.form2.addWidget(QtWidgets.QLabel('
    ')) + self.form2.addWidget(self.cmdSave, alignment = Qt.AlignmentFlag.AlignCenter) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignmentFlag.AlignCenter) + self.form.addRow(self.form2) + self.Main.addLayout(self.form) + self.setLayout(self.Main) + + def save(self): + fileObj = open(os.path.expanduser('~/.local/share/legion/legion.conf'),'w') + fileObj.write(self.configObj.getText()) + fileObj.close() + self.controller.loadSettings() diff --git a/ui/dialogs.py b/ui/dialogs.py new file mode 100644 index 00000000..f7d062e8 --- /dev/null +++ b/ui/dialogs.py @@ -0,0 +1,760 @@ +#!/usr/bin/env python +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from app.timing import getTimestamp +from six import u as unicode + +class BruteWidget(QtWidgets.QWidget): + + def __init__(self, ip, port, service, settings, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self.ip = ip + self.port = port + self.service = service + + ## + #self.hydraServices = hydraServices + #self.hydraNoUsernameServices = hydraNoUsernameServices + #self.hydraNoPasswordServices = hydraNoPasswordServices + #self.bruteSettings = bruteSettings + #self.generalSettings = generalSettings + ## + + self.settings = settings + self.pid = -1 # will store hydra's pid so we can kill it + self.setupLayout() + + self.browseUsersButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsButton.clicked.connect(lambda: self.wordlistDialog('Choose password list')) + self.usersTextinput.textEdited.connect(self.singleUserRadio.toggle) + self.passwordsTextinput.textEdited.connect(self.singlePassRadio.toggle) + self.userlistTextinput.textEdited.connect(self.userListRadio.toggle) + self.passlistTextinput.textEdited.connect(self.passListRadio.toggle) + self.checkAddMoreOptions.stateChanged.connect(self.showMoreOptions) + + def setupLayoutHlayout(self): + hydraServiceConversion = {'login':'rlogin', 'ms-sql-s':'mssql', 'ms-wbt-server':'rdp', 'netbios-ssn':'smb', + 'netbios-ns':'smb', 'microsoft-ds':'smb', 'postgresql':'postgres', 'vmware-auth':'vmauthd"'} + # sometimes nmap service name is different from hydra service name + if self.service is None: + self.service = '' + elif str(self.service) in hydraServiceConversion: + self.service = hydraServiceConversion.get(str(self.service)) + + self.label1 = QtWidgets.QLabel() + self.label1.setText('IP') + self.label1.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label1.setAlignment(Qt.AlignmentFlag.AlignVCenter) + self.ipTextinput = QtWidgets.QLineEdit() + self.ipTextinput.setText(str(self.ip)) + self.ipTextinput.setFixedWidth(125) + + self.label2 = QtWidgets.QLabel() + self.label2.setText('Port') + self.label2.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label2.setAlignment(Qt.AlignmentFlag.AlignVCenter) + self.portTextinput = QtWidgets.QLineEdit() + self.portTextinput.setText(str(self.port)) + self.portTextinput.setFixedWidth(60) + + self.label3 = QtWidgets.QLabel() + self.label3.setText('Service') + self.label3.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.label3.setAlignment(Qt.AlignmentFlag.AlignVCenter) + self.serviceComboBox = QtWidgets.QComboBox() + self.serviceComboBox.insertItems(0, self.settings.brute_services.split(",")) + self.serviceComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") + self.serviceComboBox.currentIndexChanged.connect(self.checkSelectedService) + + # autoselect service from combo box + for i in range(len(self.settings.brute_services.split(","))): + if str(self.service) in self.settings.brute_services.split(",")[i]: + self.serviceComboBox.setCurrentIndex(i) + break + +# self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force +# self.labelPath.setFixedWidth(800) +# self.labelPath.setText('/') + + self.runButton = QPushButton('Run') + self.runButton.setMaximumSize(110, 30) + self.runButton.setDefault(True) # new + + ### + self.validationLabel = QtWidgets.QLabel(self) + self.validationLabel.setText('Invalid input. Please try again!') + self.validationLabel.setStyleSheet('QLabel { color: red }') + ### + + self.hlayout = QtWidgets.QHBoxLayout() + self.hlayout.addWidget(self.label1) + self.hlayout.addWidget(self.ipTextinput) + self.hlayout.addWidget(self.label2) + self.hlayout.addWidget(self.portTextinput) + self.hlayout.addWidget(self.label3) + self.hlayout.addWidget(self.serviceComboBox) + self.hlayout.addWidget(self.runButton) + ### + self.hlayout.addWidget(self.validationLabel) + self.validationLabel.hide() + ### + self.hlayout.addStretch() + + return self.hlayout + + def setupLayoutHlayout2(self): + self.singleUserRadio = QtWidgets.QRadioButton() + self.label4 = QtWidgets.QLabel() + self.label4.setText('Username') + self.label4.setFixedWidth(70) + self.usersTextinput = QtWidgets.QLineEdit() + self.usersTextinput.setFixedWidth(125) + self.usersTextinput.setText(self.settings.brute_default_username) + self.userListRadio = QtWidgets.QRadioButton() + self.label5 = QtWidgets.QLabel() + self.label5.setText('Username list') + self.label5.setFixedWidth(90) + self.userlistTextinput = QtWidgets.QLineEdit() + self.userlistTextinput.setFixedWidth(125) + self.browseUsersButton = QPushButton('Browse') + self.browseUsersButton.setMaximumSize(80, 30) + + self.foundUsersRadio = QtWidgets.QRadioButton() + self.label9 = QtWidgets.QLabel() + self.label9.setText('Found usernames') + self.label9.setFixedWidth(117) + + self.userGroup = QtWidgets.QButtonGroup() + self.userGroup.addButton(self.singleUserRadio) + self.userGroup.addButton(self.userListRadio) + self.userGroup.addButton(self.foundUsersRadio) + self.foundUsersRadio.toggle() + + self.warningLabel = QtWidgets.QLabel() + self.warningLabel.setText('*Note: when using form-based services from the Service menu, select the \ + "Additional Options" checkbox and add the proper arguments for the webpage form. See Hydra \ + documentation for extra help when targeting HTTP/HTTPS forms.') + self.warningLabel.setWordWrap(True) + self.warningLabel.setAlignment(Qt.AlignmentFlag.AlignRight) + self.warningLabel.setStyleSheet('QLabel { color: red }') + + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.singleUserRadio) + self.hlayout2.addWidget(self.label4) + self.hlayout2.addWidget(self.usersTextinput) + self.hlayout2.addWidget(self.userListRadio) + self.hlayout2.addWidget(self.label5) + self.hlayout2.addWidget(self.userlistTextinput) + self.hlayout2.addWidget(self.browseUsersButton) + self.hlayout2.addWidget(self.foundUsersRadio) + self.hlayout2.addWidget(self.label9) + self.hlayout2.addWidget(self.warningLabel) + self.warningLabel.hide() + self.hlayout2.addStretch() + + return self.hlayout2 + + def checkSelectedService(self): + self.service = str(self.serviceComboBox.currentText()) + if 'form' in str(self.service): + self.warningLabel.show() + #else: This clause would produce an interesting logic error and crash + #self.warningLabel.hide() + + def setupLayoutHlayout3(self): + #add usernames wordlist + self.singlePassRadio = QtWidgets.QRadioButton() + self.label6 = QtWidgets.QLabel() + self.label6.setText('Password') + self.label6.setFixedWidth(70) + self.passwordsTextinput = QtWidgets.QLineEdit() + self.passwordsTextinput.setFixedWidth(125) + self.passwordsTextinput.setText(self.settings.brute_default_password) + self.passListRadio = QtWidgets.QRadioButton() + self.label7 = QtWidgets.QLabel() + self.label7.setText('Password list') + self.label7.setFixedWidth(90) + self.passlistTextinput = QtWidgets.QLineEdit() + self.passlistTextinput.setFixedWidth(125) + self.browsePasswordsButton = QPushButton('Browse') + self.browsePasswordsButton.setMaximumSize(80, 30) + + self.foundPasswordsRadio = QtWidgets.QRadioButton() + self.label10 = QtWidgets.QLabel() + self.label10.setText('Found passwords') + self.label10.setFixedWidth(115) + + self.passGroup = QtWidgets.QButtonGroup() + self.passGroup.addButton(self.singlePassRadio) + self.passGroup.addButton(self.passListRadio) + self.passGroup.addButton(self.foundPasswordsRadio) + self.foundPasswordsRadio.toggle() + + self.label8 = QtWidgets.QLabel() + self.label8.setText('Threads') + self.label8.setFixedWidth(60) + self.threadOptions = [] + for i in range(1, 129): + self.threadOptions.append(str(i)) + self.threadsComboBox = QtWidgets.QComboBox() + self.threadsComboBox.insertItems(0, self.threadOptions) + self.threadsComboBox.setMinimumContentsLength(3) + self.threadsComboBox.setMaxVisibleItems(3) + self.threadsComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") + self.threadsComboBox.setCurrentIndex(15) + + self.hlayout3 = QtWidgets.QHBoxLayout() + self.hlayout3.addWidget(self.singlePassRadio) + self.hlayout3.addWidget(self.label6) + self.hlayout3.addWidget(self.passwordsTextinput) + self.hlayout3.addWidget(self.passListRadio) + self.hlayout3.addWidget(self.label7) + self.hlayout3.addWidget(self.passlistTextinput) + self.hlayout3.addWidget(self.browsePasswordsButton) + self.hlayout3.addWidget(self.foundPasswordsRadio) + self.hlayout3.addWidget(self.label10) + self.hlayout3.addStretch() + self.hlayout3.addWidget(self.label8) + self.hlayout3.addWidget(self.threadsComboBox) + #self.hlayout3.addStretch() + + return self.hlayout3 + + def setupLayoutHlayout4(self): + #label6.setText('Try blank password') + self.checkBlankPass = QtWidgets.QCheckBox() + self.checkBlankPass.setText('Try blank password') + self.checkBlankPass.toggle() + #add 'try blank password' + #label7.setText('Try login as password') + self.checkLoginAsPass = QtWidgets.QCheckBox() + self.checkLoginAsPass.setText('Try login as password') + self.checkLoginAsPass.toggle() + #add 'try login as password' + #label8.setText('Loop around users') + self.checkLoopUsers = QtWidgets.QCheckBox() + self.checkLoopUsers.setText('Loop around users') + self.checkLoopUsers.toggle() + #add 'loop around users' + #label9.setText('Exit on first valid') + self.checkExitOnValid = QtWidgets.QCheckBox() + self.checkExitOnValid.setText('Exit on first valid') + self.checkExitOnValid.toggle() + #add 'exit after first valid combination is found' + self.checkVerbose = QtWidgets.QCheckBox() + self.checkVerbose.setText('Verbose') + + self.checkAddMoreOptions = QtWidgets.QCheckBox() + self.checkAddMoreOptions.setText('Additional Options') + + self.hlayout4 = QtWidgets.QHBoxLayout() + self.hlayout4.addWidget(self.checkBlankPass) + self.hlayout4.addWidget(self.checkLoginAsPass) + self.hlayout4.addWidget(self.checkLoopUsers) + self.hlayout4.addWidget(self.checkExitOnValid) + self.hlayout4.addWidget(self.checkVerbose) + self.hlayout4.addWidget(self.checkAddMoreOptions) + self.hlayout4.addStretch() + + return self.hlayout4 + + def setupLayout(self): + ### + self.labelPath = QtWidgets.QLineEdit() # this is the extra input field to insert the path to brute force + self.labelPath.setFixedWidth(800) + self.labelPath.setText('-m "/login/login.html:username=^USER^&password=^PASS^&Login=Login:failed"') + ### + + self.layoutAddOptions = QtWidgets.QHBoxLayout() + self.layoutAddOptions.addWidget(self.labelPath) + self.labelPath.hide() + self.layoutAddOptions.addStretch() + + self.display = QtWidgets.QPlainTextEdit() + self.display.setReadOnly(True) + if self.settings.general_tool_output_black_background == 'True': + self.__drawPalette() + + self.vlayout = QtWidgets.QVBoxLayout() + self.vlayout.addLayout(self.setupLayoutHlayout()) + self.vlayout.addLayout(self.setupLayoutHlayout4()) + self.vlayout.addLayout(self.layoutAddOptions) + self.vlayout.addLayout(self.setupLayoutHlayout2()) + self.vlayout.addLayout(self.setupLayoutHlayout3()) + self.vlayout.addWidget(self.display) + self.setLayout(self.vlayout) + + def __drawPalette(self): + p = self.display.palette() + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font + self.display.setPalette(p) + self.display.setStyleSheet("QMenu { color:black;}") + + # TODO: need to check all the methods that need an additional input field and add them here +# def showMoreOptions(self, text): +# if str(text) == "http-head": +# self.labelPath.show() +# else: +# self.labelPath.hide() + + def showMoreOptions(self): + if self.checkAddMoreOptions.isChecked(): + self.labelPath.show() + else: + self.labelPath.hide() + + def wordlistDialog(self, title='Choose username list'): + + if title == 'Choose username list': + filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_username_wordlist_path) + self.userlistTextinput.setText(str(filename[0])) + self.userListRadio.toggle() + else: + filename = QtWidgets.QFileDialog.getOpenFileName(self, title, self.settings.brute_password_wordlist_path) + self.passlistTextinput.setText(str(filename[0])) + self.passListRadio.toggle() + + def buildHydraCommand(self, runningfolder, userlistPath, passlistPath): + + self.ip = self.ipTextinput.text() + self.port = self.portTextinput.text() + self.service = str(self.serviceComboBox.currentText()) + self.command = "hydra " + str(self.ip) + " -s " + self.port + " -o " + self.outputfile = runningfolder + "/hydra/" + getTimestamp() + "-" + str(self.ip) + "-" + self.port + "-" + \ + self.service + ".txt" + self.command += "\"" + self.outputfile + "\"" + + if 'form' not in str(self.service): + self.warningLabel.hide() + + if self.service not in self.settings.brute_no_username_services.split(","): + if self.singleUserRadio.isChecked(): + self.command += " -l " + self.usersTextinput.text() + elif self.foundUsersRadio.isChecked(): + self.command += " -L \"" + userlistPath+"\"" + else: + self.command += " -L \"" + self.userlistTextinput.text()+"\"" + + if self.service not in self.settings.brute_no_password_services.split(","): + if self.singlePassRadio.isChecked(): + escaped_password = self.passwordsTextinput.text().replace('"', '\"\"\"') + self.command += " -p \"" + escaped_password + "\"" + + elif self.foundPasswordsRadio.isChecked(): + self.command += " -P \"" + passlistPath + "\"" + else: + self.command += " -P \"" + self.passlistTextinput.text() + "\"" + + if self.checkBlankPass.isChecked(): + self.command += " -e n" + if self.checkLoginAsPass.isChecked(): + self.command += "s" + + elif self.checkLoginAsPass.isChecked(): + self.command += " -e s" + + if self.checkLoopUsers.isChecked(): + self.command += " -u" + + if self.checkExitOnValid.isChecked(): + self.command += " -f" + + if self.checkVerbose.isChecked(): + self.command += " -V" + + self.command += " -t " + str(self.threadsComboBox.currentText()) + + self.command += " " + self.service + +# if self.labelPath.isVisible(): # append the additional field's content, if it was visible + if self.checkAddMoreOptions.isChecked(): + self.command += " "+str(self.labelPath.text()) # TODO: sanitise this? + + #command = "echo "+escaped_password+" > /tmp/hydra-sub.txt" + #os.system(unicode(command)) + return self.command + + def getPort(self): + return self.port + + def toggleRunButton(self): + if self.runButton.text() == 'Run': + self.runButton.setText('Stop') + else: + self.runButton.setText('Run') + + def resetDisplay(self): + self.display.setParent(None) + self.display = QtWidgets.QPlainTextEdit() + self.display.setReadOnly(True) + if self.settings.general_tool_output_black_background == 'True': + self.__drawPalette() + + self.vlayout.addWidget(self.display) + +# dialog displayed when the user clicks on the advanced filters button +class FiltersDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + self.setupLayout() + self.applyButton.clicked.connect(self.close) + self.cancelButton.clicked.connect(self.close) + + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Filters') + self.setFixedSize(640, 200) + + hostsBox = QGroupBox("Host Filters") + self.hostsUp = QCheckBox("Show up hosts") + self.hostsUp.toggle() + self.hostsDown = QCheckBox("Show down hosts") + self.hostsChecked = QCheckBox("Show checked hosts") + self.hostsChecked.toggle() + hostLayout = QVBoxLayout() + hostLayout.addWidget(self.hostsUp) + hostLayout.addWidget(self.hostsDown) + hostLayout.addWidget(self.hostsChecked) + hostsBox.setLayout(hostLayout) + + portsBox = QGroupBox("Port Filters") + self.portsOpen = QCheckBox("Show open ports") + self.portsOpen.toggle() + self.portsFiltered = QCheckBox("Show filtered ports") + self.portsClosed = QCheckBox("Show closed ports") + self.portsTcp = QCheckBox("Show tcp") + self.portsTcp.toggle() + self.portsUdp = QCheckBox("Show udp") + self.portsUdp.toggle() + servicesLayout = QVBoxLayout() + servicesLayout.addWidget(self.portsOpen) + servicesLayout.addWidget(self.portsFiltered) + servicesLayout.addWidget(self.portsClosed) + servicesLayout.addWidget(self.portsTcp) + servicesLayout.addWidget(self.portsUdp) + portsBox.setLayout(servicesLayout) + + keywordSearchBox = QGroupBox("Keyword Filters") + self.hostKeywordText = QLineEdit() + keywordLayout = QVBoxLayout() + keywordLayout.addWidget(self.hostKeywordText) + keywordSearchBox.setLayout(keywordLayout) + + hlayout = QtWidgets.QHBoxLayout() + hlayout.addWidget(hostsBox) + hlayout.addWidget(portsBox) + hlayout.addWidget(keywordSearchBox) + + buttonLayout = QtWidgets.QHBoxLayout() + self.applyButton = QPushButton('Apply', self) + self.applyButton.setMaximumSize(110, 30) + self.cancelButton = QPushButton('Cancel', self) + self.cancelButton.setMaximumSize(110, 30) + buttonLayout.addWidget(self.cancelButton) + buttonLayout.addWidget(self.applyButton) + + layout = QVBoxLayout() + layout.addLayout(hlayout) + layout.addLayout(buttonLayout) + self.setLayout(layout) + + def getFilters(self): + return [self.hostsUp.isChecked(), self.hostsDown.isChecked(), self.hostsChecked.isChecked(), + self.portsOpen.isChecked(), self.portsFiltered.isChecked(), self.portsClosed.isChecked(), + self.portsTcp.isChecked(), self.portsUdp.isChecked(), unicode(self.hostKeywordText.text()).split()] + + def setCurrentFilters(self, filters): + if not self.hostsUp.isChecked() == filters[0]: + self.hostsUp.toggle() + + if not self.hostsDown.isChecked() == filters[1]: + self.hostsDown.toggle() + + if not self.hostsChecked.isChecked() == filters[2]: + self.hostsChecked.toggle() + + if not self.portsOpen.isChecked() == filters[3]: + self.portsOpen.toggle() + + if not self.portsFiltered.isChecked() == filters[4]: + self.portsFiltered.toggle() + + if not self.portsClosed.isChecked() == filters[5]: + self.portsClosed.toggle() + + if not self.portsTcp.isChecked() == filters[6]: + self.portsTcp.toggle() + + if not self.portsUdp.isChecked() == filters[7]: + self.portsUdp.toggle() + + self.hostKeywordText.setText(" ".join(filters[8])) + + + def setKeywords(self, keywords): + self.hostKeywordText.setText(keywords) + +# widget in which the host information is shown +class HostInformationWidget(QtWidgets.QWidget): + + def __init__(self, informationTab, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self.informationTab = informationTab + self.setupLayout() + self.updateFields() # set default values + + def setupLayout(self): + self.HostStatusLabel = QtWidgets.QLabel() + + self.HostStateLabel = QtWidgets.QLabel() + self.HostStateText = QtWidgets.QLabel() + self.HostStateLayout = QtWidgets.QHBoxLayout() + self.HostStateLayout.addSpacing(20) + self.HostStateLayout.addWidget(self.HostStateLabel) + self.HostStateLayout.addWidget(self.HostStateText) + self.HostStateLayout.addStretch() + + self.OpenPortsLabel = QtWidgets.QLabel() + self.OpenPortsText = QtWidgets.QLabel() + self.OpenPortsLayout = QtWidgets.QHBoxLayout() + self.OpenPortsLayout.addSpacing(20) + self.OpenPortsLayout.addWidget(self.OpenPortsLabel) + self.OpenPortsLayout.addWidget(self.OpenPortsText) + self.OpenPortsLayout.addStretch() + + self.ClosedPortsLabel = QtWidgets.QLabel() + self.ClosedPortsText = QtWidgets.QLabel() + self.ClosedPortsLayout = QtWidgets.QHBoxLayout() + self.ClosedPortsLayout.addSpacing(20) + self.ClosedPortsLayout.addWidget(self.ClosedPortsLabel) + self.ClosedPortsLayout.addWidget(self.ClosedPortsText) + self.ClosedPortsLayout.addStretch() + + self.FilteredPortsLabel = QtWidgets.QLabel() + self.FilteredPortsText = QtWidgets.QLabel() + self.FilteredPortsLayout = QtWidgets.QHBoxLayout() + self.FilteredPortsLayout.addSpacing(20) + self.FilteredPortsLayout.addWidget(self.FilteredPortsLabel) + self.FilteredPortsLayout.addWidget(self.FilteredPortsText) + self.FilteredPortsLayout.addStretch() + ################### + self.LocationLabel = QtWidgets.QLabel() + self.AddressLabel = QtWidgets.QLabel() + + self.IP4Label = QtWidgets.QLabel() + self.IP4Text = QtWidgets.QLabel() + self.IP4Layout = QtWidgets.QHBoxLayout() + self.IP4Layout.addSpacing(20) + self.IP4Layout.addWidget(self.IP4Label) + self.IP4Layout.addWidget(self.IP4Text) + self.IP4Layout.addStretch() + + self.IP6Label = QtWidgets.QLabel() + self.IP6Text = QtWidgets.QLabel() + self.IP6Layout = QtWidgets.QHBoxLayout() + self.IP6Layout.addSpacing(20) + self.IP6Layout.addWidget(self.IP6Label) + self.IP6Layout.addWidget(self.IP6Text) + self.IP6Layout.addStretch() + + self.MacLabel = QtWidgets.QLabel() + self.MacText = QtWidgets.QLabel() + self.MacLayout = QtWidgets.QHBoxLayout() + self.MacLayout.addSpacing(20) + self.MacLayout.addWidget(self.MacLabel) + self.MacLayout.addWidget(self.MacText) + self.MacLayout.addStretch() + + self.VendorLabel = QtWidgets.QLabel() + self.VendorText = QtWidgets.QLabel() + self.VendorLayout = QtWidgets.QHBoxLayout() + self.VendorLayout.addSpacing(20) + self.VendorLayout.addWidget(self.VendorLabel) + self.VendorLayout.addWidget(self.VendorText) + self.VendorLayout.addStretch() + + self.AsnLabel = QtWidgets.QLabel() + self.AsnText = QtWidgets.QLabel() + self.AsnLayout = QtWidgets.QHBoxLayout() + self.AsnLayout.addSpacing(20) + self.AsnLayout.addWidget(self.AsnLabel) + self.AsnLayout.addWidget(self.AsnText) + self.AsnLayout.addStretch() + + self.IspLabel = QtWidgets.QLabel() + self.IspText = QtWidgets.QLabel() + self.IspLayout = QtWidgets.QHBoxLayout() + self.IspLayout.addSpacing(20) + self.IspLayout.addWidget(self.IspLabel) + self.IspLayout.addWidget(self.IspText) + self.IspLayout.addStretch() + + self.dummyLabel = QtWidgets.QLabel() + self.dummyText = QtWidgets.QLabel() + self.dummyLayout = QtWidgets.QHBoxLayout() + self.dummyLayout.addSpacing(20) + self.dummyLayout.addWidget(self.dummyLabel) + self.dummyLayout.addWidget(self.dummyText) + self.dummyLayout.addStretch() + ######### + self.OSLabel = QtWidgets.QLabel() + + self.OSNameLabel = QtWidgets.QLabel() + self.OSNameText = QtWidgets.QLabel() + self.OSNameLayout = QtWidgets.QHBoxLayout() + self.OSNameLayout.addSpacing(20) + self.OSNameLayout.addWidget(self.OSNameLabel) + self.OSNameLayout.addWidget(self.OSNameText) + self.OSNameLayout.addStretch() + + self.OSAccuracyLabel = QtWidgets.QLabel() + self.OSAccuracyText = QtWidgets.QLabel() + self.OSAccuracyLayout = QtWidgets.QHBoxLayout() + self.OSAccuracyLayout.addSpacing(20) + self.OSAccuracyLayout.addWidget(self.OSAccuracyLabel) + self.OSAccuracyLayout.addWidget(self.OSAccuracyText) + self.OSAccuracyLayout.addStretch() + + self.CountryLabel = QtWidgets.QLabel() + self.CountryText = QtWidgets.QLabel() + self.CountryLayout = QtWidgets.QHBoxLayout() + self.CountryLayout.addSpacing(20) + self.CountryLayout.addWidget(self.CountryLabel) + self.CountryLayout.addWidget(self.CountryText) + self.CountryLayout.addStretch() + + self.CityLabel = QtWidgets.QLabel() + self.CityText = QtWidgets.QLabel() + self.CityLayout = QtWidgets.QHBoxLayout() + self.CityLayout.addSpacing(20) + self.CityLayout.addWidget(self.CityLabel) + self.CityLayout.addWidget(self.CityText) + self.CityLayout.addStretch() + + self.LatitudeLabel = QtWidgets.QLabel() + self.LatitudeText = QtWidgets.QLabel() + self.LatitudeLayout = QtWidgets.QHBoxLayout() + self.LatitudeLayout.addSpacing(20) + self.LatitudeLayout.addWidget(self.LatitudeLabel) + self.LatitudeLayout.addWidget(self.LatitudeText) + self.LatitudeLayout.addStretch() + + self.LongitudeLabel = QtWidgets.QLabel() + self.LongitudeText = QtWidgets.QLabel() + self.LongitudeLayout = QtWidgets.QHBoxLayout() + self.LongitudeLayout.addSpacing(20) + self.LongitudeLayout.addWidget(self.LongitudeLabel) + self.LongitudeLayout.addWidget(self.LongitudeText) + self.LongitudeLayout.addStretch() + + font = QtGui.QFont('Calibri', 12) # in each different section + font.setBold(True) + self.HostStatusLabel.setText('Host Status') + self.HostStatusLabel.setFont(font) + self.HostStateLabel.setText("State:") + self.OpenPortsLabel.setText('Open Ports:') + self.ClosedPortsLabel.setText('Closed Ports:') + self.FilteredPortsLabel.setText('Filtered Ports:') + self.LocationLabel.setText('Location') + self.LocationLabel.setFont(font) + self.AddressLabel.setText('Addresses') + self.AddressLabel.setFont(font) + self.IP4Label.setText('IPv4:') + self.IP6Label.setText('IPv6:') + self.MacLabel.setText('MAC:') + self.VendorLabel.setText('Vendor:') + self.AsnLabel.setText('ASN:') + self.IspLabel.setText('ISP:') + self.OSLabel.setText('Operating System') + self.OSLabel.setFont(font) + self.OSNameLabel.setText('Name:') + self.OSAccuracyLabel.setText('Accuracy:') + self.CountryLabel.setText('Country Code:') + self.CityLabel.setText('City:') + self.LatitudeLabel.setText('Latitude:') + self.LongitudeLabel.setText('Longitude:') + ######### + self.vlayout_1 = QtWidgets.QVBoxLayout() + self.vlayout_2 = QtWidgets.QVBoxLayout() + self.vlayout_3 = QtWidgets.QVBoxLayout() + self.vlayout_4 = QtWidgets.QVBoxLayout() + self.vlayout_5 = QtWidgets.QVBoxLayout() + self.hlayout_1 = QtWidgets.QHBoxLayout() + + self.vlayout_1.addWidget(self.HostStatusLabel) + self.vlayout_1.addLayout(self.HostStateLayout) + self.vlayout_1.addLayout(self.OpenPortsLayout) + self.vlayout_1.addLayout(self.ClosedPortsLayout) + self.vlayout_1.addLayout(self.FilteredPortsLayout) + + self.vlayout_2.addWidget(self.AddressLabel) + self.vlayout_2.addLayout(self.IP4Layout) + self.vlayout_2.addLayout(self.IP6Layout) + self.vlayout_2.addLayout(self.MacLayout) + self.vlayout_2.addLayout(self.VendorLayout) + self.vlayout_2.addLayout(self.AsnLayout) + self.vlayout_2.addLayout(self.IspLayout) + self.vlayout_2.addLayout(self.dummyLayout) + + self.hlayout_1.addLayout(self.vlayout_1) + self.hlayout_1.addSpacing(20) + self.hlayout_1.addLayout(self.vlayout_2) + self.hlayout_1.addSpacing(20) + self.hlayout_1.addLayout(self.vlayout_5) + + self.vlayout_3.addWidget(self.OSLabel) + self.vlayout_3.addLayout(self.OSNameLayout) + self.vlayout_3.addLayout(self.OSAccuracyLayout) + self.vlayout_3.addStretch() + + self.vlayout_4.addLayout(self.hlayout_1) + self.vlayout_4.addSpacing(10) + self.vlayout_4.addLayout(self.vlayout_3) + + self.vlayout_5.addWidget(self.LocationLabel) + self.vlayout_5.addLayout(self.CountryLayout) + self.vlayout_5.addLayout(self.CityLayout) + self.vlayout_5.addLayout(self.LatitudeLayout) + self.vlayout_5.addLayout(self.LongitudeLayout) + + self.hlayout_4 = QtWidgets.QHBoxLayout(self.informationTab) + self.hlayout_4.addLayout(self.vlayout_4) + self.hlayout_4.insertStretch(-1,1) + self.hlayout_4.addStretch() + + def updateFields(self, **kwargs): + self.HostStateText.setText(kwargs.get('status') or 'unknown') + self.OpenPortsText.setText(str(kwargs.get('openPorts') or 0)) + self.ClosedPortsText.setText(str(kwargs.get('closedPorts') or 0)) + self.FilteredPortsText.setText(str(kwargs.get('filteredPorts') or 0)) + self.IP4Text.setText(kwargs.get('ipv4') or 'unknown') + self.IP6Text.setText(kwargs.get('ipv6') or 'unknown') + self.MacText.setText(kwargs.get('macaddr') or 'unknown') + self.VendorText.setText(kwargs.get('vendor') or 'unknown') + self.AsnText.setText(kwargs.get('asn') or 'unknown') + self.IspText.setText(kwargs.get('isp') or 'unknown') + self.OSNameText.setText(kwargs.get('osMatch') or 'unknown') + self.OSAccuracyText.setText(kwargs.get('osAccuracy') or 'unknown') + self.CountryText.setText(kwargs.get('countryCode') or 'unknown') + self.CityText.setText(kwargs.get('city') or 'unknown') + self.LatitudeText.setText(kwargs.get('latitude') or 'unknown') + self.LongitudeText.setText(kwargs.get('longitude') or 'unknown') diff --git a/ui/eventfilter.py b/ui/eventfilter.py new file mode 100644 index 00000000..29242ca0 --- /dev/null +++ b/ui/eventfilter.py @@ -0,0 +1,68 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" +from PyQt6.QtCore import QObject, QEvent, Qt +from PyQt6.QtWidgets import QApplication + + +# This class is used to catch events such as arrow key presses or close window (X) +class MyEventFilter(QObject): + def __init__(self, view, main_window): + super().__init__() + self.view = view + self.main_window = main_window + self.hosts_table_views = { + view.ui.HostsTableView, + view.ui.ServiceNamesTableView, + view.ui.ToolsTableView, + view.ui.ToolHostsTableView, + view.ui.ScriptsTableView, + view.ui.ServicesTableView, + view.settingsWidget.toolForHostsTableWidget, + view.settingsWidget.toolForServiceTableWidget, + view.settingsWidget.toolForTerminalTableWidget, + } + + def eventFilter(self, receiver, event): + # catch up/down arrow key presses in hosts table + if event.type() == QEvent.Type.KeyPress and receiver in self.hosts_table_views: + return self.filterKeyPressInHostsTableView(event.key(), receiver) + elif event.type() == QEvent.Type.Close and receiver == self.main_window: + event.ignore() + self.view.appExit() + return True + else: + parent = super(MyEventFilter, self) + return parent.eventFilter(receiver, event) # normal event processing + + def filterKeyPressInHostsTableView(self, key, receiver): + if not receiver.selectionModel().selectedRows(): + return True + + index = receiver.selectionModel().selectedRows()[0].row() + + if key == Qt.Key.Key_Down: + new_index = index + 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + elif key == Qt.Key.Key_Up: + new_index = index - 1 + receiver.selectRow(new_index) + receiver.clicked.emit(receiver.selectionModel().selectedRows()[0]) + elif QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier and key == Qt.Key.Key_C: + selected = receiver.selectionModel().currentIndex() + clipboard = QApplication.clipboard() + clipboard.setText(selected.data().toString()) + return True diff --git a/ui/gui.py b/ui/gui.py new file mode 100644 index 00000000..c74b409a --- /dev/null +++ b/ui/gui.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtGui import QColor + +from ui.dialogs import * # for the screenshots (image viewer) +from ui.ancillaryDialog import * +from utilities.qtLogging import * + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) # do not change this name + self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter_2 = QtWidgets.QSplitter(self.centralwidget) + self.splitter_2.setOrientation(QtCore.Qt.Orientation.Vertical) + self.splitter_2.setObjectName(_fromUtf8("splitter_2")) + + self.MainTabWidget = QtWidgets.QTabWidget(self.splitter_2) + self.MainTabWidget.setObjectName(_fromUtf8("MainTabWidget")) + self.ScanTab = QtWidgets.QWidget() + self.ScanTab.setObjectName(_fromUtf8("ScanTab")) + self.gridLayout_2 = QtWidgets.QGridLayout(self.ScanTab) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.splitter = QtWidgets.QSplitter(self.ScanTab) + self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + + # size policies + self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + # this specifies that the widget will keep its width when the window is resized + self.sizePolicy.setHorizontalStretch(0) + self.sizePolicy.setVerticalStretch(0) + + self.sizePolicy2 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + # this specifies that the widget will expand its width when the window is resized + self.sizePolicy2.setHorizontalStretch(1) + self.sizePolicy2.setVerticalStretch(0) + + self.setupLeftPanel() + self.setupRightPanel() + self.setupMainTabs() + self.setupBottomPanel() + self.setupBottom2Panel() + + self.gridLayout.addWidget(self.splitter_2, 0, 0, 1, 1) + + MainWindow.setCentralWidget(self.centralwidget) + + self.setupMenuBar(MainWindow) + self.retranslateUi(MainWindow) + self.setDefaultIndexes() + QtCore.QMetaObject.connectSlotsByName(MainWindow) + MainWindow.setWindowState(QtCore.Qt.WindowState.WindowMaximized) + + def setupLeftPanel(self): + self.HostsTabWidget = QtWidgets.QTabWidget(self.splitter) + self.sizePolicy.setHeightForWidth(self.HostsTabWidget.sizePolicy().hasHeightForWidth()) + self.HostsTabWidget.setSizePolicy(self.sizePolicy2) + self.HostsTabWidget.setObjectName(_fromUtf8("HostsTabWidget")) + + self.HostsTab = QtWidgets.QWidget() + self.HostsTab.setObjectName(_fromUtf8("HostsTab")) + self.keywordTextInput = QtWidgets.QLineEdit() + self.keywordTextInput.setToolTip('Enter keywords and click apply to filter view') + + self.FilterApplyButton = QtWidgets.QToolButton() + self.searchIcon = QtGui.QIcon() + self.searchIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/search.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.FilterApplyButton.setIconSize(QtCore.QSize(19, 19)) + self.FilterApplyButton.setIcon(self.searchIcon) + self.FilterApplyButton.setToolTip('Apply filters to view') + + self.FilterAdvancedButton = QtWidgets.QToolButton() + self.advancedIcon = QtGui.QIcon() + self.advancedIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/advanced.png")), + QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.FilterAdvancedButton.setIconSize(QtCore.QSize(19, 19)) + self.FilterAdvancedButton.setIcon(self.advancedIcon) + self.FilterAdvancedButton.setToolTip('Choose advanced filters') + + self.AddHostButton = QtWidgets.QToolButton() + self.addIcon = QtGui.QIcon() + self.addIcon.addPixmap(QtGui.QPixmap(_fromUtf8("./images/add.png")), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.AddHostButton.setIconSize(QtCore.QSize(19, 19)) + self.AddHostButton.setIcon(self.addIcon) + self.AddHostButton.setToolTip('Add host') + + self.vlayout = QtWidgets.QVBoxLayout(self.HostsTab) + self.vlayout.setObjectName(_fromUtf8("vlayout")) + self.HostsTableView = QtWidgets.QTableView(self.HostsTab) + self.HostsTableView.setObjectName(_fromUtf8("HostsTableView")) + self.vlayout.addWidget(self.HostsTableView) + + # the overlay widget that appears over the hosttableview + self.addHostsOverlay = QtWidgets.QTextEdit(self.HostsTab) + self.addHostsOverlay.setObjectName(_fromUtf8("addHostsOverlay")) + self.addHostsOverlay.setText('Click here to add host(s) to scope') + self.addHostsOverlay.setReadOnly(True) + self.addHostsOverlay.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + + ### + self.addHostsOverlay.setFont(QtGui.QFont('Calibri', 12)) + self.addHostsOverlay.setAlignment(Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) + ### + + self.vlayout.addWidget(self.addHostsOverlay) + self.hlayout = QtWidgets.QHBoxLayout() + self.hlayout.addWidget(self.keywordTextInput) + self.hlayout.addWidget(self.FilterApplyButton) + self.hlayout.addWidget(self.FilterAdvancedButton) + self.hlayout.addWidget(self.AddHostButton) + self.vlayout.addLayout(self.hlayout) + self.HostsTabWidget.addTab(self.HostsTab, _fromUtf8("")) + + self.ServicesLeftTab = QtWidgets.QWidget() + self.ServicesLeftTab.setObjectName(_fromUtf8("ServicesLeftTab")) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.ServicesLeftTab) + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.ServiceNamesTableView = QtWidgets.QTableView(self.ServicesLeftTab) + self.ServiceNamesTableView.setObjectName(_fromUtf8("ServiceNamesTableView")) + self.horizontalLayout_2.addWidget(self.ServiceNamesTableView) + self.HostsTabWidget.addTab(self.ServicesLeftTab, _fromUtf8("")) + + self.ToolsTab = QtWidgets.QWidget() + self.ToolsTab.setObjectName(_fromUtf8("ToolsTab")) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.ToolsTab) + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.ToolsTableView = QtWidgets.QTableView(self.ToolsTab) + self.ToolsTableView.setObjectName(_fromUtf8("ToolsTableView")) + self.horizontalLayout_3.addWidget(self.ToolsTableView) + self.HostsTabWidget.addTab(self.ToolsTab, _fromUtf8("")) + + # Disabled for now + #self.CvesLeftTab = QtWidgets.QWidget() + #self.CvesLeftTab.setObjectName(_fromUtf8("CvesLeftTab")) + #self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.CvesLeftTab) + #self.horizontalLayout_8.setObjectName(_fromUtf8("horizontalLayout_8")) + #self.CvesTableView = QtWidgets.QTableView(self.CvesLeftTab) + #self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) + #self.horizontalLayout_8.addWidget(self.CvesTableView) + #self.HostsTabWidget.addTab(self.CvesLeftTab, _fromUtf8("")) + + def setupRightPanel(self): + self.ServicesTabWidget = QtWidgets.QTabWidget() + self.ServicesTabWidget.setEnabled(True) + self.sizePolicy2.setHeightForWidth(self.ServicesTabWidget.sizePolicy().hasHeightForWidth()) + self.ServicesTabWidget.setSizePolicy(self.sizePolicy2) + self.ServicesTabWidget.setObjectName(_fromUtf8("ServicesTabWidget")) + self.splitter.addWidget(self.ServicesTabWidget) + + ### + + self.splitter_3 = QtWidgets.QSplitter() + self.splitter_3.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.splitter_3.setObjectName(_fromUtf8("splitter_3")) + # this makes the tools tab stay the same width when resizing the window + self.splitter_3.setSizePolicy(self.sizePolicy2) + + ### + + self.ToolHostsWidget = QtWidgets.QWidget() + self.ToolHostsWidget.setObjectName(_fromUtf8("ToolHostsTab")) + self.ToolHostsLayout = QtWidgets.QVBoxLayout(self.ToolHostsWidget) + self.ToolHostsLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ToolHostsTableView = QtWidgets.QTableView(self.ToolHostsWidget) + self.ToolHostsTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.ToolHostsLayout.addWidget(self.ToolHostsTableView) + self.splitter_3.addWidget(self.ToolHostsWidget) + + self.DisplayWidget = QtWidgets.QWidget() + self.DisplayWidget.setObjectName('ToolOutput') + self.DisplayWidget.setSizePolicy(self.sizePolicy2) + ### ? + #self.toolOutputTextView = QtWidgets.QTextEdit(self.DisplayWidget) + self.toolOutputTextView = QtWidgets.QPlainTextEdit(self.DisplayWidget) + self.toolOutputTextView.setReadOnly(True) + self.DisplayWidgetLayout = QtWidgets.QHBoxLayout(self.DisplayWidget) + self.DisplayWidgetLayout.addWidget(self.toolOutputTextView) + self.splitter_3.addWidget(self.DisplayWidget) + + self.ScreenshotWidget = ImageViewer() + self.ScreenshotWidget.setObjectName('Screenshot') + self.ScreenshotWidget.scrollArea.setSizePolicy(self.sizePolicy2) + self.ScreenshotWidget.scrollArea.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) + self.splitter_3.addWidget(self.ScreenshotWidget.scrollArea) + + self.splitter.addWidget(self.splitter_3) + + ### + + self.ServicesRightTab = QtWidgets.QWidget() + self.ServicesRightTab.setObjectName(_fromUtf8("ServicesRightTab")) + self.verticalLayout = QtWidgets.QVBoxLayout(self.ServicesRightTab) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.ServicesTableView = QtWidgets.QTableView(self.ServicesRightTab) + self.ServicesTableView.setObjectName(_fromUtf8("ServicesTableView")) + self.verticalLayout.addWidget(self.ServicesTableView) + self.ServicesTabWidget.addTab(self.ServicesRightTab, _fromUtf8("")) + + self.CvesRightTab = QtWidgets.QWidget() + self.CvesRightTab.setObjectName(_fromUtf8("CvesRightTab")) + self.verticalLayout_1 = QtWidgets.QVBoxLayout(self.CvesRightTab) + self.verticalLayout_1.setObjectName(_fromUtf8("verticalLayout_1")) + self.CvesTableView = QtWidgets.QTableView(self.CvesRightTab) + self.CvesTableView.setObjectName(_fromUtf8("CvesTableView")) + self.verticalLayout_1.addWidget(self.CvesTableView) + self.ServicesTabWidget.addTab(self.CvesRightTab, _fromUtf8("")) + + self.ScriptsTab = QtWidgets.QWidget() + self.ScriptsTab.setObjectName(_fromUtf8("ScriptsTab")) + self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.ScriptsTab) + self.horizontalLayout_6.setObjectName(_fromUtf8("horizontalLayout_6")) + + self.splitter_4 = QtWidgets.QSplitter(self.ScriptsTab) + self.splitter_4.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.splitter_4.setObjectName(_fromUtf8("splitter_4")) + + self.ScriptsTableView = QtWidgets.QTableView() + self.ScriptsTableView.setObjectName(_fromUtf8("ScriptsTableView")) + self.splitter_4.addWidget(self.ScriptsTableView) + + self.ScriptsOutputTextEdit = QtWidgets.QPlainTextEdit() + self.ScriptsOutputTextEdit.setObjectName(_fromUtf8("ScriptsOutputTextEdit")) + self.ScriptsOutputTextEdit.setReadOnly(True) + self.splitter_4.addWidget(self.ScriptsOutputTextEdit) + self.horizontalLayout_6.addWidget(self.splitter_4) + self.ServicesTabWidget.addTab(self.ScriptsTab, _fromUtf8("")) + + self.InformationTab = QtWidgets.QWidget() + self.InformationTab.setObjectName(_fromUtf8("InformationTab")) + self.ServicesTabWidget.addTab(self.InformationTab, _fromUtf8("")) + + self.NotesTab = QtWidgets.QWidget() + self.NotesTab.setObjectName(_fromUtf8("NotesTab")) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.NotesTab) + self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4")) + self.NotesTextEdit = QtWidgets.QPlainTextEdit(self.NotesTab) + self.NotesTextEdit.setObjectName(_fromUtf8("NotesTextEdit")) + self.horizontalLayout_4.addWidget(self.NotesTextEdit) + self.ServicesTabWidget.addTab(self.NotesTab, _fromUtf8("")) + + def setupMainTabs(self): + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_3 = QtWidgets.QGridLayout() + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) + self.gridLayout_2.addLayout(self.gridLayout_3, 0, 0, 1, 1) + self.MainTabWidget.addTab(self.ScanTab, _fromUtf8("")) + + self.BruteTab = QtWidgets.QWidget() + self.BruteTab.setObjectName(_fromUtf8("BruteTab")) + self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.BruteTab) + self.horizontalLayout_7.setObjectName(_fromUtf8("horizontalLayout_7")) + self.BruteTabWidget = QtWidgets.QTabWidget(self.BruteTab) + self.BruteTabWidget.setObjectName(_fromUtf8("BruteTabWidget")) + self.horizontalLayout_7.addWidget(self.BruteTabWidget) + self.MainTabWidget.addTab(self.BruteTab, _fromUtf8("")) + + def setupBottomPanel(self): + self.BottomTabWidget = QtWidgets.QTabWidget(self.splitter_2) + self.BottomTabWidget.setSizeIncrement(QtCore.QSize(0, 0)) + self.BottomTabWidget.setBaseSize(QtCore.QSize(0, 0)) + self.BottomTabWidget.setObjectName(_fromUtf8("BottomTabWidget")) + + # Process Tab + self.ProcessTab = QtWidgets.QWidget() + self.ProcessTab.setObjectName(_fromUtf8("ProcessesTab")) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.ProcessTab) + self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5")) + self.ProcessesTableView = QtWidgets.QTableView(self.ProcessTab) + self.ProcessesTableView.setObjectName(_fromUtf8("ProcessesTableView")) + self.horizontalLayout_5.addWidget(self.ProcessesTableView) + self.BottomTabWidget.addTab(self.ProcessTab, _fromUtf8("")) + + def setupBottom2Panel(self): + # Log Tab + self.LogTab = QtWidgets.QWidget() + self.LogTab.setObjectName(_fromUtf8("LogTab")) + self.LogTabLayout = QtWidgets.QHBoxLayout(self.LogTab) + self.LogTabLayout.setObjectName(_fromUtf8("LogTabLayout")) + self.LogOutputTextView = QPlainTextEditLogger(self.LogTab) + self.LogOutputTextView.widget.setObjectName(_fromUtf8("LogOutputTextView")) + self.LogOutputTextView.widget.setReadOnly(True) + self.LogTabLayout.addWidget(self.LogOutputTextView.widget) + self.BottomTabWidget.addTab(self.LogTab, _fromUtf8("")) + log.addHandler(self.LogOutputTextView) + + # Python Tab - Disabled until next release + #self.PythonTab = QtWidgets.QWidget() + #self.PythonTab.setObjectName(_fromUtf8("PythonTab")) + #self.PythonOutputTextView = QtWidgets.QPlainTextEdit(self.PythonTab) + #self.PythonOutputTextView.setReadOnly(False) + #self.PythonTabLayout = QtWidgets.QHBoxLayout(self.PythonTab) + #self.PythonTabLayout.addWidget(self.PythonOutputTextView) + #self.BottomTabWidget.addTab(self.PythonTab, _fromUtf8("")) + + def setupMenuBar(self, MainWindow): + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1010, 25)) + self.menubar.setObjectName(_fromUtf8("menubar")) + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName(_fromUtf8("menuFile")) + #self.menuSettings = QtWidgets.QMenu(self.menubar) + #self.menuSettings.setObjectName(_fromUtf8("menuSettings")) + self.menuHelp = QtWidgets.QMenu(self.menubar) + self.menuHelp.setObjectName(_fromUtf8("menuHelp")) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName(_fromUtf8("statusbar")) + MainWindow.setStatusBar(self.statusbar) + self.actionExit = QtGui.QAction(MainWindow) + self.actionExit.setObjectName(_fromUtf8("actionExit")) + self.actionOpen = QtGui.QAction(MainWindow) + self.actionOpen.setObjectName(_fromUtf8("actionOpen")) + self.actionSave = QtGui.QAction(MainWindow) + self.actionSave.setObjectName(_fromUtf8("actionSave")) + self.actionImportNmap = QtGui.QAction(MainWindow) + self.actionImportNmap.setObjectName(_fromUtf8("actionImportNmap")) + self.actionSaveAs = QtGui.QAction(MainWindow) + self.actionSaveAs.setObjectName(_fromUtf8("actionSaveAs")) + self.actionNew = QtGui.QAction(MainWindow) + self.actionNew.setObjectName(_fromUtf8("actionNew")) + self.actionAddHosts = QtGui.QAction(MainWindow) + self.actionAddHosts.setObjectName(_fromUtf8("actionAddHosts")) + self.menuFile.addAction(self.actionNew) + self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionSave) + self.menuFile.addAction(self.actionSaveAs) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionAddHosts) + self.menuFile.addAction(self.actionImportNmap) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionExit) + self.menubar.addAction(self.menuFile.menuAction()) + #self.menubar.addAction(self.menuSettings.menuAction()) + #self.actionSettings = QtGui.QAction(MainWindow) + #self.actionSettings.setObjectName(_fromUtf8("getSettingsMenu")) + #self.menuSettings.addAction(self.actionSettings) + + self.actionHelp = QtGui.QAction(MainWindow) + self.actionHelp.setObjectName(_fromUtf8("getHelp")) + self.menuHelp.addAction(self.actionHelp) + self.menubar.addAction(self.menuHelp.menuAction()) + + self.actionConfig = QtGui.QAction(MainWindow) + self.actionConfig.setObjectName(_fromUtf8("config")) + self.menuHelp.addAction(self.actionConfig) + self.menubar.addAction(self.menuHelp.menuAction()) + + def setDefaultIndexes(self): + self.MainTabWidget.setCurrentIndex(1) + self.HostsTabWidget.setCurrentIndex(1) + self.ServicesTabWidget.setCurrentIndex(1) + self.BruteTabWidget.setCurrentIndex(1) + self.BottomTabWidget.setCurrentIndex(0) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "LEGION", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.HostsTab), + QtWidgets.QApplication.translate("MainWindow", "Hosts", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ServicesLeftTab), + QtWidgets.QApplication.translate("MainWindow", "Services", None)) + #self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.CvesLeftTab), + # QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) + self.HostsTabWidget.setTabText(self.HostsTabWidget.indexOf(self.ToolsTab), + QtWidgets.QApplication.translate("MainWindow", "Tools", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ServicesRightTab), + QtWidgets.QApplication.translate("MainWindow", "Services", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.CvesRightTab), + QtWidgets.QApplication.translate("MainWindow", "CVEs", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.ScriptsTab), + QtWidgets.QApplication.translate("MainWindow", "Scripts", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.InformationTab), + QtWidgets.QApplication.translate("MainWindow", "Information", None)) + self.ServicesTabWidget.setTabText(self.ServicesTabWidget.indexOf(self.NotesTab), + QtWidgets.QApplication.translate("MainWindow", "Notes", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.ScanTab), + QtWidgets.QApplication.translate("MainWindow", "Scan", None)) + self.MainTabWidget.setTabText(self.MainTabWidget.indexOf(self.BruteTab), + QtWidgets.QApplication.translate("MainWindow", "Brute", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.ProcessTab), + QtWidgets.QApplication.translate("MainWindow", "Processes", None)) + self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.LogTab), + QtWidgets.QApplication.translate("MainWindow", "Log", None)) + # self.BottomTabWidget.setTabText(self.BottomTabWidget.indexOf(self.PythonTab), + # QtWidgets.QApplication.translate("MainWindow", "Python", None)) - Disabled until future release + self.menuFile.setTitle(QtWidgets.QApplication.translate("MainWindow", "File", None)) + #self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings", None)) + self.menuHelp.setTitle(QtWidgets.QApplication.translate("MainWindow", "Help", None)) + self.actionExit.setText(QtWidgets.QApplication.translate("MainWindow", "Exit", None)) + self.actionExit.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Exit the application", None)) + self.actionExit.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+Q", None)) + self.actionOpen.setText(QtWidgets.QApplication.translate("MainWindow", "Open", None)) + self.actionOpen.setToolTip(QtWidgets.QApplication.translate("MainWindow", + "Open an existing project file", None)) + self.actionOpen.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+O", None)) + self.actionSave.setText(QtWidgets.QApplication.translate("MainWindow", "Save", None)) + self.actionSave.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Save the current project", None)) + self.actionSave.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+S", None)) + self.actionImportNmap.setText(QtWidgets.QApplication.translate("MainWindow", "Import nmap", None)) + self.actionImportNmap.setToolTip(QtWidgets.QApplication.translate("MainWindow", + "Import an nmap xml file", None)) + self.actionImportNmap.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+I", None)) + self.actionSaveAs.setText(QtWidgets.QApplication.translate("MainWindow", "Save As", None)) + self.actionNew.setText(QtWidgets.QApplication.translate("MainWindow", "New", None)) + self.actionNew.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+N", None)) + self.actionAddHosts.setText(QtWidgets.QApplication.translate("MainWindow", "Add host(s) to scope", None)) + self.actionAddHosts.setShortcut(QtWidgets.QApplication.translate("MainWindow", "Ctrl+H", None)) + #self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Preferences", None)) + self.actionHelp.setText(QtWidgets.QApplication.translate("MainWindow", "Help", None)) + self.actionHelp.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F1", None)) + self.actionConfig.setText(QtWidgets.QApplication.translate("MainWindow", "Config", None)) + self.actionConfig.setShortcut(QtWidgets.QApplication.translate("MainWindow", "F2", None)) diff --git a/ui/helpDialog.py b/ui/helpDialog.py new file mode 100644 index 00000000..94dfb341 --- /dev/null +++ b/ui/helpDialog.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtWidgets, QtGui +from app.auxiliary import * # for timestamps +from six import u as unicode +from ui.ancillaryDialog import flipState + +class License(QtWidgets.QPlainTextEdit): + def __init__(self,parent = None): + super(License, self).__init__(parent) + self.setReadOnly(True) + self.setWindowTitle('License') + self.setGeometry(0, 0, 300, 300) + self.center() + self.setPlainText(open('LICENSE','r').read()) + + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + +class ChangeLog(QtWidgets.QPlainTextEdit): + def __init__(self, qss, parent = None): + super(ChangeLog, self).__init__(parent) + self.setMinimumHeight(240) + self.setStyleSheet(qss) + self.setPlainText(open('CHANGELOG.txt','r').read()) + self.setReadOnly(True) + +class HelpDialog(QtWidgets.QDialog): + def __init__(self, name, author, copyright, links, emails, version, build, update, license, desc, smallIcon, + bigIcon, qss, parent = None): + super(HelpDialog, self).__init__(parent) + self.name = name + self.author = author + self.copyright = copyright + self.links = links + self.emails = emails + self.version = version + self.build = build + self.update = update + self.desc = QtWidgets.QLabel(desc) + self.smallIcon = smallIcon + self.bigIcon = bigIcon + self.qss = qss + self.setWindowTitle("About {0}".format(self.name)) + self.Main = QtWidgets.QVBoxLayout() + self.frm = QtWidgets.QFormLayout() + self.setGeometry(0, 0, 350, 400) + self.center() + self.Qui_update() + self.setStyleSheet(self.qss) + + def center(self): + frameGm = self.frameGeometry() + centerPoint = QtGui.QGuiApplication.primaryScreen().availableGeometry().center() + frameGm.moveCenter(centerPoint) + self.move(frameGm.topLeft()) + + def Qui_update(self): + self.logoapp = QtWidgets.QLabel('') + self.logoapp.setPixmap(QtGui.QPixmap(self.smallIcon).scaled(64,64)) + self.form = QtWidgets.QFormLayout() + self.form2 = QtWidgets.QVBoxLayout() + self.form.addRow(self.logoapp,QtWidgets.QLabel('

    {0} {1}-{2}

    '.format(self.name, + self.version, self.build))) + self.tabwid = QtWidgets.QTabWidget(self) + self.TabAbout = QtWidgets.QWidget(self) + self.TabVersion = QtWidgets.QWidget(self) + self.TabChangelog = QtWidgets.QWidget(self) + self.cmdClose = QtWidgets.QPushButton("Close") + self.cmdClose.setFixedWidth(90) + self.cmdClose.setIcon(QtGui.QIcon('images/close.png')) + self.cmdClose.clicked.connect(self.close) + + self.formAbout = QtWidgets.QFormLayout() + self.formVersion = QtWidgets.QFormLayout() + self.formChange = QtWidgets.QFormLayout() + + # About section + self.formAbout.addRow(self.desc) + self.formAbout.addRow(QtWidgets.QLabel('
    ')) + self.formAbout.addRow(QtWidgets.QLabel('Last Update:')) + self.formAbout.addRow(QtWidgets.QLabel(self.update + '
    ')) + self.formAbout.addRow(QtWidgets.QLabel('Feedback:')) + for link in self.links: + self.formAbout.addRow(QtWidgets.QLabel('{0}'.format(link))) + for email in self.emails: + self.formAbout.addRow(QtWidgets.QLabel(email)) + self.formAbout.addRow(QtWidgets.QLabel('
    ')) + self.formAbout.addRow(QtWidgets.QLabel(self.copyright + ' ' + self.author)) + self.gnu = QtWidgets.QLabel('License: GNU General Public License Version
    ') + self.gnu.linkActivated.connect(self.link) + self.formAbout.addRow(self.gnu) + self.TabAbout.setLayout(self.formAbout) + + # Version Section + self.formVersion.addRow(QtWidgets.QLabel('Version: {0}-{1}
    '.format(self.version, + self.build))) + self.formVersion.addRow(QtWidgets.QLabel('Using:')) + import platform + python_version = platform.python_version() + self.formVersion.addRow(QtWidgets.QLabel(''' +
      +
    • QTVersion: {0}
    • +
    • Python: {1}
    • +
    '''.format(QtCore.QT_VERSION_STR,python_version))) + self.TabVersion.setLayout(self.formVersion) + + # Changelog Section + self.formChange.addRow(ChangeLog(qss = self.qss)) + self.TabChangelog.setLayout(self.formChange) + + self.tabwid.addTab(self.TabAbout,'About') + self.tabwid.addTab(self.TabVersion,'Version') + self.tabwid.addTab(self.TabChangelog,'ChangeLog') + self.form.addRow(self.tabwid) + self.form2.addWidget(QtWidgets.QLabel('
    ')) + self.form2.addWidget(self.cmdClose, alignment = Qt.AlignmentFlag.AlignCenter) + self.form.addRow(self.form2) + self.Main.addLayout(self.form) + self.setLayout(self.Main) + + def link(self): + self.formLicense = License() + self.formLicense.show() diff --git a/ui/legion.qss b/ui/legion.qss new file mode 100644 index 00000000..9e3bd5b1 --- /dev/null +++ b/ui/legion.qss @@ -0,0 +1,13 @@ +QTabBar::close-button { + image: url(./images/closetab-small.png); + height: 10px; + width: 10px; +} + +QTabBar::close-button:hover { + image: url(./images/closetab-hover.png); +} + +QTabBar::close-button:pressed { + image: url(./images/closetab-press.png); +} diff --git a/ui/models/__init__.py b/ui/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/models/cvemodels.py b/ui/models/cvemodels.py new file mode 100644 index 00000000..f89624c7 --- /dev/null +++ b/ui/models/cvemodels.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import re +from typing import Dict + +from PyQt6 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders, itemInteractive +from app.auxiliary import * # for bubble sort + +class CvesTableModel(QtCore.QAbstractTableModel): + def __init__(self, controller, cves = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__cves = cves + self.__controller = controller + self.columnMapping = { + 0: "name", + 1: "severity", + 2: "product", + 3: "version", + 4: "url", + 5: "source", + 6: "exploitId", + 7: "exploit", + 8: "exploitUrl" + } + + def setCves(self, cves): + self.__cves = cves + + def getCves(self): + return self.__cves + + def rowCount(self, parent): + return len(self.__cves) + + def columnCount(self, parent): + if len(self.__cves) != 0: + return len(self.__cves[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + def data(self, index, role): # this method takes care of how the information is displayed + if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: # how to display each cell + row = index.row() + column = index.column() + return self.__cves[row][self.columnMapping[column]] + + def sort(self, Ncol, order): + self.layoutAboutToBeChanged.emit() + + array = [] + for i in range(len(self.__cves)): + array.append(self.__cves[i][self.columnMapping[Ncol]]) + + sortArrayWithArray(array, self.__cves) # sort the services based on the values in the array + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__cves.reverse() + + self.layoutChanged.emit() + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return itemInteractive() + + ### getter functions ### + + def getCveDBIdForRow(self, row): + return self.__cves[row]['name'] + + def getCveForRow(self, row): + return self.__cves[row] + + def getRowForDBId(self, id): + for i in range(len(self.__cves)): + if self.__cves[i]['name'] == id: + return i diff --git a/ui/models/hostmodels.py b/ui/models/hostmodels.py new file mode 100644 index 00000000..043e682c --- /dev/null +++ b/ui/models/hostmodels.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import re +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtGui import QFont +from PyQt6.QtCore import pyqtSignal, QObject + +from app.ModelHelpers import resolveHeaders, itemSelectable +from app.auxiliary import * # for bubble sort + + +class HostsTableModel(QtCore.QAbstractTableModel): + + def __init__(self, hosts = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__hosts = hosts + + def setHosts(self, hosts): + self.__hosts = hosts + + def rowCount(self, parent): + return len(self.__hosts) + + def columnCount(self, parent): + if len(self.__hosts) != 0: + return len(self.__hosts[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + def data(self, index, role): # this method takes care of how the information is displayed + if role == QtCore.Qt.ItemDataRole.DecorationRole: # to show the operating system icon instead of text + if index.column() == 1: # if trying to display the operating system + os_string = self.__hosts[index.row()]['osMatch'] + if os_string == '': # if there is no OS information, use the question mark icon + return QtGui.QIcon("./images/question-icon.png") + + elif re.search('[lL]inux', os_string, re.I): + return QtGui.QIcon("./images/linux-icon.png") + + elif re.search('[wW]indows', os_string, re.I): + return QtGui.QIcon("./images/windows-icon.png") + + elif re.search('[cC]isco', os_string, re.I): + return QtGui.QIcon("./images/cisco-big.jpg") + + elif re.search('HP ', os_string, re.I): + return QtGui.QIcon("./images/hp-icon.png") + + elif re.search('[vV]x[wW]orks', os_string, re.I): + return QtGui.QIcon("./images/hp-icon.png") + + elif re.search('[vV]m[wW]are', os_string, re.I): + return QtGui.QIcon("./images/vmware-big.jpg") + + else: # if it's an unknown OS also use the question mark icon + return QtGui.QIcon("./images/question-icon.png") + + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + if column == 0: + value = self.__hosts[row]['id'] + elif column == 2: + value = self.__hosts[row]['osAccuracy'] + elif column == 3: + if not self.__hosts[row]['hostname'] == '': + value = self.__hosts[row]['ip'] + ' ('+ self.__hosts[row]['hostname'] +')' + else: + value = self.__hosts[row]['ip'] + elif column == 4: + value = self.__hosts[row]['ipv4'] + elif column == 5: + value = self.__hosts[row]['ipv6'] + elif column == 6: + value = self.__hosts[row]['macaddr'] + elif column == 7: + value = self.__hosts[row]['status'] + elif column == 8: + value = self.__hosts[row]['hostname'] + elif column == 9: + value = self.__hosts[row]['vendor'] + elif column == 10: + value = self.__hosts[row]['uptime'] + elif column == 11: + value = self.__hosts[row]['lastboot'] + elif column == 12: + value = self.__hosts[row]['distance'] + elif column == 13: + value = self.__hosts[row]['checked'] + elif column == 14: + value = self.__hosts[row]['state'] + elif column == 15: + value = self.__hosts[row]['count'] + else: + value = 'Not set in view model' + return value + + if role == QtCore.Qt.ItemDataRole.FontRole: + # if a host is checked strike it out and make it italic + if index.column() == 3 and self.__hosts[index.row()]['checked'] == 'True': + checkedFont=QFont() + checkedFont.setStrikeOut(True) + checkedFont.setItalic(True) + return checkedFont + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return itemSelectable() + + # sort function called when the user clicks on a header + def sort(self, Ncol, order): + + self.layoutAboutToBeChanged.emit() + array = [] + + if Ncol == 0 or Ncol == 3: # if sorting by IP address (and by default) + log.debug("__hosts: {0}".format(str(self.__hosts))) + for i in range(len(self.__hosts)): + array.append(IP2Int(self.__hosts[i]['ip'])) + + elif Ncol == 1: # if sorting by OS + for i in range(len(self.__hosts)): + + os_string = self.__hosts[i]['osMatch'] + if os_string == '': + array.append('') + + elif re.search('[lL]inux', os_string, re.I): + array.append('Linux') + + elif re.search('[wW]indows', os_string, re.I): + array.append('Windows') + + elif re.search('[cC]isco', os_string, re.I): + array.append('Cisco') + + elif re.search('HP ', os_string, re.I): + array.append('Hp') + + elif re.search('[vV]x[wW]orks', os_string, re.I): + array.append('Hp') + + elif re.search('[vV]m[wW]are', os_string, re.I): + array.append('Vmware') + + else: + array.append('') + + sortArrayWithArray(array, self.__hosts) # sort the array of OS + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__hosts.reverse() + + self.layoutChanged.emit() # update the UI (built-in signal) + + ### getter functions ### + + def getHostIPForRow(self, row): + return self.__hosts[row]['ip'] + + def getHostIdForRow(self, row): + return self.__hosts[row]['id'] + + def getHostCheckStatusForRow(self, row): + return self.__hosts[row]['checked'] + + def getHostCheckStatusForIp(self, ip): + for i in range(len(self.__hosts)): + if str(self.__hosts[i]['ip']) == str(ip): + return self.__hosts[i]['checked'] + + def getRowForIp(self, ip): + for i in range(len(self.__hosts)): + if self.__hosts[i]['ip'] == ip: + return i diff --git a/ui/models/processmodels.py b/ui/models/processmodels.py new file mode 100644 index 00000000..3d2622f5 --- /dev/null +++ b/ui/models/processmodels.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import re +from PyQt6 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders, itemInteractive +from app.auxiliary import * # for bubble sort + +class ProcessesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, controller, processes = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__processes = processes + self.__controller = controller + + def setProcesses(self, processes): + self.__processes = processes + + def getProcesses(self): + return self.__processes + + def rowCount(self, parent): + return len(self.__processes) + + def columnCount(self, parent): + if len(self.__processes) != 0: + return len(self.__processes[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + # this method takes care of how the information is displayed + def data(self, index, role): + if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + processColumns = {0: 'progress', 1: 'display', 2: 'elapsed', 3: 'estimatedRemaining', + 4: 'pid', 5: 'name', 6: 'tabTitle', 7: 'hostIp', 8: 'port', 9: 'protocol', 10: 'command', + 11: 'startTime', 12: 'endTime', 13: 'outputfile', 14: 'output', 15: 'status', + 16: 'closed'} + try: + if column == 0: + value = '' + elif column == 2: + pid = int(self.__processes[row]['pid']) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) + value = "{0:.2f}{1}".format(float(elapsed), "s") + elif column == 3: + status = str(self.__processes[row]['status']) + if status == "Finished" or status == "Crashed" or status == "Killed": + estimatedRemaining = 0 + else: + pid = int(self.__processes[row]['pid']) + elapsed = round(self.__controller.controller.processMeasurements.get(pid, 0), 2) + estimatedRemaining = int(self.__processes[row]['estimatedRemaining']) - float(elapsed) + value = "{0:.2f}{1}"\ + .format(float(estimatedRemaining), "s") if estimatedRemaining >= 0 else 'Unknown' + elif column == 6: + if not self.__processes[row]['tabTitle'] == '': + value = self.__processes[row]['tabTitle'] + else: + value = self.__processes[row]['name'] + elif column == 8: + if not self.__processes[row]['port'] == '' and not self.__processes[row]['protocol'] == '': + value = self.__processes[row]['port'] + '/' + self.__processes[row]['protocol'] + else: + value = self.__processes[row]['port'] + elif column == 16: + value = "" + else: + try: + value = self.__processes[row][processColumns.get(int(column))] + except: + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) + pass + except Exception: + value = "Missing data c #{0} - {1}".format(int(column), processColumns.get(int(column))) + pass + return value + + def sort(self, Ncol, order): + self.layoutAboutToBeChanged.emit() + array=[] + + sortColumns = {5:'name', 6:'tabTitle', 11:'startTime', 12:'endTime'} + field = sortColumns.get(int(Ncol)) or 'status' + + try: + if Ncol == 7: + for i in range(len(self.__processes)): + array.append(IP2Int(self.__processes[i]['hostIp'])) + + elif Ncol == 8: + for i in range(len(self.__processes)): + if self.__processes[i]['port'] == '': + return + else: + array.append(int(self.__processes[i]['port'])) + else: + for i in range(len(self.__processes)): + array.append(self.__processes[i][field]) + + sortArrayWithArray(array, self.__processes) # sort the services based on the values in the array + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__processes.reverse() + self.__controller.processesTableViewSort = 'desc' + else: + self.__controller.processesTableViewSort = 'asc' + + self.__controller.processesTableViewSortColumn = field + + ## Extra? + #self.__controller.updateProcessesIcon() # to make sure the progress GIF is displayed in the right place + self.layoutChanged.emit() + except: + log.error("Failed to sort") + pass + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return itemInteractive() + + def setDataList(self, processes): + self.__processes = processes + self.layoutAboutToBeChanged.emit() + self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(self.rowCount(0), self.columnCount(0))) + self.layoutChanged.emit() + + ### getter functions ### + + def getProcessPidForRow(self, row): + return self.__processes[row]['pid'] + + def getProcessPidForId(self, dbId): + for i in range(len(self.__processes)): + if str(self.__processes[i]['id']) == str(dbId): + return self.__processes[i]['pid'] + + def getProcessStatusForRow(self, row): + return self.__processes[row]['status'] + + def getProcessStatusForPid(self, pid): + for i in range(len(self.__processes)): + if str(self.__processes[i]['pid']) == str(pid): + return self.__processes[i]['status'] + + def getProcessStatusForId(self, dbId): + for i in range(len(self.__processes)): + if str(self.__processes[i]['id']) == str(dbId): + return self.__processes[i]['status'] + + def getProcessIdForRow(self, row): + return self.__processes[row]['id'] + + def getToolNameForRow(self, row): + return self.__processes[row]['name'] + + def getRowForToolName(self, toolname): + for i in range(len(self.__processes)): + if self.__processes[i]['name'] == toolname: + return i + + def getRowForDBId(self, dbid): # new + for i in range(len(self.__processes)): + if self.__processes[i]['id'] == dbid: + return i + + def getIpForRow(self, row): + return self.__processes[row]['hostIp'] + + def getPortForRow(self, row): + return self.__processes[row]['port'] + + def getProtocolForRow(self, row): + return self.__processes[row]['protocol'] + + def getOutputfileForRow(self, row): + return self.__processes[row]['outputfile'] diff --git a/ui/models/scriptmodels.py b/ui/models/scriptmodels.py new file mode 100644 index 00000000..7e192cc0 --- /dev/null +++ b/ui/models/scriptmodels.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import re +from PyQt6 import QtWidgets, QtGui, QtCore + +from app.ModelHelpers import resolveHeaders, itemSelectable +from app.auxiliary import * # for bubble sort + +class ScriptsTableModel(QtCore.QAbstractTableModel): + + def __init__(self, controller, scripts = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__scripts = scripts + self.__controller = controller + + def setScripts(self, scripts): + self.__scripts = scripts + + def getScripts(self): + return self.__scripts + + def rowCount(self, parent): + return len(self.__scripts) + + def columnCount(self, parent): + if len(self.__scripts) != 0: + return len(self.__scripts[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + def data(self, index, role): # this method takes care of how the information is displayed + + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + + if column == 0: + value = self.__scripts[row]['id'] + elif column == 1: + value = self.__scripts[row]['scriptId'] + elif column == 2: + if self.__scripts[row]['portId'] and self.__scripts[row]['protocol'] and \ + not self.__scripts[row]['portId'] == '' and not self.__scripts[row]['protocol'] == '': + value = self.__scripts[row]['portId'] + '/' + self.__scripts[row]['protocol'] + else: + value = '' + elif column == 3: + value = self.__scripts[row]['protocol'] + return value + + + def sort(self, Ncol, order): + self.layoutAboutToBeChanged.emit() + array=[] + + if Ncol == 1: + for i in range(len(self.__scripts)): + array.append(self.__scripts[i]['scriptId']) + if Ncol == 2: + for i in range(len(self.__scripts)): + array.append(int(self.__scripts[i]['portId'])) + + sortArrayWithArray(array, self.__scripts) # sort the services based on the values in the array + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__scripts.reverse() + + self.layoutChanged.emit() + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return itemSelectable() + + ### getter functions ### + + def getScriptDBIdForRow(self, row): + return self.__scripts[row]['id'] + + def getRowForDBId(self, id): + for i in range(len(self.__scripts)): + if self.__scripts[i]['id'] == id: + return i diff --git a/ui/models/servicemodels.py b/ui/models/servicemodels.py new file mode 100644 index 00000000..5b1857cc --- /dev/null +++ b/ui/models/servicemodels.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtCore import pyqtSignal, QObject + +from app.ModelHelpers import resolveHeaders, itemInteractive +from app.auxiliary import * # for bubble sort + +class ServicesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, services = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__services = services + + def setServices(self, services): + self.__services = services + + def rowCount(self, parent): + return len(self.__services) + + def columnCount(self, parent): + if len(self.__services) != 0: + return len(self.__services[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + # this method takes care of how the information is displayed + def data(self, index, role): + if role == QtCore.Qt.ItemDataRole.DecorationRole: # to show the open/closed/filtered icons + if index.column() == 0 or index.column() == 2: + tmp_state = self.__services[index.row()]['state'] + + stateMap = {'open':'open', 'closed':'closed', 'filtered':'filtered'} + defaultState = 'filtered' + + stateIconName = stateMap.get(str(tmp_state)) or defaultState + stateIcon = "./images/{stateIconName}.gif".format(stateIconName=stateIconName) + return QtGui.QIcon(stateIcon) + + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell + value = '' + row = index.row() + column = index.column() + + if column == 0: + # the spaces are needed for spacing with the icon that precedes the text + value = ' ' + self.__services[row]['ip'] + elif column == 1: + value = self.__services[row]['portId'] + elif column == 2: + # the spaces are needed for spacing with the icon that precedes the text + value = ' ' + self.__services[row]['portId'] + elif column == 3: + value = self.__services[row]['protocol'] + elif column == 4: + value = self.__services[row]['state'] + elif column == 5: + value = self.__services[row]['hostId'] + elif column == 6: + value = self.__services[row]['serviceId'] + elif column == 7: + value = self.__services[row]['name'] + elif column == 8: + value = self.__services[row]['product'] + elif column == 9: + if not self.__services[row]['product'] == None and not self.__services[row]['product'] == '': + value = str(self.__services[row]['product']) + + if not self.__services[row]['version'] == None and not self.__services[row]['version'] == '': + value = value + ' ' + self.__services[row]['version'] + + if not self.__services[row]['extrainfo'] == None and not self.__services[row]['extrainfo'] == '': + value = value + ' (' + self.__services[row]['extrainfo'] + ')' + elif column == 10: + value = self.__services[row]['extrainfo'] + elif column == 11: + value = self.__services[row]['fingerprint'] + return value + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return itemInteractive() + + # sort function called when the user clicks on a header + def sort(self, Ncol, order): + self.layoutAboutToBeChanged.emit() + array = [] + + if Ncol == 0: # if sorting by ip (and by default) + for i in range(len(self.__services)): + array.append(IP2Int(self.__services[i]['ip'])) + + elif Ncol == 1: # if sorting by port + for i in range(len(self.__services)): + array.append(int(self.__services[i]['portId'])) + + elif Ncol == 2: # if sorting by port + for i in range(len(self.__services)): + array.append(int(self.__services[i]['portId'])) + + elif Ncol == 3: # if sorting by protocol + for i in range(len(self.__services)): + array.append(self.__services[i]['protocol']) + + elif Ncol == 4: # if sorting by state + for i in range(len(self.__services)): + array.append(self.__services[i]['state']) + + elif Ncol == 7: # if sorting by name + for i in range(len(self.__services)): + array.append(self.__services[i]['name']) + + elif Ncol == 9: # if sorting by version + for i in range(len(self.__services)): + value = '' + if not self.__services[i]['product'] == None and not self.__services[i]['product'] == '': + value = str(self.__services[i]['product']) + + if not self.__services[i]['version'] == None and not self.__services[i]['version'] == '': + value = value + ' ' + self.__services[i]['version'] + + if not self.__services[i]['extrainfo'] == None and not self.__services[i]['extrainfo'] == '': + value = value + ' (' + self.__services[i]['extrainfo'] + ')' + array.append(value) + + # sort the services based on the values in the array + sortArrayWithArray(array, self.__services) + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__services.reverse() + + self.layoutChanged.emit() # update the UI (built-in signal) + + ### getter functions ### + + def getPortForRow(self, row): + return self.__services[row]['portId'] + + def getServiceNameForRow(self, row): + return self.__services[row]['name'] + + def getIpForRow(self, row): + return self.__services[row]['ip'] + + def getProtocolForRow(self, row): + return self.__services[row]['protocol'] + + #################################################################### + +class ServiceNamesTableModel(QtCore.QAbstractTableModel): + + def __init__(self, serviceNames = [[]], headers = [], parent = None): + QtCore.QAbstractTableModel.__init__(self, parent) + self.__headers = headers + self.__serviceNames = serviceNames + + def setServices(self, serviceNames): + self.__serviceNames = serviceNames + + def rowCount(self, parent): + return len(self.__serviceNames) + + def columnCount(self, parent): + if len(self.__serviceNames) != 0: + return len(self.__serviceNames[0]) + return 0 + + def headerData(self, section, orientation, role): + return resolveHeaders(role, orientation, section, self.__headers) + + def data(self, index, role): # This method takes care of how the information is displayed + + if role == QtCore.Qt.ItemDataRole.DisplayRole: # how to display each cell + row = index.row() + column = index.column() + if column == 0: + return self.__serviceNames[row]['name'] + + # method that allows views to know how to treat each item, eg: if it should be enabled, editable, selectable etc + def flags(self, index): + return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEditable + + # sort function called when the user clicks on a header + def sort(self, Ncol, order): + + self.layoutAboutToBeChanged.emit() + array = [] + + if Ncol == 0: # if sorting by service name (and by default) + for i in range(len(self.__serviceNames)): + array.append(self.__serviceNames[i]['name']) + + # sort the services based on the values in the array + sortArrayWithArray(array, self.__serviceNames) + + if order == Qt.SortOrder.AscendingOrder: # reverse if needed + self.__serviceNames.reverse() + + self.layoutChanged.emit() # update the UI (built-in signal) + + ### getter functions ### + + def getServiceNameForRow(self, row): + return self.__serviceNames[row]['name'] + + def getRowForServiceName(self, serviceNames): + for i in range(len(self.__serviceNames)): + if self.__serviceNames[i]['name'] == serviceNames: + return i diff --git a/ui/observers/QtUpdateProgressObserver.py b/ui/observers/QtUpdateProgressObserver.py new file mode 100644 index 00000000..74c945da --- /dev/null +++ b/ui/observers/QtUpdateProgressObserver.py @@ -0,0 +1,35 @@ +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +Author(s): Shane Scott (sscott@shanewilliamscott.com), Dmitriy Dubson (d.dubson@gmail.com) +""" +from app.actions.updateProgress.AbstractUpdateProgressObserver import AbstractUpdateProgressObserver +from ui.ancillaryDialog import ProgressWidget + + +class QtUpdateProgressObserver(AbstractUpdateProgressObserver): + def __init__(self, progressWidget: ProgressWidget): + self.progressWidget = progressWidget + + def onStart(self) -> None: + self.progressWidget.show() + + def onFinished(self) -> None: + self.progressWidget.hide() + + def onProgressUpdate(self, progress: int, title: str) -> None: + self.progressWidget.setText(title) + self.progressWidget.setProgress(progress) + self.progressWidget.show() diff --git a/ui/settingsDialog.py b/ui/settingsDialog.py new file mode 100644 index 00000000..bd957850 --- /dev/null +++ b/ui/settingsDialog.py @@ -0,0 +1,1628 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . + +""" + +import os +from PyQt6.QtGui import * # for filters dialog +from PyQt6.QtWidgets import * +from PyQt6 import QtCore, QtWidgets +from app.auxiliary import * # for timestamps +from app.shell.Shell import Shell + +log = getAppLogger() + +# used to validate user input on focusOut - more specifically only called to validate tool name in host/port/terminal +# commands tabs +class Validate(QtCore.QObject): + def eventFilter(self, widget, event): + # this horrible line is to avoid making the 'AddSettingsDialog' class visible from here + if event.type() == QtCore.QEvent.Type.FocusOut: + widget.parent().parent().parent().parent().parent().parent().validateToolName() + return False + else: + return False # TODO: check this + +# Borrowed this class from https://gist.github.com/LegoStormtroopr/5075267 +# Credit and thanks to LegoStormtroopr (http://www.twitter.com/legostormtroopr) +class SettingsTabBarWidget(QtWidgets.QTabBar): + def __init__(self, parent=None, *args, **kwargs): + self.tabSize = QtCore.QSize(kwargs.pop('width',100), kwargs.pop('height',25)) + QtWidgets.QTabBar.__init__(self, parent, *args, **kwargs) + + def paintEvent(self, event): + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionTab() + + for index in range(self.count()): + self.initStyleOption(option, index) + tabRect = self.tabRect(index) + tabRect.moveLeft(10) + painter.drawControl(QtWidgets.QStyle.ControlElement.CE_TabBarTabShape, option) + painter.drawText(tabRect, QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.TextFlag.TextDontClip, self.tabText(index)) + painter.end() + + def tabSizeHint(self,index): + return self.tabSize + +class AddSettingsDialog(QtWidgets.QDialog): # dialog shown when the user selects settings menu + def __init__(self, shell: Shell, parent=None): + QtWidgets.QDialog.__init__(self, parent) + + self.setupLayout() + self.setupConnections() + + self.validationPassed = True # TODO: rethink + self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) + + self.validate = Validate() + self.hostActionNameText.installEventFilter(self.validate) + self.portActionNameText.installEventFilter(self.validate) + self.terminalActionNameText.installEventFilter(self.validate) + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + self.shell = shell + + # TODO: maybe these shouldn't be hardcoded because the user can change them... rethink this? + self.defaultServicesList = ["mysql-default","mssql-default","ftp-default","postgres-default","oracle-default"] + + def setupConnections(self): + self.browseUsersListButton.clicked.connect(lambda: self.wordlistDialog()) + self.browsePasswordsListButton.clicked.connect(lambda: self.wordlistDialog('Choose password path')) + + self.addToolForHostButton.clicked.connect(self.addToolForHost) + self.removeToolForHostButton.clicked.connect(self.removeToolForHost) + self.addToolButton.clicked.connect(self.addToolForService) + self.removeToolButton.clicked.connect(self.removeToolForService) + self.addToolForTerminalButton.clicked.connect(self.addToolForTerminal) + self.removeToolForTerminalButton.clicked.connect(self.removeToolForTerminal) + + self.addServicesButton.clicked.connect(lambda: self.moveService(self.servicesAllTableWidget, + self.servicesActiveTableWidget)) + self.removeServicesButton.clicked.connect(lambda: self.moveService(self.servicesActiveTableWidget, + self.servicesAllTableWidget)) + self.addTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesAllTable, + self.terminalServicesActiveTable)) + self.removeTerminalServiceButton.clicked.connect(lambda: self.moveService(self.terminalServicesActiveTable, + self.terminalServicesAllTable)) + + self.toolForHostsTableWidget.clicked.connect(self.updateToolForHostInformation) + self.toolForServiceTableWidget.clicked.connect(self.updateToolForServiceInformation) + self.toolForTerminalTableWidget.clicked.connect(self.updateToolForTerminalInformation) + + self.hostActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForHostsTableWidget, + self.hostActionNameText.text())) + self.portActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate(self.toolForServiceTableWidget, + self.portActionNameText.text())) + self.terminalActionNameText.textChanged.connect(lambda: self.realTimeToolNameUpdate( + self.toolForTerminalTableWidget, self.terminalActionNameText.text())) + + self.enableAutoAttacks.clicked.connect(lambda: self.enableAutoToolsTab()) + self.checkDefaultCred.clicked.connect(self.toggleDefaultServices) + + self.settingsTabWidget.currentChanged.connect(self.switchTabClick) + self.ToolSettingsTab.currentChanged.connect(self.switchToolTabClick) + + ##################### ACTION FUNCTIONS (apply / cancel related) ##################### + + # called by the controller once the config file has been read at start time and also when the cancel button + # is pressed to forget any changes. + def setSettings(self, settings): + self.settings = settings + self.resetGui() # clear any changes the user may have made and canceled. + self.populateSettings() # populate the GUI with the new settings + + # TODO: this is most likely not the best way to do it. we should check if New_Action_1 exists and if so + # increase the number until it doesn't exist. no need for a self.variable - can be a local one. + self.hostActionsNumber = 1 + self.portActionsNumber = 1 + self.terminalActionsNumber = 1 + + def applySettings(self): # called when apply button is pressed + if self.validateCurrentTab(self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex())): + self.updateSettings() + return True + return False + + # updates the local settings object (must be called when applying settings and only after validation succeeded) + def updateSettings(self): + # LEO: reorganised stuff in a more logical way but no changes were made yet :) + # update GENERAL tab settings + self.settings.general_default_terminal = str(self.terminalComboBox.currentText()) + self.settings.general_max_fast_processes = str(self.fastProcessesComboBox.currentText()) + self.settings.general_screenshooter_timeout = str(self.screenshotTextinput.text()) + self.settings.general_web_services = str(self.webServicesTextinput.text()) + + if self.checkStoreClearPW.isChecked(): + self.settings.brute_store_cleartext_passwords_on_exit = 'True' + else: + self.settings.brute_store_cleartext_passwords_on_exit = 'False' + + if self.checkBlackBG.isChecked(): + self.settings.general_tool_output_black_background = 'True' + else: + self.settings.general_tool_output_black_background = 'False' + + # update BRUTE tab settings + self.settings.brute_username_wordlist_path = str(self.userlistPath.text()) + self.settings.brute_password_wordlist_path = str(self.passwordlistPath.text()) + self.settings.brute_default_username = str(self.defaultUserText.text()) + self.settings.brute_default_password = str(self.defaultPassText.text()) + + # update TOOLS tab settings + self.settings.tools_nmap_stage1_ports = str(self.stage1Input.text()) + self.settings.tools_nmap_stage2_ports = str(self.stage2Input.text()) + self.settings.tools_nmap_stage3_ports = str(self.stage3Input.text()) + self.settings.tools_nmap_stage4_ports = str(self.stage4Input.text()) + self.settings.tools_nmap_stage5_ports = str(self.stage5Input.text()) + + # update AUTOMATED ATTACKS tab settings + if self.enableAutoAttacks.isChecked(): + self.settings.general_enable_scheduler = 'True' + else: + self.settings.general_enable_scheduler = 'False' + + # TODO: seems like all the other settings should be updated here as well instead of + # updating them in the validation function. + + # LEO: renamed and changed the previous tabs defaults otherwise validation doesn't work the first time + #def initValues(self): + def resetGui(self): # called when the cancel button is clicked, to initialise everything + self.validationPassed = True + self.previousTab = 'General' + self.previousToolTab = 'Tool Paths' + self.hostTableRow = -1 + self.portTableRow = -1 + self.terminalTableRow = -1 + + self.hostActionNameText.setText('') + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + self.hostCommandText.setText('init value') + + self.portActionNameText.setText('') + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.portCommandText.setText('init value') + + self.terminalActionNameText.setText('') + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.terminalCommandText.setText('init value') + + # reset layouts + clearLayout(self.scrollVerLayout) + clearLayout(self.defaultBoxVerlayout) + self.terminalComboBox.clear() + + # called by setSettings at start up or when showing the settings dialog after a cancel action. + # it populates the GUI with the controller's settings object. + def populateSettings(self): + self.populateGeneralTab() # LEO: split it in functions so that it's less confusing and easier to refactor later + self.populateBruteTab() + self.populateToolsTab() + self.populateAutomatedAttacksTab() + + def populateGeneralTab(self): + self.terminalsSupported = ['gnome-terminal','xterm'] + self.terminalComboBox.insertItems(0, self.terminalsSupported) + + self.fastProcessesComboBox.setCurrentIndex(int(self.settings.general_max_fast_processes) - 1) + self.screenshotTextinput.setText(str(self.settings.general_screenshooter_timeout)) + self.webServicesTextinput.setText(str(self.settings.general_web_services)) + + if self.settings.general_tool_output_black_background == 'True' and self.checkBlackBG.isChecked() == False: + self.checkBlackBG.toggle() + elif self.settings.general_tool_output_black_background == 'False' and self.checkBlackBG.isChecked() == True: + self.checkBlackBG.toggle() + + if self.settings.brute_store_cleartext_passwords_on_exit == 'True' and \ + self.checkStoreClearPW.isChecked() == False: + self.checkStoreClearPW.toggle() + elif self.settings.brute_store_cleartext_passwords_on_exit == 'False' and \ + self.checkStoreClearPW.isChecked() == True: + self.checkStoreClearPW.toggle() + + def populateBruteTab(self): + self.userlistPath.setText(self.settings.brute_username_wordlist_path) + self.passwordlistPath.setText(self.settings.brute_password_wordlist_path) + self.defaultUserText.setText(self.settings.brute_default_username) + self.defaultPassText.setText(self.settings.brute_default_password) + + def populateToolsTab(self): + # POPULATE TOOL PATHS TAB + self.nmapPathInput.setText(self.settings.tools_path_nmap) + self.hydraPathInput.setText(self.settings.tools_path_hydra) + self.cutycaptPathInput.setText(self.settings.tools_path_cutycapt) + self.textEditorPathInput.setText(self.settings.tools_path_texteditor) + + # POPULATE STAGED NMAP TAB + self.stage1Input.setText(self.settings.tools_nmap_stage1_ports) + self.stage2Input.setText(self.settings.tools_nmap_stage2_ports) + self.stage3Input.setText(self.settings.tools_nmap_stage3_ports) + self.stage4Input.setText(self.settings.tools_nmap_stage4_ports) + self.stage5Input.setText(self.settings.tools_nmap_stage5_ports) + + # POPULATE TOOLS TABS (HOST/PORT/TERMINAL) + self.toolForHostsTableWidget.setRowCount(len(self.settings.hostActions)) + for row in range(len(self.settings.hostActions)): + # add a row to the table + self.toolForHostsTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + # add the label for the port actions + self.toolForHostsTableWidget.item(row, 0).setText(self.settings.hostActions[row][1]) + + self.toolForServiceTableWidget.setRowCount(len(self.settings.portActions)) + for row in range(len(self.settings.portActions)): + self.toolForServiceTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.item(row, 0).setText(self.settings.portActions[row][1]) + + self.servicesAllTableWidget.setRowCount(len(self.settings.portActions)) + for row in range(len(self.settings.portActions)): + self.servicesAllTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.servicesAllTableWidget.item(row, 0).setText(self.settings.portActions[row][3]) + + self.toolForTerminalTableWidget.setRowCount(len(self.settings.portTerminalActions)) + for row in range(len(self.settings.portTerminalActions)): + # add a row to the table + self.toolForTerminalTableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem()) + # add the label fro the port actions + self.toolForTerminalTableWidget.item(row, 0).setText(self.settings.portTerminalActions[row][1]) + self.terminalServicesAllTable.setRowCount(len(self.settings.portTerminalActions)) + for row in range(len(self.settings.portTerminalActions)): + self.terminalServicesAllTable.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesAllTable.item(row, 0).setText(self.settings.portTerminalActions[row][3]) + + def populateAutomatedAttacksTab(self): # TODO: this one is still to big and ugly. needs work. + self.typeDic = {} + for i in range(len(self.settings.portActions)): + # the dictionary contains the name, the text input and the layout for each tool + self.typeDic.update({self.settings.portActions[i][1]:[QtWidgets.QLabel(),QtWidgets.QLineEdit(), + QtWidgets.QCheckBox(),QtWidgets.QHBoxLayout()]}) + + for keyNum in range(len(self.settings.portActions)): + + # populate the automated attacks tools tab with every tool that is not a default creds check + if self.settings.portActions[keyNum][1] not in self.defaultServicesList: + + self.typeDic[self.settings.portActions[keyNum][1]][0].setText(self.settings.portActions[keyNum][1]) + self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) + + #if self.settings.portActions[keyNum][1] in self.settings.automatedAttacks.keys(): + foundToolInAA = False + for t in self.settings.automatedAttacks: + if self.settings.portActions[keyNum][1] == t[0]: + #self.typeDic[self.settings.portActions[keyNum][1]][1].setText( + # self.settings.automatedAttacks[self.settings.portActions[keyNum][1]]) + self.typeDic[self.settings.portActions[keyNum][1]][1].setText(t[1]) + self.typeDic[self.settings.portActions[keyNum][1]][2].toggle() + foundToolInAA = True + break + + if not foundToolInAA: + self.typeDic[self.settings.portActions[keyNum][1]][1].setText(self.settings.portActions[keyNum][3]) + + self.typeDic[self.settings.portActions[keyNum][1]][1].setFixedWidth(300) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName( + str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][1]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][2]) + self.scrollVerLayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) + + else: # populate the automated attacks tools tab with every tool that IS a default creds check + # TODO: i get the feeling we shouldn't be doing this in the else. + # the else could just skip the default ones and outside of the loop we can go through + # self.defaultServicesList and take care of these separately. + if self.settings.portActions[keyNum][1] == "mysql-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('mysql') + elif self.settings.portActions[keyNum][1] == "mssql-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('mssql') + elif self.settings.portActions[keyNum][1] == "ftp-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('ftp') + elif self.settings.portActions[keyNum][1] == "postgres-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('postgres') + elif self.settings.portActions[keyNum][1] == "oracle-default": + self.typeDic[self.settings.portActions[keyNum][1]][0].setText('oracle') + + self.typeDic[self.settings.portActions[keyNum][1]][0].setFixedWidth(150) + self.typeDic[self.settings.portActions[keyNum][1]][2].setObjectName( + str(self.typeDic[self.settings.portActions[keyNum][1]][2])) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][0]) + self.typeDic[self.settings.portActions[keyNum][1]][3].addItem(self.enabledSpacer) + self.typeDic[self.settings.portActions[keyNum][1]][3].addWidget( + self.typeDic[self.settings.portActions[keyNum][1]][2]) + + self.defaultBoxVerlayout.addLayout(self.typeDic[self.settings.portActions[keyNum][1]][3]) + + self.scrollArea.setWidget(self.scrollWidget) + self.globVerAutoToolsLayout.addWidget(self.scrollArea) + + ##################### SWITCH TAB FUNCTIONS ##################### + + # LEO: this function had duplicate code with validateCurrentTab(). so now we call that one. + def switchTabClick(self): + if self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) == 'Tools': + self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + + log.info('previous tab is: ' + str(self.previousTab)) + # LEO: we don't care about the return value in this case. it's just for debug. + if self.validateCurrentTab(self.previousTab): + log.info('validation succeeded! switching tab! yay!') + # save the previous tab for the next time we switch tabs. + # TODO: not sure this should be inside the IF but makes sense to me. no point in saving + # the previous if there is no change.. + self.previousTab = self.settingsTabWidget.tabText(self.settingsTabWidget.currentIndex()) + else: + log.info('nope! cannot let you switch tab! you fucked up!') + + def switchToolTabClick(self): # TODO: check for duplicate code. + if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': + self.toolForHostsTableWidget.selectRow(0) + self.updateToolForHostInformation(False) + + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Port Commands': + self.toolForServiceTableWidget.selectRow(0) + self.updateToolForServiceInformation(False) + + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Terminal Commands': + self.toolForTerminalTableWidget.selectRow(0) + self.updateToolForTerminalInformation(False) + + # LEO: I get the feeling the validation part could go into a validateCurrentToolTab() + # just like in the other switch tab function. + if self.previousToolTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.ToolSettingsTab.setCurrentIndex(0) + + elif self.previousToolTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.ToolSettingsTab.setCurrentIndex(1) + else: + self.updateHostActions() + + elif self.previousToolTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.ToolSettingsTab.setCurrentIndex(2) + else: + self.updatePortActions() + + elif self.previousToolTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, + self.terminalCommandText): + self.ToolSettingsTab.setCurrentIndex(3) + else: + self.updateTerminalActions() + + elif self.previousToolTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.ToolSettingsTab.setCurrentIndex(4) +# else: + # LEO: commented out because it didn't look right, please check! +# self.updateTerminalActions() + + self.previousToolTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + + ##################### AUXILIARY FUNCTIONS ##################### + + # LEO: renamed. i get the feeling this function is not necessary if we put this code somewhere else + # - eg: right before we apply/cancel. we'll see. + #def confInitState(self): + # called when the settings dialog is opened so that we always show the same tabs. + def resetTabIndexes(self): + self.settingsTabWidget.setCurrentIndex(0) + self.ToolSettingsTab.setCurrentIndex(0) + + # called by validation functions to display (or not) a red border around a text input widget when input is + # (in)valid. easier to change stylesheets in one place only. + def toggleRedBorder(self, widget, red=True): + if red: + widget.setStyleSheet("border: 1px solid red;") + else: + widget.setStyleSheet("border: 1px solid grey;") + + # LEO: I moved the really generic validation functions to the end of auxiliary.py and those are used by + # these slightly-less-generic ones. + # .. the difference is that these ones also take care of the IF/ELSE which was being duplicated all over the code. + # everything should be simpler now. + # note that I didn't use these everywhere because sometimes the IF/ELSE are not so straight-forward. + + def validateNumeric(self, widget): + if not validateNumeric(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateString(self, widget): + if not validateString(str(widget.text())): # TODO: this is too strict in some cases... + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateStringWithSpace(self, widget): + if not validateStringWithSpace(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validatePath(self, widget): + if not self.shell.isDirectory(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateFile(self, widget): + if not self.shell.isFile(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateCommandFormat(self, widget): + if not validateCommandFormat(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + def validateNmapPorts(self, widget): + if not validateNmapPorts(str(widget.text())): + self.toggleRedBorder(widget, True) + return False + else: + self.toggleRedBorder(widget, False) + return True + + ##################### VALIDATION FUNCTIONS (per tab) ##################### + # LEO: the functions are more or less in the same order as the tabs in the GUI (top-down and left-to-right) except + # for generic functions + + # LEO: your updateSettings() was split in 2. validateCurrentTab() and updateSettings() since they have + # different functionality. also, we now have a 'tab' parameter so that we can reuse the code in switchTabClick + # and avoid duplicate code. the tab parameter will either be the current or the previous tab depending where we + # call this from. + def validateCurrentTab(self, tab): + validationPassed = True + if tab == 'General': + if not self.validateGeneralTab(): + self.settingsTabWidget.setCurrentIndex(0) + validationPassed = False + + elif tab == 'Brute': + if not self.validateBruteTab(): + self.settingsTabWidget.setCurrentIndex(1) + validationPassed = False + + elif tab == 'Tools': + self.ToolSettingsTab.setCurrentIndex(0) + currentToolsTab = self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) + if currentToolsTab == 'Tool Paths': + if not self.toolPathsValidate(): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(0) + validationPassed = False + + elif currentToolsTab == 'Host Commands': + if not self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(1) + validationPassed = False + else: + self.updateHostActions() + + elif currentToolsTab == 'Port Commands': + if not self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(2) + validationPassed = False + else: + self.updatePortActions() + + elif currentToolsTab == 'Terminal Commands': + if not self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, + self.terminalCommandText): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(3) + validationPassed = False + else: + self.updateTerminalActions() + + elif currentToolsTab == 'Staged Nmap': + if not self.validateStagedNmapTab(): + self.settingsTabWidget.setCurrentIndex(2) + self.ToolSettingsTab.setCurrentIndex(4) + validationPassed = False + + else: + # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 1') + + elif tab == 'Wordlists': + log.info('Coming back from wordlists.') + + elif tab == 'Automated Attacks': + log.info('Coming back from automated attacks.') + + else: + # LEO: added this just to help when testing. we'll remove it later. + log.info('>>>> we should never be here. potential bug. 2') + + log.debug('Current tab is valid: ' + str(validationPassed)) + return validationPassed + + #def generalTabValidate(self): + def validateGeneralTab(self): + validationPassed = self.validateNumeric(self.screenshotTextinput) + + self.toggleRedBorder(self.webServicesTextinput, False) + # TODO: this is too strict! no spaces or comma allowed? we can clean up for the user in some simple cases. + # actually, i'm not sure we even need to split. + for service in str(self.webServicesTextinput.text()).split(','): + if not validateString(service): + self.toggleRedBorder(self.webServicesTextinput, True) + validationPassed = False + break + + return validationPassed + + #def bruteTabValidate(self): + # LEO: do NOT change the order of the AND statements otherwise validation may not take place + # if first condition is False + def validateBruteTab(self): + validationPassed = self.validatePath(self.userlistPath) + validationPassed = self.validatePath(self.passwordlistPath) and validationPassed + validationPassed = self.validateString(self.defaultUserText) and validationPassed + validationPassed = self.validateString(self.defaultPassText) and validationPassed + return validationPassed + + def toolPathsValidate(self): + validationPassed = self.validateFile(self.nmapPathInput) + validationPassed = self.validateFile(self.hydraPathInput) and validationPassed + validationPassed = self.validateFile(self.cutycaptPathInput) and validationPassed + validationPassed = self.validateFile(self.textEditorPathInput) and validationPassed + return validationPassed + +# def commandTabsValidate(self): + # only validates the tool name, label and command fields for host/port/terminal tabs + def validateCommandTabs(self, nameInput, labelInput, commandInput): + validationPassed = True + + if self.validationPassed == False: # the self.validationPassed comes from the focus out event + # TODO: this seems like a dodgy way to do it - functions should not depend on hope :). + # maybe it's better to simply validate again. code will be clearer too. + self.toggleRedBorder(nameInput, True) + validationPassed = False + else: + self.toggleRedBorder(nameInput, False) + + validationPassed = self.validateStringWithSpace(labelInput) and validationPassed + validationPassed = self.validateCommandFormat(commandInput) and validationPassed + return validationPassed + + # avoid using the same code for the selected tab. returns the fields for the current visible tab + # (host/ports/terminal) + # TODO: don't like this too much. seems like we could just use parameters in the validate tool name function + def selectGroup(self): + tabSelected = -1 + + if self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Host Commands': + tabSelected = 1 + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Port Commands': + tabSelected = 2 + elif self.ToolSettingsTab.tabText(self.ToolSettingsTab.currentIndex()) == 'Terminal Commands': + tabSelected = 3 + + if self.previousToolTab == 'Host Commands' or tabSelected == 1: + tmpWidget = self.toolForHostsTableWidget + tmpActionLineEdit = self.hostActionNameText + tmpLabelLineEdit = self.hostLabelText + tmpCommandLineEdit = self.hostCommandText + actions = self.settings.hostActions + tableRow = self.hostTableRow + if self.previousToolTab == 'Port Commands' or tabSelected == 2: + tmpWidget = self.toolForServiceTableWidget + tmpActionLineEdit = self.portActionNameText + tmpLabelLineEdit = self.portLabelText + tmpCommandLineEdit = self.portCommandText + actions = self.settings.portActions + tableRow = self.portTableRow + if self.previousToolTab == 'Terminal Commands' or tabSelected == 3: + tmpWidget = self.toolForTerminalTableWidget + tmpActionLineEdit = self.terminalActionNameText + tmpLabelLineEdit = self.terminalLabelText + tmpCommandLineEdit = self.terminalCommandText + actions = self.settings.portTerminalActions + tableRow = self.terminalTableRow + + return tmpWidget, tmpActionLineEdit, tmpLabelLineEdit, tmpCommandLineEdit, actions, tableRow + +# def validateInput(self): + # called when there is a focus out event. only validates the tool name (key) for host/port/terminal tabs + def validateToolName(self): + selectGroup = self.selectGroup() + tmpWidget = selectGroup[0] + tmplineEdit = selectGroup[1] + actions = selectGroup[4] + row = selectGroup[5] + + if tmplineEdit: + row = tmpWidget.currentRow() + + # LEO: when the first condition is True the validateUniqueToolName is never called (bad if we want to + # show a nice error message for the unique key) + if row != -1: + if not validateString(str(tmplineEdit.text())) or not self.validateUniqueToolName( + tmpWidget, row, str(tmplineEdit.text())): + tmplineEdit.setStyleSheet("border: 1px solid red;") + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection) + self.validationPassed = False + log.info('the validation is: ' + str(self.validationPassed)) + return self.validationPassed + else: + tmplineEdit.setStyleSheet("border: 1px solid grey;") + tmpWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + self.validationPassed = True + log.info('the validation is: ' + str(self.validationPassed)) + if tmpWidget.item(row,0).text() != str(actions[row][1]): + log.info('difference found') + actions[row][1] = tmpWidget.item(row,0).text() + return self.validationPassed + + #def validateUniqueKey(self, widget, tablerow, text): + def validateUniqueToolName(self, widget, tablerow, text): + if tablerow != -1: + for row in [i for i in range(widget.rowCount()) if i not in [tablerow]]: + if widget.item(row,0).text() == text: + return False + return True + + #def nmapValidate(self): + # LEO: renamed and fixed bugs. + # TODO: this function is being called way too often. something seems wrong in the overall logic + def validateStagedNmapTab(self): + validationPassed = self.validateNmapPorts(self.stage1Input) + validationPassed = self.validateNmapPorts(self.stage2Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage3Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage4Input) and validationPassed + validationPassed = self.validateNmapPorts(self.stage5Input) and validationPassed + return validationPassed + + ##################### TOOLS / HOST COMMANDS FUNCTIONS ##################### + + def addToolForHost(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + currentRows = self.toolForHostsTableWidget.rowCount() + self.toolForHostsTableWidget.setRowCount(currentRows + 1) + + self.toolForHostsTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForHostsTableWidget.item( + self.toolForHostsTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.hostActionsNumber)) + self.toolForHostsTableWidget.selectRow(currentRows) + self.settings.hostActions.append(['', 'New_Action_'+str(self.hostActionsNumber), '']) + self.hostActionsNumber +=1 + self.updateToolForHostInformation() + + def removeToolForHost(self): + row = self.toolForHostsTableWidget.currentRow() + + # set default values to avoid the error when the first action is add and remove tools + self.hostActionNameText.setText('removed') + self.hostLabelText.setText('removed') + self.hostCommandText.setText('removed') + + for tool in self.settings.hostActions: + if tool[1] == str(self.hostActionNameText.text()): + self.settings.hostActions.remove(tool) + break + + self.toolForHostsTableWidget.removeRow(row) + + self.toolForHostsTableWidget.selectRow(row-1) + + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + + self.updateToolForHostInformation(False) + + def updateHostActions(self): + self.settings.hostActions[self.hostTableRow][0] = str(self.hostLabelText.text()) + self.settings.hostActions[self.hostTableRow][2] = str(self.hostCommandText.text()) + + # update variable -> do not update the values when a line is removed + def updateToolForHostInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.hostActionNameText, self.hostLabelText, self.hostCommandText): + + # do not update any values the first time or when the remove button is clicked + if self.hostTableRow == -1 or update == False: + pass + else: + self.updateHostActions() + +# self.hostLabelText.setStyleSheet("border: 1px solid grey;") +# self.hostCommandText.setStyleSheet("border: 1px solid grey;") + self.hostTableRow = self.toolForHostsTableWidget.currentRow() + self.hostLabelText.setReadOnly(False) + if self.toolForHostsTableWidget.item(self.hostTableRow, 0) is not None: + key = self.toolForHostsTableWidget.item(self.hostTableRow, 0).text() + for tool in self.settings.hostActions: + if tool[1] == key: + self.hostActionNameText.setText(tool[1]) + self.hostLabelText.setText(tool[0]) + self.hostCommandText.setText(tool[2]) + else: + self.toolForHostsTableWidget.selectRow(self.hostTableRow) + + # this function is used to REAL TIME update the tool table when a user enters a edit a tool name in the + # HOST/PORT/TERMINAL commands tabs + # LEO: this one replaces updateToolForHostTable + updateToolForServicesTable + updateToolForTerminalTable + def realTimeToolNameUpdate(self, tablewidget, text): + row = tablewidget.currentRow() + if row != -1: + tablewidget.item(row, 0).setText(str(text)) + + ##################### TOOLS / PORT COMMANDS FUNCTIONS ##################### + + def addToolForService(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + currentRows = self.toolForServiceTableWidget.rowCount() + self.toolForServiceTableWidget.setRowCount(currentRows + 1) + self.toolForServiceTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.item( + self.toolForServiceTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.portActionsNumber)) + self.toolForServiceTableWidget.selectRow(currentRows) + self.settings.portActions.append(['', 'New_Action_'+str(self.portActionsNumber), '']) + self.portActionsNumber +=1 + self.updateToolForServiceInformation() + + def removeToolForService(self): + row = self.toolForServiceTableWidget.currentRow() + self.portActionNameText.setText('removed') + self.portLabelText.setText('removed') + self.portCommandText.setText('removed') + for tool in self.settings.portActions: + if tool[1] == str(self.portActionNameText.text()): + self.settings.portActions.remove(tool) + break + self.toolForServiceTableWidget.removeRow(row) + self.toolForServiceTableWidget.selectRow(row-1) + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.updateToolForServiceInformation(False) + + def updatePortActions(self): + self.settings.portActions[self.portTableRow][0] = str(self.portLabelText.text()) + self.settings.portActions[self.portTableRow][2] = str(self.portCommandText.text()) + + def updateToolForServiceInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.portActionNameText, self.portLabelText, self.portCommandText): + # the first time do not update anything + if self.portTableRow == -1 or update == False: + log.info('no update') + pass + else: + log.info('update done') + self.updatePortActions() +# self.portLabelText.setStyleSheet("border: 1px solid grey;") +# self.portCommandText.setStyleSheet("border: 1px solid grey;") + self.portTableRow = self.toolForServiceTableWidget.currentRow() + self.portLabelText.setReadOnly(False) + + if self.toolForServiceTableWidget.item(self.portTableRow, 0) is not None: + key = self.toolForServiceTableWidget.item(self.portTableRow, 0).text() + for tool in self.settings.portActions: + if tool[1] == key: + self.portActionNameText.setText(tool[1]) + self.portLabelText.setText(tool[0]) + self.portCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have services assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForServiceTableWidget.selectRow(self.portTableRow) + + ##################### TOOLS / TERMINAL COMMANDS FUNCTIONS ##################### + + def addToolForTerminal(self): + #if self.commandTabsValidate(): + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + currentRows = self.toolForTerminalTableWidget.rowCount() + self.toolForTerminalTableWidget.setRowCount(currentRows + 1) + self.toolForTerminalTableWidget.setItem(currentRows, 0, QtWidgets.QTableWidgetItem()) + self.toolForTerminalTableWidget.item( + self.toolForTerminalTableWidget.rowCount()-1, 0).setText('New_Action_'+str(self.terminalActionsNumber)) + self.toolForTerminalTableWidget.selectRow(currentRows) + self.settings.portTerminalActions.append(['', 'New_Action_'+str(self.terminalActionsNumber), '']) + self.terminalActionsNumber +=1 + self.updateToolForTerminalInformation() + + def removeToolForTerminal(self): + row = self.toolForTerminalTableWidget.currentRow() + self.terminalActionNameText.setText('removed') + self.terminalLabelText.setText('removed') + self.terminalCommandText.setText('removed') + for tool in self.settings.portTerminalActions: + if tool[1] == str(self.terminalActionNameText.text()): + self.settings.portTerminalActions.remove(tool) + break + self.toolForTerminalTableWidget.removeRow(row) + self.toolForTerminalTableWidget.selectRow(row-1) + self.portTableRow = self.toolForTerminalTableWidget.currentRow() + self.updateToolForTerminalInformation(False) + + def updateTerminalActions(self): + self.settings.portTerminalActions[self.terminalTableRow][0] = str(self.terminalLabelText.text()) + self.settings.portTerminalActions[self.terminalTableRow][2] = str(self.terminalCommandText.text()) + + def updateToolForTerminalInformation(self, update = True): + #if self.commandTabsValidate() == True: + if self.validateCommandTabs(self.terminalActionNameText, self.terminalLabelText, self.terminalCommandText): + # do not update anything the first time or when you remove a line + if self.terminalTableRow == -1 or update == False: + pass + else: + self.updateTerminalActions() + +# self.terminalLabelText.setStyleSheet("border: 1px solid grey;") +# self.terminalCommandText.setStyleSheet("border: 1px solid grey;") + self.terminalTableRow = self.toolForTerminalTableWidget.currentRow() + self.terminalLabelText.setReadOnly(False) + + if self.toolForTerminalTableWidget.item(self.terminalTableRow, 0) is not None: + key = self.toolForTerminalTableWidget.item(self.terminalTableRow, 0).text() + for tool in self.settings.portTerminalActions: + if tool[1] == key: + self.terminalActionNameText.setText(tool[1]) + self.terminalLabelText.setText(tool[0]) + self.terminalCommandText.setText(tool[2]) + # for the case that the tool (ex. new added tool) does not have any service assigned + if len(tool) == 4: + servicesList = tool[3].split(',') + self.terminalServicesActiveTable.setRowCount(len(servicesList)) + for i in range(len(servicesList)): + self.terminalServicesActiveTable.setItem(i, 0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.item(i, 0).setText(str(servicesList[i])) + else: + self.toolForTerminalTableWidget.selectRow(self.terminalTableRow) + + ##################### TOOLS / AUTOMATED ATTACKS FUNCTIONS ##################### + + # when 'Run automated attacks' is checked this function is called + def enableAutoToolsTab(self): + if self.enableAutoAttacks.isChecked(): + self.AutoAttacksSettingsTab.setTabEnabled(1,True) + else: + self.AutoAttacksSettingsTab.setTabEnabled(1,False) + + #def selectDefaultServices(self): # toggles select/deselect all default creds checkboxes + def toggleDefaultServices(self): # toggles select/deselect all default creds checkboxes + for service in self.defaultServicesList: + if not self.typeDic[service][2].isChecked() == self.checkDefaultCred.isChecked(): + self.typeDic[service][2].toggle() + + #def addRemoveServices(self, add=True): + # in the multiple choice widget (port/terminal commands tabs) it transfers services bidirectionally + def moveService(self, src, dst): + if src.selectionModel().selectedRows(): + row = src.currentRow() + dst.setRowCount(dst.rowCount() + 1) + dst.setItem(dst.rowCount() - 1, 0, QtWidgets.QTableWidgetItem()) + dst.item(dst.rowCount() - 1, 0).setText(str(src.item(row, 0).text())) + src.removeRow(row) + + ##################### SETUP FUNCTIONS ##################### + def setupLayout(self): + self.setModal(True) + self.setWindowTitle('Settings') + self.setFixedSize(900, 500) + + self.flayout = QtWidgets.QVBoxLayout() + self.settingsTabWidget = QtWidgets.QTabWidget() + self.settingsTabWidget.setTabBar(SettingsTabBarWidget(width=200,height=25)) + self.settingsTabWidget.setTabPosition(QtWidgets.QTabWidget.TabPosition.West) # put the tab titles on the left + + # left tab menu items + self.GeneralSettingsTab = QtWidgets.QWidget() + self.BruteSettingsTab = QtWidgets.QWidget() + self.ToolSettingsTab = QtWidgets.QTabWidget() + self.WordlistsSettingsTab = QtWidgets.QTabWidget() + self.AutoAttacksSettingsTab = QtWidgets.QTabWidget() + + self.setupGeneralTab() + self.setupBruteTab() + self.setupToolsTab() + self.setupAutomatedAttacksTab() + + self.settingsTabWidget.addTab(self.GeneralSettingsTab,"General") + self.settingsTabWidget.addTab(self.BruteSettingsTab,"Brute") + self.settingsTabWidget.addTab(self.ToolSettingsTab,"Tools") + self.settingsTabWidget.addTab(self.WordlistsSettingsTab,"Wordlists") + self.settingsTabWidget.addTab(self.AutoAttacksSettingsTab,"Automated Attacks") + + self.settingsTabWidget.setCurrentIndex(0) + + self.flayout.addWidget(self.settingsTabWidget) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.cancelButton = QPushButton('Cancel') + self.cancelButton.setMaximumSize(60, 30) + self.applyButton = QPushButton('Apply') + self.applyButton.setMaximumSize(60, 30) + self.spacer2 = QSpacerItem(750,0) + self.horLayout1.addItem(self.spacer2) + self.horLayout1.addWidget(self.applyButton) + self.horLayout1.addWidget(self.cancelButton) + + self.flayout.addLayout(self.horLayout1) + self.setLayout(self.flayout) + + def setupGeneralTab(self): + self.terminalLabel = QtWidgets.QLabel() + self.terminalLabel.setText('Terminal') + self.terminalLabel.setFixedWidth(150) + self.terminalComboBox = QtWidgets.QComboBox() + self.terminalComboBox.setFixedWidth(150) + self.terminalComboBox.setMinimumContentsLength(3) + self.terminalComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") + self.terminalComboBox.setCurrentIndex(0) + self.hlayout1 = QtWidgets.QHBoxLayout() + self.hlayout1.addWidget(self.terminalLabel) + self.hlayout1.addWidget(self.terminalComboBox) + self.hlayout1.addStretch() + + self.label3 = QtWidgets.QLabel() + self.label3.setText('Maximum processes') + self.label3.setFixedWidth(150) + self.fastProcessesNumber = [] + for i in range(1, 50): + self.fastProcessesNumber.append(str(i)) + self.fastProcessesComboBox = QtWidgets.QComboBox() + self.fastProcessesComboBox.insertItems(0, self.fastProcessesNumber) + self.fastProcessesComboBox.setMinimumContentsLength(3) + self.fastProcessesComboBox.setStyleSheet("QComboBox { combobox-popup: 0; }") + self.fastProcessesComboBox.setCurrentIndex(19) + self.fastProcessesComboBox.setFixedWidth(150) + self.fastProcessesComboBox.setMaxVisibleItems(3) + self.hlayoutGeneral_4 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_4.addWidget(self.label3) + self.hlayoutGeneral_4.addWidget(self.fastProcessesComboBox) + self.hlayoutGeneral_4.addStretch() + + self.label1 = QtWidgets.QLabel() + self.label1.setText('Screenshot timeout') + self.label1.setFixedWidth(150) + self.screenshotTextinput = QtWidgets.QLineEdit() + self.screenshotTextinput.setFixedWidth(150) + self.hlayoutGeneral_2 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_2.addWidget(self.label1) + self.hlayoutGeneral_2.addWidget(self.screenshotTextinput) + self.hlayoutGeneral_2.addStretch() + + self.label2 = QtWidgets.QLabel() + self.label2.setText('Web services') + self.label2.setFixedWidth(150) + self.webServicesTextinput = QtWidgets.QLineEdit() + self.webServicesTextinput.setFixedWidth(350) + self.hlayoutGeneral_3 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_3.addWidget(self.label2) + self.hlayoutGeneral_3.addWidget(self.webServicesTextinput) + self.hlayoutGeneral_3.addStretch() + + self.checkStoreClearPW = QtWidgets.QCheckBox() + self.checkStoreClearPW.setText('Store cleartext passwords on exit') + self.hlayoutGeneral_6 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_6.addWidget(self.checkStoreClearPW) + + self.checkBlackBG = QtWidgets.QCheckBox() + self.checkBlackBG.setText('Use black backgrounds for tool output') + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.checkBlackBG) + + self.vlayoutGeneral = QtWidgets.QVBoxLayout(self.GeneralSettingsTab) + self.vlayoutGeneral.addLayout(self.hlayout1) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_4) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_2) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_3) + self.vlayoutGeneral.addLayout(self.hlayoutGeneral_6) + self.vlayoutGeneral.addLayout(self.hlayout2) + + self.generalSpacer = QSpacerItem(10,350) + self.vlayoutGeneral.addItem(self.generalSpacer) + + def setupBruteTab(self): + self.vlayoutBrute = QtWidgets.QVBoxLayout(self.BruteSettingsTab) + + self.label5 = QtWidgets.QLabel() + self.label5.setText('Username lists path') + self.label5.setFixedWidth(150) + self.userlistPath = QtWidgets.QLineEdit() + self.userlistPath.setFixedWidth(350) + self.browseUsersListButton = QPushButton('Browse') + self.browseUsersListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_7 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_7.addWidget(self.label5) + self.hlayoutGeneral_7.addWidget(self.userlistPath) + self.hlayoutGeneral_7.addWidget(self.browseUsersListButton) + self.hlayoutGeneral_7.addStretch() + + self.label6 = QtWidgets.QLabel() + self.label6.setText('Password lists path') + self.label6.setFixedWidth(150) + self.passwordlistPath = QtWidgets.QLineEdit() + self.passwordlistPath.setFixedWidth(350) + self.browsePasswordsListButton = QPushButton('Browse') + self.browsePasswordsListButton.setMaximumSize(80, 30) + self.hlayoutGeneral_8 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_8.addWidget(self.label6) + self.hlayoutGeneral_8.addWidget(self.passwordlistPath) + self.hlayoutGeneral_8.addWidget(self.browsePasswordsListButton) + self.hlayoutGeneral_8.addStretch() + + self.label7 = QtWidgets.QLabel() + self.label7.setText('Default username') + self.label7.setFixedWidth(150) + self.defaultUserText = QtWidgets.QLineEdit() + self.defaultUserText.setFixedWidth(125) + self.hlayoutGeneral_9 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_9.addWidget(self.label7) + self.hlayoutGeneral_9.addWidget(self.defaultUserText) + self.hlayoutGeneral_9.addStretch() + + self.label8 = QtWidgets.QLabel() + self.label8.setText('Default password') + self.label8.setFixedWidth(150) + self.defaultPassText = QtWidgets.QLineEdit() + self.defaultPassText.setFixedWidth(125) + self.hlayoutGeneral_10 = QtWidgets.QHBoxLayout() + self.hlayoutGeneral_10.addWidget(self.label8) + self.hlayoutGeneral_10.addWidget(self.defaultPassText) + self.hlayoutGeneral_10.addStretch() + + self.vlayoutBrute.addLayout(self.hlayoutGeneral_7) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_8) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_9) + self.vlayoutBrute.addLayout(self.hlayoutGeneral_10) + self.bruteSpacer = QSpacerItem(10,380) + self.vlayoutBrute.addItem(self.bruteSpacer) + + def setupToolsTab(self): + self.ToolPathsWidget = QtWidgets.QWidget() + self.ToolSettingsTab.addTab(self.ToolPathsWidget, "Tool Paths") + self.HostActionsWidget = QtWidgets.QWidget() + self.ToolSettingsTab.addTab(self.HostActionsWidget, "Host Commands") + self.PortActionsWidget = QtWidgets.QWidget() + self.ToolSettingsTab.addTab(self.PortActionsWidget, "Port Commands") + self.portTerminalActionsWidget = QtWidgets.QWidget() + self.ToolSettingsTab.addTab(self.portTerminalActionsWidget, "Terminal Commands") + self.StagedNmapWidget = QtWidgets.QWidget() + self.ToolSettingsTab.addTab(self.StagedNmapWidget, "Staged Nmap") + + self.setupToolPathsTab() + self.setupHostCommandsTab() + self.setupPortCommandsTab() + self.setupTerminalCommandsTab() + self.setupStagedNmapTab() + + def setupToolPathsTab(self): + self.nmapPathlabel = QtWidgets.QLabel() + self.nmapPathlabel.setText('Nmap') + self.nmapPathlabel.setFixedWidth(100) + self.nmapPathInput = QtWidgets.QLineEdit() + self.nmapPathHorLayout = QtWidgets.QHBoxLayout() + self.nmapPathHorLayout.addWidget(self.nmapPathlabel) + self.nmapPathHorLayout.addWidget(self.nmapPathInput) + self.nmapPathHorLayout.addStretch() + + self.hydraPathlabel = QtWidgets.QLabel() + self.hydraPathlabel.setText('Hydra') + self.hydraPathlabel.setFixedWidth(100) + self.hydraPathInput = QtWidgets.QLineEdit() + self.hydraPathHorLayout = QtWidgets.QHBoxLayout() + self.hydraPathHorLayout.addWidget(self.hydraPathlabel) + self.hydraPathHorLayout.addWidget(self.hydraPathInput) + self.hydraPathHorLayout.addStretch() + + self.cutycaptPathlabel = QtWidgets.QLabel() + self.cutycaptPathlabel.setText('Cutycapt') + self.cutycaptPathlabel.setFixedWidth(100) + self.cutycaptPathInput = QtWidgets.QLineEdit() + self.cutycaptPathHorLayout = QtWidgets.QHBoxLayout() + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathlabel) + self.cutycaptPathHorLayout.addWidget(self.cutycaptPathInput) + self.cutycaptPathHorLayout.addStretch() + + self.textEditorPathlabel = QtWidgets.QLabel() + self.textEditorPathlabel.setText('Text editor') + self.textEditorPathlabel.setFixedWidth(100) + self.textEditorPathInput = QtWidgets.QLineEdit() + self.textEditorPathHorLayout = QtWidgets.QHBoxLayout() + self.textEditorPathHorLayout.addWidget(self.textEditorPathlabel) + self.textEditorPathHorLayout.addWidget(self.textEditorPathInput) + self.textEditorPathHorLayout.addStretch() + + self.toolsPathVerLayout = QtWidgets.QVBoxLayout() + self.toolsPathVerLayout.addLayout(self.nmapPathHorLayout) + self.toolsPathVerLayout.addLayout(self.hydraPathHorLayout) + self.toolsPathVerLayout.addLayout(self.cutycaptPathHorLayout) + self.toolsPathVerLayout.addLayout(self.textEditorPathHorLayout) + self.toolsPathVerLayout.addStretch() + + self.globToolsPathHorLayout = QtWidgets.QHBoxLayout(self.ToolPathsWidget) + self.globToolsPathHorLayout.addLayout(self.toolsPathVerLayout) + self.toolsPathHorSpacer = QSpacerItem(50,0) # right margin spacer + self.globToolsPathHorLayout.addItem(self.toolsPathHorSpacer) + + def setupHostCommandsTab(self): + self.toolForHostsTableWidget = QtWidgets.QTableWidget(self.HostActionsWidget) + self.toolForHostsTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.toolForHostsTableWidget.setFixedWidth(180) + self.toolForHostsTableWidget.setShowGrid(False) # to make the cells of the table read only + self.toolForHostsTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + + self.toolForHostsTableWidget.setColumnCount(1) + self.toolForHostsTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForHostsTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForHostsTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForHostsTableWidget.horizontalHeader().setVisible(False) + self.toolForHostsTableWidget.verticalHeader().setVisible(False) # row header - is hidden + self.toolForHostsTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayoutPortActions = QtWidgets.QHBoxLayout() + self.removeToolForHostButton = QPushButton('Remove') + self.removeToolForHostButton.setMaximumSize(90, 30) + self.addToolForHostButton = QPushButton('Add') + self.addToolForHostButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolForHostButton) + self.horLayoutPortActions.addWidget(self.removeToolForHostButton) + + self.actionHost = QtWidgets.QLabel() + self.actionHost.setText('Tools') + + self.verLayoutPortActions = QtWidgets.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.actionHost) + self.verLayoutPortActions.addWidget(self.toolForHostsTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtWidgets.QVBoxLayout() + + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.hostActionNameText = QtWidgets.QLineEdit() + + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.hostActionNameText) + + self.label9 = QtWidgets.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + + self.hostLabelText = QtWidgets.QLineEdit() + self.hostLabelText.setText(' ') + self.hostLabelText.setReadOnly(True) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.hostLabelText) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + + self.hostCommandText = QtWidgets.QLineEdit() + self.hostCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.hostCommandText) + + self.spacer6 = QSpacerItem(0,20) + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.spacer1 = QSpacerItem(0,800) + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.HostActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + self.spacer5 = QSpacerItem(10,0) + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) + self.globLayoutPortActions.addItem(self.spacer2) + + def setupPortCommandsTab(self): + self.label11 = QtWidgets.QLabel() + self.label11.setText('Tools') + + self.toolForServiceTableWidget = QtWidgets.QTableWidget(self.PortActionsWidget) + self.toolForServiceTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.toolForServiceTableWidget.setFixedWidth(180) + self.toolForServiceTableWidget.setShowGrid(False) + self.toolForServiceTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + # table headers + self.toolForServiceTableWidget.setColumnCount(1) + self.toolForServiceTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForServiceTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForServiceTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForServiceTableWidget.horizontalHeader().setVisible(False) + self.toolForServiceTableWidget.verticalHeader().setVisible(False) + self.toolForServiceTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayoutPortActions = QtWidgets.QHBoxLayout() + self.addToolButton = QPushButton('Add') + self.addToolButton.setMaximumSize(90, 30) + self.removeToolButton = QPushButton('Remove') + self.removeToolButton.setMaximumSize(90, 30) + self.horLayoutPortActions.addWidget(self.addToolButton) + self.horLayoutPortActions.addWidget(self.removeToolButton) + + self.verLayoutPortActions = QtWidgets.QVBoxLayout() + self.verLayoutPortActions.addWidget(self.label11) + self.verLayoutPortActions.addWidget(self.toolForServiceTableWidget) + self.verLayoutPortActions.addLayout(self.horLayoutPortActions) + + self.verLayout1 = QtWidgets.QVBoxLayout() + # right side + self.horLayout4 = QtWidgets.QHBoxLayout() + self.label12 = QtWidgets.QLabel() + self.label12.setText('Tool') + self.label12.setFixedWidth(70) + self.portActionNameText = QtWidgets.QLineEdit() + self.horLayout4.addWidget(self.label12) + self.horLayout4.addWidget(self.portActionNameText) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.label9 = QtWidgets.QLabel() + self.label9.setText('Label') + self.label9.setFixedWidth(70) + self.portLabelText = QtWidgets.QLineEdit() + self.portLabelText.setText(' ') + self.portLabelText.setReadOnly(True) + self.horLayout1.addWidget(self.label9) + self.horLayout1.addWidget(self.portLabelText) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.label10 = QtWidgets.QLabel() + self.label10.setText('Command') + self.label10.setFixedWidth(70) + self.portCommandText = QtWidgets.QLineEdit() + self.portCommandText.setText('init value') + self.horLayout2.addWidget(self.label10) + self.horLayout2.addWidget(self.portCommandText) + + self.servicesAllTableWidget = QtWidgets.QTableWidget() + self.servicesAllTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.servicesAllTableWidget.setMaximumSize(150, 300) + self.servicesAllTableWidget.setColumnCount(1) + self.servicesAllTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesAllTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.servicesAllTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesAllTableWidget.horizontalHeader().setVisible(False) + self.servicesAllTableWidget.setShowGrid(False) + self.servicesAllTableWidget.verticalHeader().setVisible(False) + + self.servicesActiveTableWidget = QtWidgets.QTableWidget() + self.servicesActiveTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.servicesActiveTableWidget.setMaximumSize(150, 300) + self.servicesActiveTableWidget.setColumnCount(1) + self.servicesActiveTableWidget.horizontalHeader().resizeSection(0,150) + self.servicesActiveTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.servicesActiveTableWidget.horizontalHeaderItem(0).setText("Name") + self.servicesActiveTableWidget.horizontalHeader().setVisible(False) + self.servicesActiveTableWidget.setShowGrid(False) + self.servicesActiveTableWidget.verticalHeader().setVisible(False) + + self.verLayout2 = QtWidgets.QVBoxLayout() + + self.addServicesButton = QPushButton('-->') + self.addServicesButton.setMaximumSize(30, 30) + self.removeServicesButton = QPushButton('<--') + self.removeServicesButton.setMaximumSize(30, 30) + + self.spacer4 = QSpacerItem(0,90) # space above and below arrow buttons + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addWidget(self.addServicesButton) + self.verLayout2.addWidget(self.removeServicesButton) + self.verLayout2.addItem(self.spacer4) + + self.horLayout3 = QtWidgets.QHBoxLayout() # space left of multiple choice widget + self.spacer3 = QSpacerItem(78,0) + self.horLayout3.addItem(self.spacer3) + self.horLayout3.addWidget(self.servicesAllTableWidget) + self.horLayout3.addLayout(self.verLayout2) + self.horLayout3.addWidget(self.servicesActiveTableWidget) + + self.spacer6 = QSpacerItem(0,20) # top right space + self.verLayout1.addItem(self.spacer6) + self.verLayout1.addLayout(self.horLayout4) + self.verLayout1.addLayout(self.horLayout1) + self.verLayout1.addLayout(self.horLayout2) + self.verLayout1.addLayout(self.horLayout3) + self.spacer1 = QSpacerItem(0,50) # bottom right space + self.verLayout1.addItem(self.spacer1) + + self.globLayoutPortActions = QtWidgets.QHBoxLayout(self.PortActionsWidget) + self.globLayoutPortActions.addLayout(self.verLayoutPortActions) + + self.spacer5 = QSpacerItem(10,0) # space between left and right layouts + self.globLayoutPortActions.addItem(self.spacer5) + self.globLayoutPortActions.addLayout(self.verLayout1) + self.spacer2 = QSpacerItem(50,0) # right margin space + self.globLayoutPortActions.addItem(self.spacer2) + + def setupTerminalCommandsTab(self): + self.actionTerminalLabel = QtWidgets.QLabel() + self.actionTerminalLabel.setText('Tools') + + self.toolForTerminalTableWidget = QtWidgets.QTableWidget(self.portTerminalActionsWidget) + self.toolForTerminalTableWidget.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.toolForTerminalTableWidget.setFixedWidth(180) + self.toolForTerminalTableWidget.setShowGrid(False) + # to make the cells of the table read only + self.toolForTerminalTableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + # table headers + self.toolForTerminalTableWidget.setColumnCount(1) + self.toolForTerminalTableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.toolForTerminalTableWidget.horizontalHeaderItem(0).setText("Name") + self.toolForTerminalTableWidget.horizontalHeader().resizeSection(0,200) + self.toolForTerminalTableWidget.horizontalHeader().setVisible(False) + self.toolForTerminalTableWidget.verticalHeader().setVisible(False) + self.toolForTerminalTableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem()) + + self.horLayout1 = QtWidgets.QHBoxLayout() + self.addToolForTerminalButton = QPushButton('Add') + self.addToolForTerminalButton.setMaximumSize(90, 30) + self.removeToolForTerminalButton = QPushButton('Remove') + self.removeToolForTerminalButton.setMaximumSize(90, 30) + self.horLayout1.addWidget(self.addToolForTerminalButton) + self.horLayout1.addWidget(self.removeToolForTerminalButton) + + self.verLayout1 = QtWidgets.QVBoxLayout() + self.verLayout1.addWidget(self.actionTerminalLabel) + self.verLayout1.addWidget(self.toolForTerminalTableWidget) + self.verLayout1.addLayout(self.horLayout1) + + self.horLayout2 = QtWidgets.QHBoxLayout() + self.actionNameTerminalLabel = QtWidgets.QLabel() + self.actionNameTerminalLabel.setText('Tool') + self.actionNameTerminalLabel.setFixedWidth(70) + self.terminalActionNameText = QtWidgets.QLineEdit() + self.horLayout2.addWidget(self.actionNameTerminalLabel) + self.horLayout2.addWidget(self.terminalActionNameText) + + self.horLayout3 = QtWidgets.QHBoxLayout() + self.labelTerminalLabel = QtWidgets.QLabel() + self.labelTerminalLabel.setText('Label') + self.labelTerminalLabel.setFixedWidth(70) + self.terminalLabelText = QtWidgets.QLineEdit() + self.terminalLabelText.setText(' ') + self.terminalLabelText.setReadOnly(True) + self.horLayout3.addWidget(self.labelTerminalLabel) + self.horLayout3.addWidget(self.terminalLabelText) + + self.horLayout4 = QtWidgets.QHBoxLayout() + self.commandTerminalLabel = QtWidgets.QLabel() + self.commandTerminalLabel.setText('Command') + self.commandTerminalLabel.setFixedWidth(70) + self.terminalCommandText = QtWidgets.QLineEdit() + self.terminalCommandText.setText('init value') + self.horLayout4.addWidget(self.commandTerminalLabel) + self.horLayout4.addWidget(self.terminalCommandText) + + self.terminalServicesAllTable = QtWidgets.QTableWidget() + self.terminalServicesAllTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.terminalServicesAllTable.setMaximumSize(150, 300) + self.terminalServicesAllTable.setColumnCount(1) + self.terminalServicesAllTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesAllTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.terminalServicesAllTable.horizontalHeaderItem(0).setText("Available Services") + self.terminalServicesAllTable.horizontalHeader().setVisible(False) + self.terminalServicesAllTable.setShowGrid(False) + self.terminalServicesAllTable.verticalHeader().setVisible(False) + + self.terminalServicesActiveTable = QtWidgets.QTableWidget() + self.terminalServicesActiveTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.terminalServicesActiveTable.setMaximumSize(150, 300) + self.terminalServicesActiveTable.setColumnCount(1) + self.terminalServicesActiveTable.horizontalHeader().resizeSection(0,150) + self.terminalServicesActiveTable.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem()) + self.terminalServicesActiveTable.horizontalHeaderItem(0).setText("Applied Services") + self.terminalServicesActiveTable.horizontalHeader().setVisible(False) + self.terminalServicesActiveTable.setShowGrid(False) + self.terminalServicesActiveTable.verticalHeader().setVisible(False) + + self.addTerminalServiceButton = QPushButton('-->') + self.addTerminalServiceButton.setMaximumSize(30, 30) + self.removeTerminalServiceButton = QPushButton('<--') + self.removeTerminalServiceButton.setMaximumSize(30, 30) + + self.verLayout3 = QtWidgets.QVBoxLayout() + self.spacer2 = QSpacerItem(0,90) + self.verLayout3.addItem(self.spacer2) + self.verLayout3.addWidget(self.addTerminalServiceButton) + self.verLayout3.addWidget(self.removeTerminalServiceButton) + self.verLayout3.addItem(self.spacer2) + + self.horLayout5 = QtWidgets.QHBoxLayout() + self.spacer3 = QSpacerItem(78,0) + self.horLayout5.addItem(self.spacer3) + self.horLayout5.addWidget(self.terminalServicesAllTable) + self.horLayout5.addLayout(self.verLayout3) + self.horLayout5.addWidget(self.terminalServicesActiveTable) + + self.verLayout2 = QtWidgets.QVBoxLayout() + self.spacer4 = QSpacerItem(0,20) + self.verLayout2.addItem(self.spacer4) + self.verLayout2.addLayout(self.horLayout2) + self.verLayout2.addLayout(self.horLayout3) + self.verLayout2.addLayout(self.horLayout4) + self.verLayout2.addLayout(self.horLayout5) + self.spacer5 = QSpacerItem(0,50) + self.verLayout2.addItem(self.spacer5) + + self.globLayoutTerminalActions = QtWidgets.QHBoxLayout(self.portTerminalActionsWidget) + self.globLayoutTerminalActions.addLayout(self.verLayout1) + self.spacer6 = QSpacerItem(10,0) + self.globLayoutTerminalActions.addItem(self.spacer6) + self.globLayoutTerminalActions.addLayout(self.verLayout2) + self.spacer7 = QSpacerItem(50,0) + self.globLayoutTerminalActions.addItem(self.spacer7) + + def setupStagedNmapTab(self): + self.stage1label = QtWidgets.QLabel() + self.stage1label.setText('nmap stage 1') + self.stage1label.setFixedWidth(100) + self.stage1Input = QtWidgets.QLineEdit() + self.stage1Input.setFixedWidth(500) + self.hlayout1 = QtWidgets.QHBoxLayout() + self.hlayout1.addWidget(self.stage1label) + self.hlayout1.addWidget(self.stage1Input) + + self.stage2label = QtWidgets.QLabel() + self.stage2label.setText('nmap stage 2') + self.stage2label.setFixedWidth(100) + self.stage2Input = QtWidgets.QLineEdit() + self.stage2Input.setFixedWidth(500) + self.hlayout2 = QtWidgets.QHBoxLayout() + self.hlayout2.addWidget(self.stage2label) + self.hlayout2.addWidget(self.stage2Input) + + self.stage3label = QtWidgets.QLabel() + self.stage3label.setText('nmap stage 3') + self.stage3label.setFixedWidth(100) + self.stage3Input = QtWidgets.QLineEdit() + self.stage3Input.setFixedWidth(500) + self.hlayout3 = QtWidgets.QHBoxLayout() + self.hlayout3.addWidget(self.stage3label) + self.hlayout3.addWidget(self.stage3Input) + + self.stage4label = QtWidgets.QLabel() + self.stage4label.setText('nmap stage 4') + self.stage4label.setFixedWidth(100) + self.stage4Input = QtWidgets.QLineEdit() + self.stage4Input.setFixedWidth(500) + self.hlayout4 = QtWidgets.QHBoxLayout() + self.hlayout4.addWidget(self.stage4label) + self.hlayout4.addWidget(self.stage4Input) + + self.stage5label = QtWidgets.QLabel() + self.stage5label.setText('nmap stage 5') + self.stage5label.setFixedWidth(100) + self.stage5Input = QtWidgets.QLineEdit() + self.stage5Input.setFixedWidth(500) + self.hlayout5 = QtWidgets.QHBoxLayout() + self.hlayout5.addWidget(self.stage5label) + self.hlayout5.addWidget(self.stage5Input) + + self.vlayout1 = QtWidgets.QVBoxLayout() + self.vlayout1.addLayout(self.hlayout1) + self.vlayout1.addLayout(self.hlayout2) + self.vlayout1.addLayout(self.hlayout3) + self.vlayout1.addLayout(self.hlayout4) + self.vlayout1.addLayout(self.hlayout5) + self.vlayout1.addStretch() + + self.gHorLayout = QtWidgets.QHBoxLayout(self.StagedNmapWidget) + self.gHorLayout.addLayout(self.vlayout1) + self.spacer2 = QSpacerItem(50,0) # right margin spacer + self.gHorLayout.addItem(self.spacer2) + + def setupAutomatedAttacksTab(self): + self.GeneralAutoSettingsWidget = QtWidgets.QWidget() + self.AutoAttacksSettingsTab.addTab(self.GeneralAutoSettingsWidget, "General") + self.AutoToolsWidget = QtWidgets.QWidget() + self.AutoAttacksSettingsTab.addTab(self.AutoToolsWidget, "Tool Configuration") + + self.setupAutoAttacksGeneralTab() + self.setupAutoAttacksToolTab() + + def setupAutoAttacksGeneralTab(self): + self.globVerAutoSetLayout = QtWidgets.QVBoxLayout(self.GeneralAutoSettingsWidget) + + self.enableAutoAttacks = QtWidgets.QCheckBox() + self.enableAutoAttacks.setText('Run automated attacks') + self.checkDefaultCred = QtWidgets.QCheckBox() + self.checkDefaultCred.setText('Check for default credentials') + + self.defaultBoxVerlayout = QtWidgets.QVBoxLayout() + self.defaultCredentialsBox = QGroupBox("Default Credentials") + self.defaultCredentialsBox.setLayout(self.defaultBoxVerlayout) + self.globVerAutoSetLayout.addWidget(self.enableAutoAttacks) + self.globVerAutoSetLayout.addWidget(self.checkDefaultCred) + self.globVerAutoSetLayout.addWidget(self.defaultCredentialsBox) + self.globVerAutoSetLayout.addStretch() + + def setupAutoAttacksToolTab(self): + self.toolNameLabel = QtWidgets.QLabel() + self.toolNameLabel.setText('Tool') + self.toolNameLabel.setFixedWidth(150) + self.toolServicesLabel = QtWidgets.QLabel() + self.toolServicesLabel.setText('Services') + self.toolServicesLabel.setFixedWidth(300) + self.enableAllToolsLabel = QtWidgets.QLabel() + self.enableAllToolsLabel.setText('Run automatically') + self.enableAllToolsLabel.setFixedWidth(150) + + self.autoToolTabHorLayout = QtWidgets.QHBoxLayout() + self.autoToolTabHorLayout.addWidget(self.toolNameLabel) + self.autoToolTabHorLayout.addWidget(self.toolServicesLabel) + self.autoToolTabHorLayout.addWidget(self.enableAllToolsLabel) + + self.scrollArea = QtWidgets.QScrollArea() + self.scrollWidget = QtWidgets.QWidget() + + self.globVerAutoToolsLayout = QtWidgets.QVBoxLayout(self.AutoToolsWidget) + self.globVerAutoToolsLayout.addLayout(self.autoToolTabHorLayout) + + self.scrollVerLayout = QtWidgets.QVBoxLayout(self.scrollWidget) + self.enabledSpacer = QSpacerItem(60,0) + + # by default the automated attacks are not activated and the tab is not enabled + self.AutoAttacksSettingsTab.setTabEnabled(1,False) + + # for all the browse buttons + def wordlistDialog(self, title='Choose username path'): + if title == 'Choose username path': + path = QtWidgets.QFileDialog\ + .getExistingDirectory(self, title, '/', QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks) + self.userlistPath.setText(str(path)) + else: + path = QtWidgets.QFileDialog.getExistingDirectory(self, title, '/') + self.passwordlistPath.setText(str(path)) + diff --git a/ui/view.py b/ui/view.py new file mode 100644 index 00000000..8b909902 --- /dev/null +++ b/ui/view.py @@ -0,0 +1,1771 @@ +#!/usr/bin/env python + +""" +LEGION (https://shanewilliamscott.com) +Copyright (c) 2024 Shane Scott + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with this program. + If not, see . +""" + +import ntpath # for file operations, to kill processes and for regex + +from app.ApplicationInfo import applicationInfo, getVersion +from app.timing import getTimestamp +from ui.ViewState import ViewState +from ui.dialogs import * +from ui.settingsDialog import * +from ui.configDialog import * +from ui.helpDialog import * +from ui.addHostDialog import * +from ui.ancillaryDialog import * +from ui.models.hostmodels import * +from ui.models.servicemodels import * +from ui.models.scriptmodels import * +from ui.models.cvemodels import * +from ui.models.processmodels import * +from app.auxiliary import * +from six import u as unicode +import pandas as pd +from PyQt6.QtWidgets import QAbstractItemView +from PyQt6.QtCore import Qt + +log = getAppLogger() + +# this class handles everything gui-related +class View(QtCore.QObject): + tick = QtCore.pyqtSignal(int, name="changed") # signal used to update the progress bar + + def __init__(self, viewState: ViewState, ui, ui_mainwindow, shell: Shell, app, loop): + QtCore.QObject.__init__(self) + self.ui = ui + self.ui_mainwindow = ui_mainwindow # TODO: retrieve window dimensions/location from settings + self.app = app + self.loop = loop + + self.bottomWindowSize = 100 + self.leftPanelSize = 300 + + self.ui.splitter_2.setSizes([250, self.bottomWindowSize]) # set better default size for bottom panel + self.qss = None + self.processesTableViewSort = 'desc' + self.processesTableViewSortColumn = 'status' + self.toolsTableViewSort = 'desc' + self.toolsTableViewSortColumn = 'id' + self.shell = shell + self.viewState = viewState + + # the view needs access to controller methods to link gui actions with real actions + def setController(self, controller): + self.controller = controller + + def startOnce(self): + # the number of fixed host tabs (services, scripts, information, notes) + self.fixedTabsCount = self.ui.ServicesTabWidget.count() + self.hostInfoWidget = HostInformationWidget(self.ui.InformationTab) + self.filterdialog = FiltersDialog(self.ui.centralwidget) + self.importProgressWidget = ProgressWidget('Importing nmap..', self.ui.centralwidget) + self.adddialog = AddHostsDialog(self.ui.centralwidget) + self.settingsWidget = AddSettingsDialog(self.shell, self.ui.centralwidget) + self.helpDialog = HelpDialog(applicationInfo["name"], applicationInfo["author"], applicationInfo["copyright"], + applicationInfo["links"], applicationInfo["emails"], applicationInfo["version"], + applicationInfo["build"], applicationInfo["update"], applicationInfo["license"], + applicationInfo["desc"], applicationInfo["smallIcon"], applicationInfo["bigIcon"], + qss = self.qss, parent = self.ui.centralwidget) + self.configDialog = ConfigDialog(controller = self.controller, qss = self.qss, parent = self.ui.centralwidget) + + self.ui.HostsTableView.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.ui.ServiceNamesTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.CvesTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ToolsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ScriptsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.ui.ToolHostsTableView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + + # initialisations (globals, etc) + def start(self, title='*untitled'): + self.viewState = ViewState() + self.ui.keywordTextInput.setText('') # clear keyword filter + + self.ProcessesTableModel = None # fixes bug when sorting processes for the first time + self.ToolsTableModel = None + self.setupProcessesTableView() + self.setupToolsTableView() + + self.setMainWindowTitle(title) + self.ui.statusbar.showMessage('Starting up..', msecs=1000) + + self.initTables() # initialise all tables + + self.updateInterface() + self.restoreToolTabWidget(True) # True means we want to show the original textedit + self.updateScriptsOutputView('') # update the script output panel (right) + self.updateToolHostsTableView('') + self.ui.MainTabWidget.setCurrentIndex(0) # display scan tab by default + self.ui.HostsTabWidget.setCurrentIndex(0) # display Hosts tab by default + self.ui.ServicesTabWidget.setCurrentIndex(0) # display Services tab by default + self.ui.BottomTabWidget.setCurrentIndex(0) # display Log tab by default + self.ui.BruteTabWidget.setTabsClosable(True) # sets all tabs as closable in bruteforcer + + self.ui.ServicesTabWidget.setTabsClosable(True) # hide the close button (cross) from the fixed tabs + + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.ButtonPosition.RightSide, None) + + self.resetBruteTabs() # clear brute tabs (if any) and create default brute tab + self.displayToolPanel(False) + self.displayScreenshots(False) + # displays an overlay over the hosttableview saying 'click here to add host(s) to scope' + self.displayAddHostsOverlay(True) + + def startConnections(self): # signal initialisations (signals/slots, actions, etc) + ### MENU ACTIONS ### + self.connectCreateNewProject() + self.connectOpenExistingProject() + self.connectSaveProject() + self.connectSaveProjectAs() + self.connectAddHosts() + self.connectImportNmap() + #self.connectSettings() + self.connectHelp() + self.connectConfig() + self.connectAppExit() + ### TABLE ACTIONS ### + self.connectAddHostsOverlayClick() + self.connectHostTableClick() + self.connectServiceNamesTableClick() + self.connectToolsTableClick() + self.connectScriptTableClick() + self.connectToolHostsClick() + self.connectAdvancedFilterClick() + self.connectAddHostClick() + self.connectSwitchTabClick() # to detect changing tabs (on left panel) + self.connectSwitchMainTabClick() # to detect changing top level tabs + self.connectTableDoubleClick() # for double clicking on host (it redirects to the host view) + self.connectProcessTableHeaderResize() + ### CONTEXT MENUS ### + self.connectHostsTableContextMenu() + self.connectServiceNamesTableContextMenu() + self.connectServicesTableContextMenu() + self.connectToolHostsTableContextMenu() + self.connectProcessesTableContextMenu() + self.connectScreenshotContextMenu() + ### OTHER ### + self.ui.NotesTextEdit.textChanged.connect(self.setDirty) + self.ui.FilterApplyButton.clicked.connect(self.updateFilterKeywords) + self.ui.ServicesTabWidget.tabCloseRequested.connect(self.closeHostToolTab) + self.ui.BruteTabWidget.tabCloseRequested.connect(self.closeBruteTab) + self.ui.keywordTextInput.returnPressed.connect(self.ui.FilterApplyButton.click) + self.filterdialog.applyButton.clicked.connect(self.updateFilter) + #self.settingsWidget.applyButton.clicked.connect(self.applySettings) + #self.settingsWidget.cmdCancelButton.clicked.connect(self.cancelSettings) + #self.settingsWidget.applyButton.clicked.connect(self.controller.applySettings(self.settingsWidget.settings)) + self.tick.connect(self.importProgressWidget.setProgress) # slot used to update the progress bar + + #################### AUXILIARY #################### + + def initTables(self): # this function prepares the default settings for each table + # hosts table (left) + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", + "Count", "Closed"] + setTableProperties(self.ui.HostsTableView, len(headers), [0, 2, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24]) + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + ## + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) + # Set the model of the HostsTableView to the HostsTableModel + self.ui.HostsTableView.setModel(self.HostsTableModel) + # Resize the OS column + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + # Sort the model by the Host column in descending order + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + # Connect the clicked signal of the HostsTableView to the hostTableClick() method + self.ui.HostsTableView.clicked.connect(self.hostTableClick) + self.ui.HostsTableView.doubleClicked.connect(self.hostTableDoubleClick) + + ## + + # service names table (left) + headers = ["Name"] + setTableProperties(self.ui.ServiceNamesTableView, len(headers)) + + # cves table (right) + headers = ["CVE Id", "Severity", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] + setTableProperties(self.ui.CvesTableView, len(headers)) + self.ui.CvesTableView.setSortingEnabled(True) + + # tools table (left) + headers = ["Progress", "Display", "Pid", "Tool", "Tool", "Host", "Port", "Protocol", "Command", "Start time", + "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ToolsTableView, len(headers), [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + + # service table (right) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [0, 1, 5, 6, 8, 10, 11]) + + # ports by service (right) + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + setTableProperties(self.ui.ServicesTableView, len(headers), [2, 5, 6, 8, 10, 11]) + self.ui.ServicesTableView.horizontalHeader().resizeSection(0, 130) # resize IP + + # scripts table (right) + headers = ["Id", "Script", "Port", "Protocol"] + setTableProperties(self.ui.ScriptsTableView, len(headers), [0, 3]) + + # tool hosts table (right) + headers = ["Progress", "Display", "Pid", "Name", "Action", "Target", "Port", "Protocol", "Command", + "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ToolHostsTableView, len(headers), [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 12]) + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(5,150) # default width for Host column + + # process table + headers = ["Progress", "Elapsed", "Est. Remaining", "Display", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "OutputFile", "Output", "Status"] + setTableProperties(self.ui.ProcessesTableView, len(headers), [1, 2, 3, 4, 5, 8, 9, 10, 13, 14, 16]) + self.ui.ProcessesTableView.setSortingEnabled(True) + + def setMainWindowTitle(self, title): + self.ui_mainwindow.setWindowTitle(str(title)) + + def yesNoDialog(self, message, title): + dialog = QtWidgets.QMessageBox.question(self.ui.centralwidget, title, message, + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.No) + return dialog + + def setDirty(self, status=True): # this function is called for example when the user edits notes + self.viewState.dirty = status + title = '' + + if self.viewState.dirty: + title = '*' + if self.controller.isTempProject(): + title += 'untitled' + else: + title += ntpath.basename(str(self.controller.getProjectName())) + + self.setMainWindowTitle(applicationInfo["name"] + ' ' + getVersion() + ' - ' + title + ' - ' + + self.controller.getCWD()) + + #################### ACTIONS #################### + + def connectProcessTableHeaderResize(self): + self.ui.ProcessesTableView.horizontalHeader().sectionResized.connect(self.saveProcessHeaderWidth) + + def saveProcessHeaderWidth(self, index, oldSize, newSize): + columnWidths = self.controller.getSettings().gui_process_tab_column_widths.split(',') + difference = abs(int(columnWidths[index]) - newSize) + if difference >= 5: + columnWidths[index] = str(newSize) + self.controller.settings.gui_process_tab_column_widths = ','.join(columnWidths) + self.controller.applySettings(self.controller.settings) + + def dealWithRunningProcesses(self, exiting=False): + if len(self.controller.getRunningProcesses()) > 0: + message = "There are still processes running. If you continue, every process will be terminated. " + \ + "Are you sure you want to continue?" + reply = self.yesNoDialog(message, 'Confirm') + + if not reply == QtWidgets.QMessageBox.StandardButton.Yes: + return False + self.controller.killRunningProcesses() + + elif exiting: + return self.confirmExit() + + return True + + # returns True if we can proceed with: creating/opening a project or exiting + def dealWithCurrentProject(self, exiting=False): + if self.viewState.dirty: # if there are unsaved changes, show save dialog first + if not self.saveOrDiscard(): # if the user canceled, stop + return False + + return self.dealWithRunningProcesses(exiting) # deal with running processes + + def confirmExit(self): + message = "Are you sure to exit the program?" + reply = self.yesNoDialog(message, 'Confirm') + return (reply == QtWidgets.QMessageBox.StandardButton.Yes) + + def killProcessConfirmation(self): + message = "Are you sure you want to kill the selected processes?" + reply = self.yesNoDialog(message, 'Confirm') + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + return True + return False + + def connectCreateNewProject(self): + self.ui.actionNew.triggered.connect(self.createNewProject) + + def createNewProject(self): + if self.dealWithCurrentProject(): + log.info('Creating new project..') + self.controller.createNewProject() + + def connectOpenExistingProject(self): + self.ui.actionOpen.triggered.connect(self.openExistingProject) + + def openExistingProject(self): + if self.dealWithCurrentProject(): + filename = QtWidgets.QFileDialog.getOpenFileName( + self.ui.centralwidget, 'Open project', self.controller.getCWD(), + filter='Legion session (*.legion);; Sparta session (*.sprt)')[0] + + if not filename == '': # check for permissions + if not os.access(filename, os.R_OK) or not os.access(filename, os.W_OK): + log.info('Insufficient permissions to open this file.') + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this file.", + "Ok") + return + + if '.legion' in str(filename): + projectType = 'legion' + elif '.sprt' in str(filename): + projectType = 'sparta' + + self.controller.openExistingProject(filename, projectType) + self.viewState.firstSave = False # overwrite this variable because we are opening an existing file + # do not show the overlay because the hosttableview is already populated + self.displayAddHostsOverlay(False) + else: + log.info('No file chosen..') + + def connectSaveProject(self): + self.ui.actionSave.triggered.connect(self.saveProject) + + def saveProject(self): + self.ui.statusbar.showMessage('Saving..') + if self.viewState.firstSave: + self.saveProjectAs() + else: + log.info('Saving project..') + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + self.setDirty(False) + self.ui.statusbar.showMessage('Saved!', msecs=1000) + log.info('Saved!') + + def connectSaveProjectAs(self): + self.ui.actionSaveAs.triggered.connect(self.saveProjectAs) + + def saveProjectAs(self): + self.ui.statusbar.showMessage('Saving..') + log.info('Saving project..') + + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', + self.controller.getCWD(), filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.Option.DontConfirmOverwrite)[0] + + while not filename =='': + if not os.access(ntpath.dirname(str(filename)), os.R_OK) or not os.access( + ntpath.dirname(str(filename)), os.W_OK): + log.info('Insufficient permissions on this folder.') + reply = QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions on this folder.") + + else: + if self.controller.saveProjectAs(filename): + break + + if not str(filename).endswith('.legion'): + filename = str(filename) + '.legion' + msgBox = QtWidgets.QMessageBox() + reply = msgBox.question(self.ui.centralwidget, 'Confirm', + "A file named \""+ntpath.basename(str(filename))+"\" already exists. " + + "Do you want to replace it?", + QtWidgets.QMessageBox.StandardButton.Abort | QtWidgets.QMessageBox.StandardButton.Save) + + if reply == QtWidgets.QMessageBox.StandardButton.Save: + self.controller.saveProjectAs(filename, 1) # replace + break + + filename = QtWidgets.QFileDialog.getSaveFileName(self.ui.centralwidget, 'Save project as', '.', + filter='Legion session (*.legion)', + options=QtWidgets.QFileDialog.Option.DontConfirmOverwrite)[0] + + if not filename == '': + self.setDirty(False) + self.viewState.firstSave = False + self.ui.statusbar.showMessage('Saved!', msecs=1000) + self.controller.updateOutputFolder() + log.info('Saved!') + else: + log.info('No file chosen..') + + def saveOrDiscard(self): + reply = QtWidgets.QMessageBox.question( + self.ui.centralwidget, 'Confirm', "The project has been modified. Do you want to save your changes?", + QtWidgets.QMessageBox.StandardButton.Save | QtWidgets.QMessageBox.StandardButton.Discard | QtWidgets.QMessageBox.StandardButton.Cancel, + QtWidgets.QMessageBox.StandardButton.Save) + + if reply == QtWidgets.QMessageBox.StandardButton.Save: + self.saveProject() + return True + elif reply == QtWidgets.QMessageBox.StandardButton.Discard: + return True + else: + return False # the user cancelled + + def closeProject(self): + self.ui.statusbar.showMessage('Closing project..', msecs=1000) + self.controller.closeProject() + self.removeToolTabs() # to make them disappear from the UI + + def connectAddHosts(self): + self.ui.actionAddHosts.triggered.connect(self.connectAddHostsDialog) + + def connectAddHostsDialog(self): + self.adddialog.cmdAddButton.setDefault(True) + self.adddialog.txtHostList.setFocus(Qt.FocusReason.OtherFocusReason) + self.adddialog.validationLabel.hide() + self.adddialog.spacer.changeSize(15, 15) + self.adddialog.show() + self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) + self.adddialog.cmdCancelButton.clicked.connect(self.adddialog.close) + + def callAddHosts(self): + hostListStr = str(self.adddialog.txtHostList.toPlainText()).replace(';',' ') + nmapOptions = [] + scanMode = 'Unset' + + if validateNmapInput(hostListStr): + self.adddialog.close() + hostList = [] + splitTypes = [';', ' ', '\n'] + + for splitType in splitTypes: + hostListStr = hostListStr.replace(splitType, ';') + + hostList = hostListStr.split(';') + hostList = [hostEntry for hostEntry in hostList if len(hostEntry) > 0] + + hostAddOptionControls = [self.adddialog.rdoScanOptTcpConnect, self.adddialog.rdoScanOptObfuscated, + self.adddialog.rdoScanOptFin, self.adddialog.rdoScanOptNull, + self.adddialog.rdoScanOptXmas, self.adddialog.rdoScanOptPingTcp, + self.adddialog.rdoScanOptPingUdp, self.adddialog.rdoScanOptPingDisable, + self.adddialog.rdoScanOptPingRegular, self.adddialog.rdoScanOptPingSyn, + self.adddialog.rdoScanOptPingAck, self.adddialog.rdoScanOptPingTimeStamp, + self.adddialog.rdoScanOptPingNetmask, self.adddialog.chkScanOptFragmentation] + nmapOptions = [] + + if self.adddialog.rdoModeOptEasy.isChecked(): + scanMode = 'Easy' + else: + scanMode = 'Hard' + for hostAddOptionControl in hostAddOptionControls: + if hostAddOptionControl.isChecked(): + nmapOptionValue = str(hostAddOptionControl.toolTip()) + nmapOptionValueSplit = nmapOptionValue.split('[') + if len(nmapOptionValueSplit) > 1: + nmapOptionValue = nmapOptionValueSplit[1].replace(']','') + nmapOptions.append(nmapOptionValue) + nmapOptions.append(str(self.adddialog.txtCustomOptList.text())) + for hostListEntry in hostList: + self.controller.addHosts(targetHosts=hostListEntry, + runHostDiscovery=self.adddialog.chkDiscovery.isChecked(), + runStagedNmap=self.adddialog.chkNmapStaging.isChecked(), + nmapSpeed=self.adddialog.sldScanTimingSlider.value(), + scanMode=scanMode, + nmapOptions=nmapOptions) + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + else: + self.adddialog.spacer.changeSize(0,0) + self.adddialog.validationLabel.show() + self.adddialog.cmdAddButton.clicked.disconnect() # disconnect all the signals from that button + self.adddialog.cmdAddButton.clicked.connect(self.callAddHosts) + + ### + + def connectImportNmap(self): + self.ui.actionImportNmap.triggered.connect(self.importNmap) + + def importNmap(self): + self.ui.statusbar.showMessage('Importing nmap xml..', msecs=1000) + filename = QtWidgets.QFileDialog.getOpenFileName(self.ui.centralwidget, 'Choose nmap file', + self.controller.getCWD(), filter='XML file (*.xml)')[0] + log.info('Importing nmap xml from {0}...'.format(str(filename))) + if not filename == '': + if not os.access(filename, os.R_OK): # check for read permissions on the xml file + log.info('Insufficient permissions to read this file.') + QtWidgets.QMessageBox.warning(self.ui.centralwidget, 'Warning', + "You don't have the necessary permissions to read this file.", + "Ok") + return + + self.controller.nmapImporter.setFilename(str(filename)) + self.controller.nmapImporter.start() + self.controller.copyNmapXMLToOutputFolder(str(filename)) + else: + log.info('No file chosen..') + + def connectSettings(self): + self.ui.actionSettings.triggered.connect(self.showSettingsWidget) + + def showSettingsWidget(self): + self.settingsWidget.resetTabIndexes() + self.settingsWidget.show() + + def applySettings(self): + if self.settingsWidget.applySettings(): + self.controller.applySettings(self.settingsWidget.settings) + self.settingsWidget.hide() + + def cancelSettings(self): + log.debug('Cancel button pressed') # LEO: we can use this later to test ESC button once implemented. + self.settingsWidget.hide() + self.controller.cancelSettings() + + def connectHelp(self): + self.ui.actionHelp.triggered.connect(self.helpDialog.show) + + def connectConfig(self): + self.ui.actionConfig.triggered.connect(self.configDialog.show) + + def connectAppExit(self): + self.ui.actionExit.triggered.connect(self.appExit) + + def appExit(self): + if self.dealWithCurrentProject(True): # the parameter indicates that we are exiting the application + self.closeProject() + log.info('Exiting application..') + #self.loop.quit() + #self.app.quit() + from PyQt6.QtCore import QCoreApplication + QCoreApplication.quit() + #sys.exit(0) + + ### TABLE ACTIONS ### + + def connectAddHostsOverlayClick(self): + self.ui.addHostsOverlay.selectionChanged.connect(self.connectAddHostsDialog) + + def connectHostTableClick(self): + self.ui.HostsTableView.clicked.connect(self.hostTableClick) + + # TODO: review - especially what tab is selected when coming from another host + def hostTableClick(self): + if self.ui.HostsTableView.selectionModel().selectedRows(): # get the IP address of the selected host (if any) + row = self.ui.HostsTableView.selectionModel().selectedRows()[len(self.ui.HostsTableView. + selectionModel().selectedRows())-1].row() + ip = self.HostsTableModel.getHostIPForRow(row) + self.viewState.ip_clicked = ip + save = self.ui.ServicesTabWidget.currentIndex() + self.removeToolTabs() + self.restoreToolTabsForHost(self.viewState.ip_clicked) + # display services tab if we are coming from a dynamic tab (non-fixed) + self.ui.ServicesTabWidget.setCurrentIndex(save) + self.updateRightPanel(self.viewState.ip_clicked) + else: + self.removeToolTabs() + self.updateRightPanel('') + + ### + + def connectServiceNamesTableClick(self): + self.ui.ServiceNamesTableView.clicked.connect(self.serviceNamesTableClick) + + def hostTableDoubleClick(self, index): + # Get the item from the model using the index + model = self.ui.HostsTableView.model() + row = index.row() + new_index = model.index(row, 3) + data = model.data(new_index, QtCore.Qt.ItemDataRole.DisplayRole) + if data: + self.controller.copyToClipboard(data) + + def serviceNamesTableClick(self): + if self.ui.ServiceNamesTableView.selectionModel().selectedRows(): + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.updatePortsByServiceTableView(self.viewState.service_clicked) + + ### + + def connectToolsTableClick(self): + self.ui.ToolsTableView.clicked.connect(self.toolsTableClick) + + def toolsTableClick(self): + if self.ui.ToolsTableView.selectionModel().selectedRows(): + row = self.ui.ToolsTableView.selectionModel().selectedRows()[len( + self.ui.ToolsTableView.selectionModel().selectedRows())-1].row() + self.viewState.tool_clicked = self.ToolsTableModel.getToolNameForRow(row) + self.updateToolHostsTableView(self.viewState.tool_clicked) + # if we clicked on the screenshooter we need to display the screenshot widget + self.displayScreenshots(self.viewState.tool_clicked == 'screenshooter') + + # update the updateToolHostsTableView when the user closes all the host tabs + # TODO: this doesn't seem right + else: + self.updateToolHostsTableView('') + self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) + + ### + + def connectScriptTableClick(self): + self.ui.ScriptsTableView.clicked.connect(self.scriptTableClick) + + def scriptTableClick(self): + if self.ui.ScriptsTableView.selectionModel().selectedRows(): + row = self.ui.ScriptsTableView.selectionModel().selectedRows()[len( + self.ui.ScriptsTableView.selectionModel().selectedRows())-1].row() + self.viewState.script_clicked = self.ScriptsTableModel.getScriptDBIdForRow(row) + self.updateScriptsOutputView(self.viewState.script_clicked) + + ### + + def connectToolHostsClick(self): + self.ui.ToolHostsTableView.clicked.connect(self.toolHostsClick) + + # TODO: review / duplicate code + def toolHostsClick(self): + if self.ui.ToolHostsTableView.selectionModel().selectedRows(): + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + self.viewState.tool_host_clicked = self.ToolHostsTableModel.getProcessIdForRow(row) + ip = self.ToolHostsTableModel.getIpForRow(row) + + if self.viewState.tool_clicked == 'screenshooter': + filename = self.ToolHostsTableModel.getOutputfileForRow(row) + self.ui.ScreenshotWidget.open(str(self.controller.getOutputFolder())+'/screenshots/'+str(filename)) + + else: + # restore the tool output textview now showing in the tools display panel to its original host tool tab + self.restoreToolTabWidget() + + # remove the tool output currently in the tools display panel (if any) + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): + self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) + + tabs = [] # fetch tab list for this host (if any) + if str(ip) in self.viewState.hostTabs: + tabs = self.viewState.hostTabs[str(ip)] + + for tab in tabs: # place the tool output textview in the tools display panel + if tab.findChild(QtWidgets.QPlainTextEdit) and \ + str(tab.findChild(QtWidgets.QPlainTextEdit).property('dbId')) == \ + str(self.viewState.tool_host_clicked): + self.ui.DisplayWidgetLayout.addWidget(tab.findChild(QtWidgets.QPlainTextEdit)) + break + + ### + + def connectAddHostClick(self): + self.ui.AddHostButton.clicked.connect(self.connectAddHostsDialog) + + def connectAdvancedFilterClick(self): + self.ui.FilterAdvancedButton.clicked.connect(self.advancedFilterClick) + + def advancedFilterClick(self, current): + # to make sure we don't show filters than have been clicked but cancelled + self.filterdialog.setCurrentFilters(self.viewState.filters.getFilters()) + self.filterdialog.show() + + def updateFilter(self): + f = self.filterdialog.getFilters() + self.viewState.filters.apply(f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8]) + self.ui.keywordTextInput.setText(" ".join(f[8])) + self.updateInterface() + + def updateFilterKeywords(self): + self.viewState.filters.setKeywords(unicode(self.ui.keywordTextInput.text()).split()) + self.updateInterface() + + ### + + def connectTableDoubleClick(self): + self.ui.ServicesTableView.doubleClicked.connect(self.tableDoubleClick) + self.ui.ToolHostsTableView.doubleClicked.connect(self.tableDoubleClick) + self.ui.CvesTableView.doubleClicked.connect(self.rightTableDoubleClick) + + def rightTableDoubleClick(self, signal): + row = signal.row() # RETRIEVES ROW OF CELL THAT WAS DOUBLE CLICKED + column = signal.column() # RETRIEVES COLUMN OF CELL THAT WAS DOUBLE CLICKED + model = self.CvesTableModel + cell_dict = model.itemData(signal) # RETURNS DICT VALUE OF SIGNAL + cell_value = cell_dict.get(0) # RETRIEVE VALUE FROM DICT + + index = signal.sibling(row, 0) + index_dict = model.itemData(index) + index_value = index_dict.get(0) + log.info('Row {}, Column {} clicked - value: {}\nColumn 1 contents: {}' + .format(row, column, cell_value, index_value)) + + ## Does not work under WSL! + df = pd.DataFrame([cell_value]) + df.to_clipboard(index = False, header = False) + + + def tableDoubleClick(self): + tab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) + + if tab == 'Services': + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + ip = self.PortsByServiceTableModel.getIpForRow(row) + elif tab == 'Tools': + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + ip = self.ToolHostsTableModel.getIpForRow(row) + else: + return + + hostrow = self.HostsTableModel.getRowForIp(ip) + if hostrow is not None: + self.ui.HostsTabWidget.setCurrentIndex(0) + self.ui.HostsTableView.selectRow(hostrow) + self.hostTableClick() + + ### + + def connectSwitchTabClick(self): + self.ui.HostsTabWidget.currentChanged.connect(self.switchTabClick) + + def switchTabClick(self): + if self.ServiceNamesTableModel: # fixes bug when switching tabs at start-up + selectedTab = self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) + + if selectedTab == 'Hosts': + self.ui.ServicesTabWidget.insertTab(1,self.ui.ScriptsTab,("Scripts")) + self.ui.ServicesTabWidget.insertTab(2,self.ui.InformationTab,("Information")) + self.ui.ServicesTabWidget.insertTab(3,self.ui.CvesRightTab,("CVEs")) + self.ui.ServicesTabWidget.insertTab(4,self.ui.NotesTab,("Notes")) + self.ui.ServicesTabWidget.tabBar().setTabButton(0, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(1, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(2, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(3, QTabBar.ButtonPosition.RightSide, None) + self.ui.ServicesTabWidget.tabBar().setTabButton(4, QTabBar.ButtonPosition.RightSide, None) + + self.restoreToolTabWidget() + ### + if self.viewState.lazy_update_hosts == True: + self.updateHostsTableView() + ### + self.hostTableClick() + + elif selectedTab == 'Services': + self.ui.ServicesTabWidget.setCurrentIndex(0) + self.removeToolTabs(0) # remove the tool tabs + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + if self.viewState.lazy_update_services == True: + self.updateServiceNamesTableView() + self.serviceNamesTableClick() + + # Todo + #elif selectedTab == 'CVEs': + # self.ui.ServicesTabWidget.setCurrentIndex(0) + # self.removeToolTabs(0) # remove the tool tabs + # self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + # if self.viewState.lazy_update_services == True: + # self.updateServiceNamesTableView() + # self.serviceNamesTableClick() + + elif selectedTab == 'Tools': + self.updateToolsTableView() + + # display tool panel if we are in tools tab, hide it otherwise + self.displayToolPanel(selectedTab == 'Tools') + + ### + + def connectSwitchMainTabClick(self): + self.ui.MainTabWidget.currentChanged.connect(self.switchMainTabClick) + + def switchMainTabClick(self): + selectedTab = self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) + + if selectedTab == 'Scan': + self.switchTabClick() + + elif selectedTab == 'Brute': + self.ui.BruteTabWidget.currentWidget().runButton.setFocus() + self.restoreToolTabWidget() + + # in case the Brute tab was red because hydra found stuff, change it back to black + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor()) + + ### + # indicates that a context menu is showing so that the ui doesn't get updated disrupting the user + def setVisible(self): + self.viewState.menuVisible = True + + # indicates that a context menu has now closed and any pending ui updates can take place now + def setInvisible(self): + self.viewState.menuVisible = False + ### + + def connectHostsTableContextMenu(self): + self.ui.HostsTableView.customContextMenuRequested.connect(self.contextMenuHostsTableView) + + def contextMenuHostsTableView(self, pos): + if len(self.ui.HostsTableView.selectionModel().selectedRows()) > 0: + row = self.ui.HostsTableView.selectionModel().selectedRows()[ + len(self.ui.HostsTableView.selectionModel().selectedRows())-1].row() + # because when we right click on a different host, we need to select it + self.viewState.ip_clicked = self.HostsTableModel.getHostIPForRow(row) + self.ui.HostsTableView.selectRow(row) # select host when right-clicked + self.hostTableClick() + + menu, actions = self.controller.getContextMenuForHost( + str(self.HostsTableModel.getHostCheckStatusForRow(row))) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + hostid = self.HostsTableModel.getHostIdForRow(row) + action = menu.exec(self.ui.HostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) + + ### + + def connectServiceNamesTableContextMenu(self): + self.ui.ServiceNamesTableView.customContextMenuRequested.connect(self.contextMenuServiceNamesTableView) + + def contextMenuServiceNamesTableView(self, pos): + if len(self.ui.ServiceNamesTableView.selectionModel().selectedRows()) > 0: + row = self.ui.ServiceNamesTableView.selectionModel().selectedRows()[len( + self.ui.ServiceNamesTableView.selectionModel().selectedRows())-1].row() + self.viewState.service_clicked = self.ServiceNamesTableModel.getServiceNameForRow(row) + self.ui.ServiceNamesTableView.selectRow(row) # select service when right-clicked + self.serviceNamesTableClick() + + menu, actions, shiftPressed = self.controller.getContextMenuForServiceName(self.viewState.service_clicked) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + action = menu.exec(self.ui.ServiceNamesTableView.viewport().mapToGlobal(pos)) + + if action: + # because we will need to populate the right-side panel in order to select those rows + self.serviceNamesTableClick() + # we must only fetch the targets on which we haven't run the tool yet + tool = None + for i in range(0,len(actions)): # fetch the tool name + if action == actions[i][1]: + srvc_num = actions[i][0] + tool = self.controller.getSettings().portActions[srvc_num][1] + break + + if action.text() == 'Take screenshot': + tool = 'screenshooter' + + targets = [] # get (IP,port,protocol) combinations for this service + for row in range(self.PortsByServiceTableModel.rowCount("")): + targets.append([self.PortsByServiceTableModel.getIpForRow(row), + self.PortsByServiceTableModel.getPortForRow(row), + self.PortsByServiceTableModel.getProtocolForRow(row)]) + + # if the user pressed SHIFT+Right-click, ignore the rule of only running the tool on targets on + # which we haven't ran it yet + if shiftPressed: + tool=None + + if tool: + # fetch the hosts that we already ran the tool on + hosts=self.controller.getHostsForTool(tool, 'FetchAll') + oldTargets = [] + for i in range(0,len(hosts)): + oldTargets.append([hosts[i][5], hosts[i][6], hosts[i][7]]) + + # remove from the targets the hosts:ports we have already run the tool on + for host in oldTargets: + if host in targets: + targets.remove(host) + + self.controller.handleServiceNameAction(targets, actions, action) + + ### + + def connectToolHostsTableContextMenu(self): + self.ui.ToolHostsTableView.customContextMenuRequested.connect(self.contextToolHostsTableContextMenu) + + def contextToolHostsTableContextMenu(self, pos): + if len(self.ui.ToolHostsTableView.selectionModel().selectedRows()) > 0: + + row = self.ui.ToolHostsTableView.selectionModel().selectedRows()[len( + self.ui.ToolHostsTableView.selectionModel().selectedRows())-1].row() + ip = self.ToolHostsTableModel.getIpForRow(row) + port = self.ToolHostsTableModel.getPortForRow(row) + + if port: + serviceName = self.controller.getServiceNameForHostAndPort(ip, port)[0] + + menu, actions, terminalActions = self.controller.getContextMenuForPort(str(serviceName)) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + # this can handle multiple host selection if we apply it in the future + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + # context menu when the left services tab is selected + for row in self.ui.ToolHostsTableView.selectionModel().selectedRows(): + targets.append([self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()), + self.ToolHostsTableModel.getProtocolForRow(row.row()), + self.controller.getServiceNameForHostAndPort( + self.ToolHostsTableModel.getIpForRow(row.row()), + self.ToolHostsTableModel.getPortForRow(row.row()))[0]]) + restore = True + + action = menu.exec(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handlePortAction(targets, actions, terminalActions, action, restore) + + else: # in case there was no port, we show the host menu (without the portscan / mark as checked) + menu, actions = self.controller.getContextMenuForHost(str( + self.HostsTableModel.getHostCheckStatusForRow(self.HostsTableModel.getRowForIp(ip))), False) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + hostid = self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(ip)) + + action = menu.exec(self.ui.ToolHostsTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleHostAction(self.viewState.ip_clicked, hostid, actions, action) + + ### + + def connectServicesTableContextMenu(self): + self.ui.ServicesTableView.customContextMenuRequested.connect(self.contextMenuServicesTableView) + + # this function is longer because there are two cases we are in the services table + def contextMenuServicesTableView(self, pos): + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) > 0: + # if there is only one row selected, get service name + if len(self.ui.ServicesTableView.selectionModel().selectedRows()) == 1: + row = self.ui.ServicesTableView.selectionModel().selectedRows()[len( + self.ui.ServicesTableView.selectionModel().selectedRows())-1].row() + + if self.ui.ServicesTableView.isColumnHidden(0): # if we are in the services tab of the hosts view + serviceName = self.ServicesTableModel.getServiceNameForRow(row) + else: # if we are in the services tab of the services view + serviceName = self.PortsByServiceTableModel.getServiceNameForRow(row) + + else: + serviceName = '*' # otherwise show full menu + + menu, actions, terminalActions = self.controller.getContextMenuForPort(serviceName) + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + targets = [] # get (IP,port,protocol,serviceName) combinations for each selected row + if self.ui.ServicesTableView.isColumnHidden(0): + for row in self.ui.ServicesTableView.selectionModel().selectedRows(): + targets.append([self.ServicesTableModel.getIpForRow(row.row()), + self.ServicesTableModel.getPortForRow(row.row()), + self.ServicesTableModel.getProtocolForRow(row.row()), + self.ServicesTableModel.getServiceNameForRow(row.row())]) + restore = False + + else: # context menu when the left services tab is selected + for row in self.ui.ServicesTableView.selectionModel().selectedRows(): + targets.append([self.PortsByServiceTableModel.getIpForRow(row.row()), + self.PortsByServiceTableModel.getPortForRow(row.row()), + self.PortsByServiceTableModel.getProtocolForRow(row.row()), + self.PortsByServiceTableModel.getServiceNameForRow(row.row())]) + restore = True + + action = menu.exec(self.ui.ServicesTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handlePortAction(targets, actions, terminalActions, action, restore) + + ### + + def connectProcessesTableContextMenu(self): + self.ui.ProcessesTableView.customContextMenuRequested.connect(self.contextMenuProcessesTableView) + + def contextMenuProcessesTableView(self, pos): + if self.ui.ProcessesTableView.selectionModel() and self.ui.ProcessesTableView.selectionModel().selectedRows(): + + menu = self.controller.getContextMenuForProcess() + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + selectedProcesses = [] # list of tuples (pid, status, procId) + for row in self.ui.ProcessesTableView.selectionModel().selectedRows(): + pid = self.ProcessesTableModel.getProcessPidForRow(row.row()) + selectedProcesses.append([int(pid), self.ProcessesTableModel.getProcessStatusForRow(row.row()), + self.ProcessesTableModel.getProcessIdForRow(row.row())]) + + action = menu.exec(self.ui.ProcessesTableView.viewport().mapToGlobal(pos)) + + if action: + self.controller.handleProcessAction(selectedProcesses, action) + + ### + + def connectScreenshotContextMenu(self): + self.ui.ScreenshotWidget.scrollArea.customContextMenuRequested.connect(self.contextMenuScreenshot) + + def contextMenuScreenshot(self, pos): + menu = QMenu() + + zoomInAction = menu.addAction("Zoom in (25%)") + zoomOutAction = menu.addAction("Zoom out (25%)") + fitToWindowAction = menu.addAction("Fit to window") + normalSizeAction = menu.addAction("Original size") + + menu.aboutToShow.connect(self.setVisible) + menu.aboutToHide.connect(self.setInvisible) + + action = menu.exec(self.ui.ScreenshotWidget.scrollArea.viewport().mapToGlobal(pos)) + + if action == zoomInAction: + self.ui.ScreenshotWidget.zoomIn() + elif action == zoomOutAction: + self.ui.ScreenshotWidget.zoomOut() + elif action == fitToWindowAction: + self.ui.ScreenshotWidget.fitToWindow() + elif action == normalSizeAction: + self.ui.ScreenshotWidget.normalSize() + + #################### LEFT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def updateHostsTableView(self): + # Update the data source of the model with the hosts from the database + self.HostsTableModel.setHosts(self.controller.getHostsFromDB(self.viewState.filters)) + + # Set the viewState.lazy_update_hosts to False to indicate that it doesn't need to be updated anymore + self.viewState.lazy_update_hosts = False + + ## Resize the OS column of the HostsTableView + #self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + + # Sort the model by the Host column in descending order + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + + # Get the list of IPs from the model + ips = [] # ensure that there is always something selected + for row in range(self.HostsTableModel.rowCount("")): + ips.append(self.HostsTableModel.getHostIPForRow(row)) + + # Check if the IP we previously clicked is still visible + if self.viewState.ip_clicked in ips: + # Get the row for the IP we previously clicked + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) + else: + # Select the first row + row = 0 + + # Check if the row is not None + if row is not None: + # Select the row in the HostsTableView + self.ui.HostsTableView.selectRow(row) + # Call the hostTableClick() method + self.hostTableClick() + + # Resize the OS column of the HostsTableView + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + + # Hide colmns we don't want + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: + self.ui.HostsTableView.hideColumn(i) + + def updateHostsTableViewX(self): + headers = ["Id", "OS", "Accuracy", "Host", "IPv4", "IPv6", "Mac", "Status", "Hostname", "Vendor", "Uptime", + "Lastboot", "Distance", "CheckedHost", "Country Code", "State", "City", "Latitude", "Longitude", + "Count", "Closed"] + self.HostsTableModel = HostsTableModel(self.controller.getHostsFromDB(self.viewState.filters), headers) + self.ui.HostsTableView.setModel(self.HostsTableModel) + #self.HostsTableModel.setHosts(self.controller.getHostsFromDB(self.viewState.filters)) + + self.viewState.lazy_update_hosts = False # to indicate that it doesn't need to be updated anymore + + # hide some columns + for i in [0, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]: + self.ui.HostsTableView.setColumnHidden(i, True) + + self.ui.HostsTableView.horizontalHeader().resizeSection(1, 30) + self.HostsTableModel.sort(3, Qt.SortOrder.DescendingOrder) + + self.ui.HostsTableView.repaint() + self.ui.HostsTableView.update() + + ips = [] # ensure that there is always something selected + for row in range(self.HostsTableModel.rowCount("")): + ips.append(self.HostsTableModel.getHostIPForRow(row)) + + # the ip we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.ip_clicked in ips: + row = self.HostsTableModel.getRowForIp(self.viewState.ip_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.HostsTableView.selectRow(row) + self.hostTableClick() + + def updateServiceNamesTableView(self): + headers = ["Name"] + self.ServiceNamesTableModel = ServiceNamesTableModel( + self.controller.getServiceNamesFromDB(self.viewState.filters), headers) + self.ui.ServiceNamesTableView.setModel(self.ServiceNamesTableModel) + + self.viewState.lazy_update_services = False # to indicate that it doesn't need to be updated anymore + + services = [] # ensure that there is always something selected + for row in range(self.ServiceNamesTableModel.rowCount("")): + services.append(self.ServiceNamesTableModel.getServiceNameForRow(row)) + + # the service we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.service_clicked in services: + row = self.ServiceNamesTableModel.getRowForServiceName(self.viewState.service_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ServiceNamesTableView.selectRow(row) + self.serviceNamesTableClick() + + def setupToolsTableView(self): + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolsTableModel = ProcessesTableModel(self, self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses='noNmap', + sort=self.toolsTableViewSort, + ncol=self.toolsTableViewSortColumn), headers) + self.ui.ToolsTableView.setModel(self.ToolsTableModel) + + def updateToolsTableView(self): + if self.ui.MainTabWidget.tabText(self.ui.MainTabWidget.currentIndex()) == 'Scan' and \ + self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.ToolsTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, + showProcesses = 'noNmap', + sort = self.toolsTableViewSort, + ncol = self.toolsTableViewSortColumn)) + self.ui.ToolsTableView.repaint() + self.ui.ToolsTableView.update() + + self.viewState.lazy_update_tools = False # to indicate that it doesn't need to be updated anymore + + # Hides columns we don't want to see + for i in [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]: # hide some columns + self.ui.ToolsTableView.setColumnHidden(i, True) + + tools = [] # ensure that there is always something selected + for row in range(self.ToolsTableModel.rowCount("")): + tools.append(self.ToolsTableModel.getToolNameForRow(row)) + + # the tool we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_clicked in tools: + row = self.ToolsTableModel.getRowForToolName(self.viewState.tool_clicked) + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ToolsTableView.selectRow(row) + self.toolsTableClick() + + #################### RIGHT PANEL INTERFACE UPDATE FUNCTIONS #################### + + def updateServiceTableView(self, hostIP): + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.ServicesTableModel = ServicesTableModel( + self.controller.getPortsAndServicesForHostFromDB(hostIP, self.viewState.filters), headers) + self.ui.ServicesTableView.setModel(self.ServicesTableModel) + + for i in range(0, len(headers)): # reset all the hidden columns + self.ui.ServicesTableView.setColumnHidden(i, False) + + for i in [0,1,5,6,8,10,11]: # hide some columns + self.ui.ServicesTableView.setColumnHidden(i, True) + + self.ServicesTableModel.sort(2, Qt.SortOrder.DescendingOrder) # sort by port by default (override default) + + def updatePortsByServiceTableView(self, serviceName): + headers = ["Host", "Port", "Port", "Protocol", "State", "HostId", "ServiceId", "Name", "Product", "Version", + "Extrainfo", "Fingerprint"] + self.PortsByServiceTableModel = ServicesTableModel( + self.controller.getHostsAndPortsForServiceFromDB(serviceName, self.viewState.filters), headers) + self.ui.ServicesTableView.setModel(self.PortsByServiceTableModel) + + for i in range(0, len(headers)): # reset all the hidden columns + self.ui.ServicesTableView.setColumnHidden(i, False) + + for i in [2,5,6,7,8,10,11]: # hide some columns + self.ui.ServicesTableView.setColumnHidden(i, True) + + self.ui.ServicesTableView.horizontalHeader().resizeSection(0,165) # resize IP + self.ui.ServicesTableView.horizontalHeader().resizeSection(1,65) # resize port + self.ui.ServicesTableView.horizontalHeader().resizeSection(3,100) # resize protocol + self.PortsByServiceTableModel.sort(0, Qt.SortOrder.DescendingOrder) # sort by IP by default (override default) + + def updateInformationView(self, hostIP): + + if hostIP: + host = self.controller.getHostInformation(hostIP) + + if host: + states = self.controller.getPortStatesForHost(host.id) + counterOpen = counterClosed = counterFiltered = 0 + + for s in states: + if s[0] == 'open': + counterOpen+=1 + elif s[0] == 'closed': + counterClosed+=1 + else: + counterFiltered+=1 + + if host.state == 'closed': # check the extra ports + counterClosed = 65535 - counterOpen - counterFiltered + else: + counterFiltered = 65535 - counterOpen - counterClosed + + self.hostInfoWidget.updateFields(status=host.status, openPorts=counterOpen, closedPorts=counterClosed, + filteredPorts=counterFiltered, ipv4=host.ipv4, ipv6=host.ipv6, + macaddr=host.macaddr, osMatch=host.osMatch, osAccuracy=host.osAccuracy, + vendor=host.vendor, asn=host.asn, isp=host.isp, + countryCode=host.countryCode, city=host.city, latitude=host.latitude, + longitude=host.longitude) + + def updateScriptsView(self, hostIP): + headers = ["Id", "Script", "Port", "Protocol"] + self.ScriptsTableModel = ScriptsTableModel(self,self.controller.getScriptsFromDB(hostIP), headers) + self.ui.ScriptsTableView.setModel(self.ScriptsTableModel) + + for i in [0,3]: # hide some columns + self.ui.ScriptsTableView.setColumnHidden(i, True) + + scripts = [] # ensure that there is always something selected + for row in range(self.ScriptsTableModel.rowCount("")): + scripts.append(self.ScriptsTableModel.getScriptDBIdForRow(row)) + + # the script we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.script_clicked in scripts: + row = self.ScriptsTableModel.getRowForDBId(self.viewState.script_clicked) + + else: + row = 0 # or select the first row + + if not row == None: + self.ui.ScriptsTableView.selectRow(row) + self.scriptTableClick() + + self.ui.ScriptsTableView.repaint() + self.ui.ScriptsTableView.update() + + def updateCvesByHostView(self, hostIP): + headers = ["CVE Id", "CVSS Score", "Product", "Version", "CVE URL", "Source", "ExploitDb ID", "ExploitDb", + "ExploitDb URL"] + cves = self.controller.getCvesFromDB(hostIP) + self.CvesTableModel = CvesTableModel(self, cves, headers) + + self.ui.CvesTableView.horizontalHeader().resizeSection(0,175) + self.ui.CvesTableView.horizontalHeader().resizeSection(2,175) + self.ui.CvesTableView.horizontalHeader().resizeSection(4,225) + + self.ui.CvesTableView.setModel(self.CvesTableModel) + self.ui.CvesTableView.repaint() + self.ui.CvesTableView.update() + + def updateScriptsOutputView(self, scriptId): + self.ui.ScriptsOutputTextEdit.clear() + lines = self.controller.getScriptOutputFromDB(scriptId) + for line in lines: + self.ui.ScriptsOutputTextEdit.insertPlainText(line.output.rstrip()) + + # TODO: check if this hack can be improved because we are calling setDirty more than we need + def updateNotesView(self, hostid): + self.viewState.lastHostIdClicked = str(hostid) + note = self.controller.getNoteFromDB(hostid) + + saved_dirty = self.viewState.dirty # save the status so we can restore it after we update the note panel + self.ui.NotesTextEdit.clear() # clear the text box from the previous notes + + if note: + self.ui.NotesTextEdit.insertPlainText(note.text) + + if saved_dirty == False: + self.setDirty(False) + + def updateToolHostsTableView(self, toolname): + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ToolHostsTableModel = ProcessesTableModel(self, self.controller.getHostsForTool(toolname), headers) + self.ui.ToolHostsTableView.setModel(self.ToolHostsTableModel) + + for i in [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15]: # hide some columns + self.ui.ToolHostsTableView.setColumnHidden(i, True) + + self.ui.ToolHostsTableView.horizontalHeader().resizeSection(7, 150) # default width for Host column + + ids = [] # ensure that there is always something selected + for row in range(self.ToolHostsTableModel.rowCount("")): + ids.append(self.ToolHostsTableModel.getProcessIdForRow(row)) + + # the host we previously clicked may not be visible anymore (eg: due to filters) + if self.viewState.tool_host_clicked in ids: + row = self.ToolHostsTableModel.getRowForDBId(self.viewState.tool_host_clicked) + + else: + row = 0 # or select the first row + + if not row == None and self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.ui.ToolHostsTableView.selectRow(row) + self.toolHostsClick() + + def updateRightPanel(self, hostIP): + self.updateServiceTableView(hostIP) + self.updateScriptsView(hostIP) + self.updateCvesByHostView(hostIP) + self.updateInformationView(hostIP) # populate host info tab + self.controller.saveProject(self.viewState.lastHostIdClicked, self.ui.NotesTextEdit.toPlainText()) + + if hostIP: + self.updateNotesView(self.HostsTableModel.getHostIdForRow(self.HostsTableModel.getRowForIp(hostIP))) + else: + self.updateNotesView('') + + def displayToolPanel(self, display=False): + size = self.ui.splitter.parentWidget().width() - self.leftPanelSize - 24 # note: 24 is a fixed value + if display: + self.ui.ServicesTabWidget.hide() + self.ui.splitter_3.show() + self.ui.splitter.setSizes([self.leftPanelSize, 0, size]) # reset hoststableview width + + if self.viewState.tool_clicked == 'screenshooter': + self.displayScreenshots(True) + else: + self.displayScreenshots(False) + #self.ui.splitter_3.setSizes([275,size-275,0]) # reset middle panel width + + else: + self.ui.splitter_3.hide() + self.ui.ServicesTabWidget.show() + self.ui.splitter.setSizes([self.leftPanelSize, size, 0]) + + def displayScreenshots(self, display=False): + size = self.ui.splitter.parentWidget().width() - self.leftPanelSize - 24 # note: 24 is a fixed value + + if display: + self.ui.DisplayWidget.hide() + self.ui.ScreenshotWidget.scrollArea.show() + self.ui.splitter_3.setSizes([275, 0, size - 275]) # reset middle panel width + + else: + self.ui.ScreenshotWidget.scrollArea.hide() + self.ui.DisplayWidget.show() + self.ui.splitter_3.setSizes([275, size - 275, 0]) # reset middle panel width + + def displayAddHostsOverlay(self, display=False): + if display: + self.ui.addHostsOverlay.show() + self.ui.HostsTableView.hide() + else: + self.ui.addHostsOverlay.hide() + self.ui.HostsTableView.show() + + #################### BOTTOM PANEL INTERFACE UPDATE FUNCTIONS #################### + + def setupProcessesTableView(self): + headers = ["Progress", "Display", "Elapsed", "Est. Remaining", "Pid", "Name", "Tool", "Host", "Port", + "Protocol", "Command", "Start time", "End time", "OutputFile", "Output", "Status", "Closed"] + self.ProcessesTableModel = ProcessesTableModel(self,self.controller.getProcessesFromDB( + self.viewState.filters, showProcesses = True, sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn), headers) + self.ui.ProcessesTableView.setModel(self.ProcessesTableModel) + self.ProcessesTableModel.sort(15, Qt.SortOrder.DescendingOrder) + + def updateProcessesTableView(self): + self.ProcessesTableModel.setDataList( + self.controller.getProcessesFromDB(self.viewState.filters, showProcesses = True, + sort = self.processesTableViewSort, + ncol = self.processesTableViewSortColumn)) + self.ui.ProcessesTableView.repaint() + self.ui.ProcessesTableView.update() + + # load the column widths from settings to persist widths between sessions + columnWidths = self.controller.getSettings().gui_process_tab_column_widths.split(',') + header = self.ui.ProcessesTableView.horizontalHeader() + for index, width in enumerate(columnWidths): + header.resizeSection(index, int(width)) + + #Hides columns we don't want to see + showDetail = self.controller.settings.gui_process_tab_detail + if showDetail == True: + columnsToHide = [1, 5, 8, 9, 12, 14, 16] + else: + columnsToHide = [1, 5, 8, 9, 10, 11, 12, 13, 14, 16] + for i in columnsToHide: + self.ui.ProcessesTableView.setColumnHidden(i, True) + + # Force size of progress animation + self.ui.ProcessesTableView.horizontalHeader().resizeSection(0, 125) + self.ui.ProcessesTableView.horizontalHeader().resizeSection(15, 125) + + # Update animations + self.updateProcessesIcon() + + def updateProcessesIcon(self): + if self.ProcessesTableModel: + for row in range(len(self.ProcessesTableModel.getProcesses())): + status = self.ProcessesTableModel.getProcesses()[row].status + + directStatus = {'Waiting':'waiting', 'Running':'running', 'Finished':'finished', 'Crashed':'killed'} + defaultStatus = 'killed' + + processIconName = directStatus.get(status) or defaultStatus + processIcon = './images/{processIconName}.gif'.format(processIconName=processIconName) + + self.runningWidget = ImagePlayer(processIcon) + self.ui.ProcessesTableView.setIndexWidget(self.ui.ProcessesTableView.model().index(row,0), + self.runningWidget) + + #################### GLOBAL INTERFACE UPDATE FUNCTION #################### + + # TODO: when nmap file is imported select last IP clicked (or first row if none) + def updateInterface(self): + self.ui_mainwindow.show() + + if self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Hosts': + self.updateHostsTableView() + self.viewState.lazy_update_services = True + self.viewState.lazy_update_tools = True + + elif self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Services': + self.updateServiceNamesTableView() + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_tools = True + + elif self.ui.HostsTabWidget.tabText(self.ui.HostsTabWidget.currentIndex()) == 'Tools': + self.updateToolsTableView() + self.viewState.lazy_update_hosts = True + self.viewState.lazy_update_services = True + + #################### TOOL TABS #################### + + # this function creates a new tool tab for a given host + # TODO: refactor/review, especially the restoring part. we should not check if toolname=nmap everywhere in the code + # ..maybe we should do it here. rethink + def createNewTabForHost(self, ip, tabTitle, restoring=False, content='', filename=''): + # TODO: use regex otherwise tools with 'screenshot' in the name are screwed. + if 'screenshot' in str(tabTitle): + tempWidget = ImageViewer() + tempWidget.setObjectName(str(tabTitle)) + tempWidget.open(str(filename)) + tempTextView = tempWidget.scrollArea + tempTextView.setObjectName(str(tabTitle)) + else: + tempWidget = QtWidgets.QWidget() + tempWidget.setObjectName(str(tabTitle)) + tempTextView = QtWidgets.QPlainTextEdit(tempWidget) + tempTextView.setReadOnly(True) + if self.controller.getSettings().general_tool_output_black_background == 'True': + p = tempTextView.palette() + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font + tempTextView.setPalette(p) + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") + tempLayout = QtWidgets.QHBoxLayout(tempWidget) + tempLayout.addWidget(tempTextView) + + if not content == '': # if there is any content to display + tempTextView.appendPlainText(content) + + # if restoring tabs (after opening a project) don't show the tab in the ui + if restoring == False: + self.ui.ServicesTabWidget.addTab(tempWidget, str(tabTitle)) + + hosttabs = [] # fetch tab list for this host (if any) + if str(ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(ip)] + + if 'screenshot' in str(tabTitle): + hosttabs.append(tempWidget.scrollArea) # add the new tab to the list + else: + hosttabs.append(tempWidget) # add the new tab to the list + + self.viewState.hostTabs.update({str(ip):hosttabs}) + + return tempTextView + + + def createNewConsole(self, tabTitle, content='Hello\n', filename=''): + + tempWidget = QtWidgets.QWidget() + tempWidget.setObjectName(str(tabTitle)) + tempTextView = QtWidgets.QPlainTextEdit(tempWidget) + tempTextView.setReadOnly(True) + if self.controller.getSettings().general_tool_output_black_background == 'True': + p = tempTextView.palette() + p.setColor(QtGui.QPalette.ColorRole.Base, Qt.GlobalColor.black) # black background + p.setColor(QtGui.QPalette.ColorRole.Text, Qt.GlobalColor.white) # white font + tempTextView.setPalette(p) + # font-size:18px; width: 150px; color:red; left: 20px;}"); # set the menu font color: black + tempTextView.setStyleSheet("QMenu { color:black;}") + tempLayout = QtWidgets.QHBoxLayout(tempWidget) + tempLayout.addWidget(tempTextView) + self.ui.PythonTabLayout.addWidget(tempWidget) + + if not content == '': # if there is any content to display + tempTextView.appendPlainText(content) + + + return tempTextView + + def closeHostToolTab(self, index): + currentTabIndex = self.ui.ServicesTabWidget.currentIndex() # remember the currently selected tab + self.ui.ServicesTabWidget.setCurrentIndex(index) # select the tab for which the cross button was clicked + + currentWidget = self.ui.ServicesTabWidget.currentWidget() + if 'screenshot' in str(self.ui.ServicesTabWidget.currentWidget().objectName()): + dbId = int(currentWidget.property('dbId')) + else: + dbId = int(currentWidget.findChild(QtWidgets.QPlainTextEdit).property('dbId')) + + pid = int(self.controller.getPidForProcess(dbId)) # the process ID (=os) + + if str(self.controller.getProcessStatusForDBId(dbId)) == 'Running': + message = "This process is still running. Are you sure you want to kill it?" + reply = self.yesNoDialog(message, 'Confirm') + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + self.controller.killProcess(pid, dbId) + else: + return + + # TODO: duplicate code + if str(self.controller.getProcessStatusForDBId(dbId)) == 'Waiting': + message = "This process is waiting to start. Are you sure you want to cancel it?" + reply = self.yesNoDialog(message, 'Confirm') + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + self.controller.cancelProcess(dbId) + else: + return + + # remove tab from host tabs list + hosttabs = [] + for ip in self.viewState.hostTabs.keys(): + if self.ui.ServicesTabWidget.currentWidget() in self.viewState.hostTabs[ip]: + hosttabs = self.viewState.hostTabs[ip] + hosttabs.remove(self.ui.ServicesTabWidget.currentWidget()) + self.viewState.hostTabs.update({ip:hosttabs}) + break + + self.controller.storeCloseTabStatusInDB(dbId) # update the closed status in the db - getting the dbid + self.ui.ServicesTabWidget.removeTab(index) # remove the tab + + if currentTabIndex >= self.ui.ServicesTabWidget.currentIndex(): # select the initially selected tab + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex - 1) + else: + self.ui.ServicesTabWidget.setCurrentIndex(currentTabIndex) + + # this function removes tabs that were created when running tools (starting from the end to avoid index problems) + def removeToolTabs(self, position=-1): + if position == -1: + position = self.fixedTabsCount-1 + for i in range(self.ui.ServicesTabWidget.count()-1, position, -1): + self.ui.ServicesTabWidget.removeTab(i) + + # this function restores the tool tabs based on the DB content (should be called when opening an existing project). + def restoreToolTabs(self): + # false means we are fetching processes with display flag=False, which is the case for every process once + # a project is closed. + tools = self.controller.getProcessesFromDB(self.viewState.filters, showProcesses=False) + nbr = len(tools) # show a progress bar because this could take long + if nbr==0: + nbr=1 + progress = 100.0 / nbr + totalprogress = 0 + self.tick.emit(int(totalprogress)) + for t in tools: + if not t.tabTitle == '': + if 'screenshot' in str(t.tabTitle): + imageviewer = self.createNewTabForHost( + t.hostIp, t.tabTitle, True, '', + str(self.controller.getOutputFolder())+'/screenshots/'+str(t.outputfile)) + imageviewer.setObjectName(str(t.tabTitle)) + imageviewer.setProperty('dbId', str(t.id)) + else: + # True means we are restoring tabs. Set the widget's object name to the DB id of the process + self.createNewTabForHost(t.hostIp, t.tabTitle, True, t.output).setProperty('dbId', str(t.id)) + + totalprogress += progress # update the progress bar + self.tick.emit(int(totalprogress)) + + def restoreToolTabsForHost(self, ip): + if (self.viewState.hostTabs) and (ip in self.viewState.hostTabs): + tabs = self.viewState.hostTabs[ip] # use the ip as a key to retrieve its list of tooltabs + for tab in tabs: + # do not display hydra and nmap tabs when restoring for that host + if 'hydra' not in tab.objectName() and 'nmap' not in tab.objectName(): + self.ui.ServicesTabWidget.addTab(tab, tab.objectName()) + + # this function restores the textview widget (now in the tools display widget) to its original tool tab + # (under the correct host) + def restoreToolTabWidget(self, clear=False): + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit) == self.ui.toolOutputTextView: + return + + for host in self.viewState.hostTabs.keys(): + hosttabs = self.viewState.hostTabs[host] + for tab in hosttabs: + if 'screenshot' not in str(tab.objectName()) and not tab.findChild(QtWidgets.QPlainTextEdit): + tab.layout().addWidget(self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit)) + break + + if clear: + # remove the tool output currently in the tools display panel + if self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit): + self.ui.DisplayWidget.findChild(QtWidgets.QPlainTextEdit).setParent(None) + + self.ui.DisplayWidgetLayout.addWidget(self.ui.toolOutputTextView) + + #################### BRUTE TABS #################### + + def createNewBruteTab(self, ip, port, service): + self.ui.statusbar.showMessage('Sending to Brute: '+str(ip)+':'+str(port)+' ('+str(service)+')', msecs=1000) + bWidget = BruteWidget(ip, port, service, self.controller.getSettings()) + bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) + self.ui.BruteTabWidget.addTab(bWidget, str(self.viewState.bruteTabCount)) + self.viewState.bruteTabCount += 1 # update tab count + # show the last added tab in the brute widget + self.ui.BruteTabWidget.setCurrentIndex(self.ui.BruteTabWidget.count()-1) + + def closeBruteTab(self, index): + currentTabIndex = self.ui.BruteTabWidget.currentIndex() # remember the currently selected tab + # select the tab for which the cross button was clicked + self.ui.BruteTabWidget.setCurrentIndex(index) + + if not self.ui.BruteTabWidget.currentWidget().pid == -1: # if process is running + if self.ProcessesTableModel.getProcessStatusForPid(self.ui.BruteTabWidget.currentWidget().pid)=="Running": + message = "This process is still running. Are you sure you want to kill it?" + reply = self.yesNoDialog(message, 'Confirm') + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + self.killBruteProcess(self.ui.BruteTabWidget.currentWidget()) + else: + return + + dbIdString = self.ui.BruteTabWidget.currentWidget().display.property('dbId') + if dbIdString: + if not dbIdString == '': + self.controller.storeCloseTabStatusInDB(int(dbIdString)) + + self.ui.BruteTabWidget.removeTab(index) # remove the tab + + if currentTabIndex >= self.ui.BruteTabWidget.currentIndex(): # select the initially selected tab + # all the tab indexes shift if we remove a tab index smaller than the current tab index + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex - 1) + else: + self.ui.BruteTabWidget.setCurrentIndex(currentTabIndex) + + if self.ui.BruteTabWidget.count() == 0: # if the last tab was removed, add default tab + self.createNewBruteTab('127.0.0.1', '22', 'ssh') + + def resetBruteTabs(self): + count = self.ui.BruteTabWidget.count() + for i in range(0, count): + self.ui.BruteTabWidget.removeTab(count -i -1) + self.createNewBruteTab('127.0.0.1', '22', 'ssh') + + # TODO: show udp in tabTitle when udp service + def callHydra(self, bWidget): + if validateNmapInput(bWidget.ipTextinput.text()) and validateNmapInput(bWidget.portTextinput.text()): + # check if host is already in scope + if not self.controller.isHostInDB(bWidget.ipTextinput.text()): + message = "This host is not in scope. Add it to scope and continue?" + reply = self.yesNoDialog(message, 'Confirm') + if reply == QtWidgets.QMessageBox.StandardButton.No: + return + else: + log.info('Adding host to scope here!!') + self.controller.addHosts(str(bWidget.ipTextinput.text()).replace(';',' '), False, False, + "unset", "unset") + + bWidget.validationLabel.hide() + bWidget.toggleRunButton() + bWidget.resetDisplay() # fixes tab bug + + hydraCommand = bWidget.buildHydraCommand(self.controller.getRunningFolder(), + self.controller.getUserlistPath(), + self.controller.getPasslistPath()) + bWidget.setObjectName(str("hydra"+" ("+bWidget.getPort()+"/tcp)")) + + hosttabs = [] # add widget to host tabs (needed to be able to move the widget between brute/tools tabs) + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] + + hosttabs.append(bWidget) + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) + + bWidget.pid = self.controller.runCommand("hydra", bWidget.objectName(), bWidget.ip, bWidget.getPort(), + 'tcp', unicode(hydraCommand), getTimestamp(human=True), + bWidget.outputfile, bWidget.display) + bWidget.runButton.clicked.disconnect() + bWidget.runButton.clicked.connect(lambda: self.killBruteProcess(bWidget)) + + else: + bWidget.validationLabel.show() + + def killBruteProcess(self, bWidget): + dbId = str(bWidget.display.property('dbId')) + status = self.controller.getProcessStatusForDBId(dbId) + if status == "Running": # check if we need to kill or cancel + self.controller.killProcess(self.controller.getPidForProcess(dbId), dbId) + + elif status == "Waiting": + self.controller.cancelProcess(dbId) + self.bruteProcessFinished(bWidget) + + def bruteProcessFinished(self, bWidget): + bWidget.toggleRunButton() + bWidget.pid = -1 + + # disassociate textview from bWidget (create new textview for bWidget) and replace it with a new host tab + self.createNewTabForHost( + str(bWidget.ip), str(bWidget.objectName()), restoring=True, + content=unicode(bWidget.display.toPlainText())).setProperty('dbId', str(bWidget.display.property('dbId'))) + + hosttabs = [] # go through host tabs and find the correct bWidget + if str(bWidget.ip) in self.viewState.hostTabs: + hosttabs = self.viewState.hostTabs[str(bWidget.ip)] + + if hosttabs.count(bWidget) > 1: + hosttabs.remove(bWidget) + + self.viewState.hostTabs.update({str(bWidget.ip):hosttabs}) + + bWidget.runButton.clicked.disconnect() + bWidget.runButton.clicked.connect(lambda: self.callHydra(bWidget)) + + def findFinishedBruteTab(self, pid): + for i in range(0, self.ui.BruteTabWidget.count()): + if str(self.ui.BruteTabWidget.widget(i)) == pid: + self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + return + + def findFinishedServiceTab(self, pid): + for i in range(0, self.ui.ServicesTabWidget.count()): + if str(self.ui.ServicesTabWidget.widget(i)) == pid: + self.bruteProcessFinished(self.ui.BruteTabWidget.widget(i)) + log.info("Close Tab: {0}".format(str(i))) + return + + def blinkBruteTab(self, bWidget): + self.ui.MainTabWidget.tabBar().setTabTextColor(1, QtGui.QColor('red')) + for i in range(0, self.ui.BruteTabWidget.count()): + if self.ui.BruteTabWidget.widget(i) == bWidget: + self.ui.BruteTabWidget.tabBar().setTabTextColor(i, QtGui.QColor('red')) + return diff --git a/utilities.py b/utilities.py new file mode 100644 index 00000000..bf016df4 --- /dev/null +++ b/utilities.py @@ -0,0 +1,24 @@ +class DictObject(object): + ''' + Simple conversion to a Class. + ''' + def __init__(self, d): + for a, b in d.items(): + if isinstance(b, (list, tuple)): + setattr(self, a, [DictObject(x) if isinstance(x, dict) else x for x in b]) + else: + setattr(self, a, DictObject(b) if isinstance(b, dict) else b) + +class DictToObject(object): + def __init__(self, inputStructure:list): + self.outputStructure = self.create(inputStructure) + + def create(self, inputList:list)-> list: + outputStructure = [] + for entry in inputList: + outputStructure.append(DictObject(entry)) + return outputStructure + + def __repr__(self): + return '<{0}.incidents={1} object at {2}>'.format( + self.__class__.__name__, self.outputStructure, hex(id(self))) diff --git a/utilities/__init__.py b/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utilities/qtLogging.py b/utilities/qtLogging.py new file mode 100644 index 00000000..72d6ae21 --- /dev/null +++ b/utilities/qtLogging.py @@ -0,0 +1,20 @@ +from PyQt6 import QtWidgets +import logging + +class QPlainTextEditLogger(logging.Handler): + def __init__(self, parent): + super().__init__() + self.widget = QtWidgets.QPlainTextEdit(parent) + #self.widget.setReadOnly(True) + #self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + #self.sizePolicy.setHorizontalStretch(1) + #self.sizePolicy.setVerticalStretch(1) + #self.widget.setSizePolicy(self.sizePolicy) + #self.widget.setGeometry(0, 0, 200, 400) + + def emit(self, record): + msg = self.format(record) + self.widget.appendPlainText(msg) + + def append(self, msg): + self.widget.appendPlainText(msg) diff --git a/wordlists/db2-betterdefaultpasslist.txt b/wordlists/db2-betterdefaultpasslist.txt new file mode 100644 index 00000000..9d9c7997 --- /dev/null +++ b/wordlists/db2-betterdefaultpasslist.txt @@ -0,0 +1,8 @@ +ADONIS:BPMS +db2inst1:db2inst1 +db2inst1:db2pass +db2inst1:db2pw +db2inst1:db2password +dasusr1:dasusr1 +db2fenc1:db2fenc1 +db2admin:db2admin diff --git a/wordlists/ftp-betterdefaultpasslist.txt b/wordlists/ftp-betterdefaultpasslist.txt new file mode 100644 index 00000000..5f5f90dd --- /dev/null +++ b/wordlists/ftp-betterdefaultpasslist.txt @@ -0,0 +1,66 @@ +anonymous:anonymous +root:rootpasswd +root:12hrs37 +ftp:b1uRR3 +admin:admin +localadmin:localadmin +admin:1234 +apc:apc +admin:nas +Root:wago +Admin:wago +User:user +Guest:guest +ftp:ftp +admin:password +a:avery +admin:123456 +adtec:none +admin:admin12345 +none:dpstelecom +instrument:instrument +user:password +root:password +default:default +admin:default +nmt:1234 +admin:Janitza +supervisor:supervisor +user1:pass1 +avery:avery +IEIeMerge:eMerge +ADMIN:12345 +beijer:beijer +Admin:admin +admin:1234 +admin:1111 +root:admin +se:1234 +admin:stingray +device:apc +apc:apc +dm:ftp +dmftp:ftp +httpadmin:fhttpadmin +user:system +MELSEC:MELSEC +QNUDECPU:QNUDECPU +ftp_boot:ftp_boot +uploader:ZYPCOM +ftpuser:password +USER:USER +qbf77101:hexakisoctahedron +ntpupdate:ntpupdate +sysdiag:factorycast@schneider +wsupgrade:wsupgrade +pcfactory:pcfactory +loader:fwdownload +test:testingpw +webserver:webpages +fdrusers:sresurdf +nic2212:poiuypoiuy +user:user00 +su:ko2003wa +MayGion:maygion.com +admin:9999 +PlcmSpIp:PlcmSpIp diff --git a/wordlists/gvit_subdomain_wordlist.txt b/wordlists/gvit_subdomain_wordlist.txt new file mode 100644 index 00000000..493a9f82 --- /dev/null +++ b/wordlists/gvit_subdomain_wordlist.txt @@ -0,0 +1,11611 @@ +0 +000 +01 +010 +02 +03 +080 +09 +1 +10 +100 +1000 +101 +104 +11 +111 +1111 +114 +117 +12 +120 +123 +1234 +124 +125 +125125 +129 +129979 +13 +1314 +132 +135 +14 +147 +148 +149 +15 +154 +157 +159 +16 +160 +162 +163 +164 +166 +167 +168 +17 +171 +172 +177 +18 +181 +182 +184 +187 +19 +190 +192 +193 +194 +197 +198 +1c +2 +20 +200 +2005 +2006 +2007 +2008 +2009 +2010 +2011 +2012 +2013 +2014 +202 +208 +209 +21 +211 +212 +213 +216 +22 +220 +222 +23 +232 +237 +24 +244 +25 +26 +27 +28 +29 +2for1gift +3 +30 +31 +32 +321 +33 +34 +35 +36 +360 +365 +369 +37 +38 +386 +39 +3ans +3com +3d +3g +3g66 +3img +3w +4 +40 +400 +4006 +404 +4050 +41 +42 +43 +44 +45 +46 +47 +48 +49 +4g +4k +4x4 +5 +50 +51 +52 +53 +54 +55 +555 +56 +57 +58 +59 +6 +60 +61 +65 +66 +666 +67 +69 +7 +70 +71 +72 +73 +74 +75 +76 +77 +777 +78 +8 +80 +800 +81 +82 +84 +85 +85cc +85st +86 +87 +88 +888 +89 +8u +9 +90 +91 +911 +94 +95 +96 +97 +98 +98-62 +988 +99 +9933 +999 +99comcn +HINET-IP +ILMI +IN +UN +Unused +a +a-213-171-216-114 +a-dtap +a.auth-ns +a01 +a02 +a1 +a10 +a12 +a2 +a3 +a4 +a66 +a8 +aa +aaa +aaa2 +aab +aac +aachen +aacse +aad +aadams +aag +aage +aaguirre +aahl +aahz +aai +aal +aallan +aap +aapo +aardvark +aardwolf +aaron +aas +aasa +ab +aba +abacus +abakan +abalone +abbott +abbott-labs +abbottlaboratories +abbottlabs +abby +abc +abcd +abe +abel +abell +aberdeen +abhsia +abi +abit +abiturient +abo +about +abra +abracadabra +abragam +abraham +abrams +abramson +abraxas +abricot +abs +absolute +abuse +ac +ac2 +aca +acacia +acad +academia +academic +academico +academics +academy +acc +accelerator +acceptatie +acces +acceso +access +access1 +access2 +accessories +accommodation +account +accounting +accounts +accreditation +acct +acd +ace +acer +acervo +acesso +acessonet +achieve +acid +acm +acme +acp +acs +acsvax +act +acta +action +activate +activation +active +activestat +activesync +activities +activity +actu +ad +ad1 +ad2 +ad3 +ad4 +ada +adam +adams +aday +adc +add +addon +addons +adfs +adi +adidas +adimg +adkit +adm +adm2 +adm3 +admanager +admin +admin1 +admin2 +admin3 +admin4 +admindev +administracion +administrador +administration +administrator +administrators +adminmail +admins +admintest +admision +admisiones +admission +admissions +admitere +adnet +adobe +adp +adrian +ads +ads1 +ads2 +ads3 +adsense +adserv +adserver +adsl +adslgp +adt +adtest +adult +adv +advance +advantage +advent +adventure +advert +advertise +advertiser +advertising +advice +advisor +adwords +adx +ae +aec +aero +aes +aetna +af +aff +affiliate +affiliatepage +affiliates +affiliati +affiliation +afiliados +afisha +afp +africa +afrodita +afs +ag +ag-hinrichs +ag-kopf-moertz +aga +agate +age +agencia +agency +agenda +agent +agents +agile +aging +agk +agnes +agora +agri +agriculture +agro +ags +ah +ai +aic +aida +aide +aiesec +aig +aikido +aim +aim4 +aims +aion +aip +air +aire +airport +airsoft +airwatch +airwave +ais +aist +ait +aix +aj +ajax +ajuda +ak +ak-gw +akademia +akademik +akamai +akira +al +alabama +aladdin +alan +alan5 +alaska +alba +albert +albq +album +albums +albuquerque +alc +alcor +aldebaran +aleph +alert +alerts +alertus +alestra +alesund-gw1 +alex +alexander +alexandre +alexandria +alf +alfa +alfred +alfresco +ali +alice +alien +alive +all +allegro +allen +alliance +allianz +allstate +allwww +alma +aloha +alpda +alpha +alpha1 +alpha2 +alpine +als +alt +altair +alterwind +alumni +alumnos +aluno +am +ama +amadeus +amanda +amarillo +amateur +amazon +amber +amc +amd +amedd +america +american-express +americanexpress +americaninternationalgroup +americas +amerisourcebergen +ami +amigo +amigos +amp +ams +amsterdam +amur +amway +amy +an +ana +ana-dev +anaheim +anakin +anal +analog +analysis +analytics +analyzer +ancien +and +andrew +android +andromeda +andromede +andy +angel +angola +anhui +ani +animal +animals +animation +anime +ankara +anket +anketa +ankieta +ankiety +ann +anna +annonces +announce +announcements +annuaire +annualreport +annunci +ans +answer +answers +ant +antalya +antares +anthony +anthropology +antigo +antispam +antispam2 +antivir +antivirus +anton +antonio +anubis +anuncios +anunturi +anywhere +anzeigen +ao +aoc +aoe1 +aol +ap +ap01 +ap02 +ap1 +ap2 +ap3 +apa +apache +apartment +apc +apc1 +apc2 +apc3 +apc4 +apd +ape +apex +apg +aphrodite +api +api-dev +api-test +api1 +api2 +api3 +apidev +apis +apitest +apk +apl +aplicaciones +aplicativos +aplus +apm +apns +apogee +apol +apollo +apollo2 +apolo +app +app01 +app02 +app1 +app2 +app3 +app4 +app5 +app6 +appdev +appengine +apple +appli +application +applications +applwi +apply +appnews +appraisal +apps +apps1 +apps2 +apps3 +appserver +appstore +apptest +april +aps +apt +apteka +apus +aq +aqua +aquarius +aquila +ar +ara +arabic +aragon +aragorn +arc +arcade +arcgis +arch +archer +archerdaniels +archerdanielsmidland +archi +archie +architecture +archiv +archive +archive1 +archive2 +archives +archivio +archivo +archivos +archiwum +arcsight +arctic +arcturus +area +area51 +arena +ares +argentina +argo +argon +argos +argus +arhiv +arhiva +ari +aria +ariane +ariel +aries +aris +arizona +ark +arkansas +arlington +arm +army +arnold +arp +arpa +arquitectura +arquivos +arrow +ars +arsenal +arsip +art +arte +artem +artemis +arthur +article +articles +artist +arts +aruba +aruba-master +arwen +as +as1 +as2 +as2test +as3 +as400 +asa +asap +asb +asc +asd +asdf +ase +asf +asg +asgard +ash +asi +asia +asian +asianet +asistencia +ask +asl +asm +asp +asp1 +asp2 +aspen +aspera +asr +assessment +asset +assets +assets0 +assets1 +assets2 +assets3 +assets4 +assets5 +assist +assistance +assistenza +asso +association +ast +asta +aster +asteriks +asterisk +asterisk2 +asteriskos +asterix +astra +astrahan +astrakhan +astro +astronomy +asu +asus +async +at +at820 +ata +atc +atelier +atendimento +atenea +athena +athens +athletics +ati +atl +atlant +atlanta +atlantic +atlantis +atlas +atm +atmail +atom +aton +atp +atrium +ats +att +attach +attachments +attendance +au +auction +auctions +aud +audi +audio +audit +august +aukcje +aula +aulas +aulavirtual +aura +auriga +aurora +aus +austin +australia +austria +austtx +aut +auth +auth1 +auth2 +author +authors +auto +auto-mx +autoconfig +autodiscover +autodiscovery +automail +automation +automotive +autopromo +autoreply +autorun +autos +aux +av +av1 +av2 +ava +available +avalon +avantel +avasin +avatar +avatars +avaya +avdesk +avg +avia +aviation +avis +avl +avon +avp +avs +avto +aw +award +awards +awc +awp +aws +awstats +awverify +ax +axa +axel +axis +ay +ayniyat +ayuda +az +azmoon +azs +azure +b +b.auth-ns +b01 +b02 +b1 +b10 +b11 +b2 +b2b +b2btest +b2c +b3 +b4 +b5 +b6 +b7 +b8 +b9 +ba +babel +baby +babylon +bac +bacchus +bach +back +backdoor +backend +backlinks +backoffice +backstage +backup +backup-mailserver +backup01 +backup02 +backup1 +backup2 +backup3 +backup4 +backup5 +backupmx +backuppc +backups +bacula +badger +baidu2014 +baike +bailefang +bak +bak204 +bak219 +bak7 +bak78 +baker +bakersfield +baku +balance +balancer +bali +baltimore +bam +bamboo +ban +banana +banco +bancuri +band +bandwidth +bang +bangalore +bangkok +bangladesh +bank +banking +bankofamerica +bankofamericacorp +banner +banners +bannerweb +bao +baoming +bap +bappeda +bar +barbados +barbara +barcelona +barcode +barnaul +barney +barracuda +barracuda2 +bars +bart +bas +base +base2 +baseball +bash +basic +basin +basis +basket +bass +bastion +bat +batch +batman +battle +battlestar-galactica +bau +bayarea +baza +bazaar +bazar +baze +bb +bb1 +bb2 +bbb +bbc +bbdd +bbm +bbs +bbs1 +bbs2 +bbtest +bc +bc1 +bc2 +bca +bcc +bchsia +bck +bcm +bcn +bcp +bcs +bcvloh +bd +bd123002 +bdc +bdd +bdf +bds +bdsm +be +be2 +bea +beacon +beagle +bear +beasiswa +beast +beauty +beaver +becas +bee +beeline +beer +beginners +beheer +bei +beian +beijing +bel +belarus +belgium +belgorod +belize +bell +bellatrix +bem +ben +bender +benefits +benz +bergen-gw2 +bergen-gw7 +berkshire +berkshirehathaway +berlin +bes +best +best-buy +bestbuy +bestdeal +besyo +bet +beta +beta1 +beta2 +beta3 +beta4 +betatest +betty +bewerbung +bf +bf2 +bfn1 +bfn2 +bg +bgk +bgp +bgs +bh +bhm +bhs +bi +bia +bialystok +bib +bible +biblio +biblioteca +bibliotecadigital +bibliotecas +biblioteka +bibliotheque +bic +bid +bidb +big +big5 +bigbrother +bigpond +bigsave +bigsavings +bigtits +bike +bilbo +bilder +bilet +bilety +bill +billing +billing2 +bim +bin +binaries +binary +bindmaster +bing +bingo +bio +biochem +bioinfo +bioinformatics +biologia +biology +biomed +biotech +bip +bird +birmingham +birthday +bis +bisexual +bison +bit +bitrix +biuro +biurokarier +biyoloji +biz +biznes +biztalk +bj +bj01 +bk +bkd +bkp +bl +black +blackberry +blackbird +blackboard +blackbox +blackhole +blacklist +blade +blade1 +blade2 +blade3 +blago +blast +blink +bliss +blitz +block +blocked +blog +blog-dev +blog1 +blog2 +blog3 +blogdev +blogg +blogger +blogi +blogs +blogs2 +blogsearch +blogtest +blogue +blue +bluebird +blues +bluesky +blueyonder +bm +bmail +bmc +bme +bmp +bms +bmt +bmw +bmx +bn +bna +bnc +bo +boa +board +boards +bob +bobae +bobcat +bobo +boc +bod +boeing +bof +bogdan +bogota +bohr +bois +boise +bok +bol +boletin +boletines +boleto +bolg +bolivia +bologna +bolsa +bond +bonus +book +booking +bookings +bookit +bookmark +bookmarks +books +bookshop +bookstore +boom +bootp +bordeaux +border +boris +boron +bos +bosch +boson +boss +boston +bot +botany +boulder +bounce +bouncer +bounces +boutique +box +box2 +boy +bp +bpb +bpc +bpi +bpm +bps +bq +br +br1 +br2 +brad +brahms +brain +branch +brand +branding +brands +brasil +brasiltelecom +bravo +brazil +brc +bredband +breeze +brest +bri +brian +bridge +brisbane +bristol +britian +brlconsulting +broadband +broadcast +broadcast-ip +broker +bronx +bronze +brown +browse +browser +bruce +bruno +brutus +bryansk +bs +bsc +bscw +bsd +bsd0 +bsd01 +bsd02 +bsd1 +bsd2 +bsh +bshs +bsmtp +bss +bt +btas +btp +bts +bu +bubbles +budapest +budget +buffalo +bug +buggalo +bugs +bugtrack +bugtracker +bugz +bugzilla +buh +build +buildbot +builder +building +bulgaria +bulk +bulkmail +bulksms +bull +bulletin +bulletins +bulten +bunny +burn +burner +bursa +bus +busca +buscador +business +butler +butterfly +bux +buy +buydigitaltv +buyersguide +buzon +buzz +bv +bw +bwc +bx +by +byby +bydgoszcz +byseg854 +bz +c +c-00 +c.auth-ns +c1 +c10 +c11 +c12 +c13 +c2 +c21 +c2i +c3 +c3po +c4 +c5 +c6 +c7 +c8 +c9 +ca +ca1 +ca2 +cab +cabal +cabinet +cable +cac +cache +cache01 +cache1 +cache2 +cache3 +cacti +cacti2 +cactus +cad +cadastro +cae +cafe +cag +cai +caiwu +cake +cal +calc +calcium +calculator +caldav +calendar +calendario +calendars +calender +calendrier +calgary +calidad +california +call +callback +callcenter +callisto +callpilot +calls +calvin +calypso +cam +cam1 +cam2 +cam3 +cam4 +cambridge +camel +camera +camera1 +camera2 +camera3 +cameras +cameron +camp +campaign +campaigns +camping +campus +campus2 +campusvirtual +cams +can +canada +canal +cancer +candy +canon +canopus +canvas +cap +capacitacion +capella +capital +captcha +car +carbon +card +cardinal-health +cardinalhealth +cards +care +career +careers +cargo +carl +carlos +carmen +carnival +caronte +carrefour +carrier +cars +cart +carte +cartman +carto +cartoon +cas +cas1 +cas2 +casa +cascade +case +cash +cashier +casino +casper +cassini +cast +casting +castle +castor +cat +catalog +catalogo +catalogs +catalogue +catalyst +catering +caterpillar +cats +cau +cavalrydesign +cb +cba +cbc +cbf1 +cbf2 +cbf3 +cbf4 +cbf5 +cbf7 +cbf8 +cbh +cbi +cbs +cbt +cc +cc2 +cca +ccb +ccc +cce +ccgg +cci +ccm +ccnet +cco +ccp +ccr +ccs +cct +cctv +cd +cdb +cdburner +cdc +cde +cdl +cdm +cdn +cdn0 +cdn01 +cdn02 +cdn1 +cdn1122 +cdn2 +cdn3 +cdn4 +cdn5 +cdn6 +cdn7 +cdn8 +cdn9 +cdo +cdp +cdp1 +cdr +cdrom +cds +cdt +ce +cea +cead +cec +ced +cedar +cee +cef +cei +cel +celebrity +cell +cem +ceng +census +center +centos +central +centre +centreon +ceo +cep +cer +cerbere +cerberus +ceres +cert +certificados +certificate +certificates +certification +certify +certserv +certsrv +ces +ceshi +cet +cf +cf2 +cfd +cfnm +cg +cgc +cgi +cgp +cgs +ch +cha +challenge +challenger +chameleon +chanel +chang +change +channel +channels +chaos +chaosm-th +chapters +charge +charity +charlie +charlotte +charon +chart +charts +chase +chat +chat-service +chat-service2 +chat1 +chat2 +chat3 +chat4 +chats +chatserver +chaxun +chcgil +che +cheboksary +check +checkout +checkpoint +checkrelay +checksrv +cheetah +chef +chel +chelny +chelsea +chelyabinsk +chem +chemeng +chemistry +chemlab +chennai +cher +cherry +chess +chevrolet +chevron +chewbacca +chi +chiba +chicago +chicken +chico +child +children +chile +chimera +china +chinese +chip +chita +chopin +choup +chris +christmas +chrome +chronos +chrysler +chs +church +ci +cia +cib +cic +cicril +cid +cidr +cie +cim +cims +cinci +cincinnati +cine +cinema +cio +cip +cirrus +cis +cisco +cisco-capwap-controller +cisco-lwapp-controller +cisco-systems +cisco1 +cisco2 +ciscosystems +ciscoworks +cit +citi +citibank +citigroup +citrix +citrix1 +citrix2 +citrix3 +citroen +city +civil +cj +cjxy +cjy +ck +ckp +cl +cl1 +cla +claims +clamav +clara +clarity +clark +clasificados +class +classes +classic +classics +classificados +classified +classifieds +classroom +clc +cle +clean +cleveland +click +click3 +clicks +clicktrack +client +client1 +client2 +clientes +clienti +clients +clients1 +climate +clinic +clio +clip +clips +clk +clock +clone +clothes +cloud +cloud1 +cloud2 +cloud3 +cloud4 +cloudflare-resolve-to +cloudfront +cls +clsp +clt +clta +club +clubs +cluster +cluster1 +cluster2 +clustermail +clusters +cm +cma +cmail +cmc +cmcc +cmd +cmdb +cme +cmi +cmp +cmr +cms +cms-test +cms1 +cms2 +cms3 +cmsadmin +cmsdev +cmstest +cmt +cn +cn1 +cn2 +cna +cname +cnap +cnarne +cnc +cnet +cnki +cns +cns1 +cns2 +cnt +cntv +co +coach +cob +cobalt +cobbler +cobra +coca-cola +cocacola +cockpit +coco +cocoa +cod +cod4 +code +codereview +codetel +codex +coe +coffee +cognos +coke +col +colaboracion +coldfusion +colibri +collab +collaborate +collaboration +collection +collections +collector +college +colo +colombia +colombo +colombus +color +colorado +colossus +columbia +columbo +columbus +com +combo +comcast +comercial +comercio +comet +comet1 +comet2 +comet3 +comet4 +comic +comics +comm +comment +comments +commerce +commerceserver +commercial +common +commons +comms +communication +communications +communicator +communigate +communities +community +comp +company +compaq +compare +compass +competitions +compras +compta +compute-1 +computer +computers +comune +comunicacion +comunicare +comunicati +comunicazione +comunidad +comunidades +con +concentrator +concord +concorde +concours +concurso +concursos +condor +conf +conference +conferences +conferencia +conferencing +confidential +config +confirm +confirmation +confluence +conges +connect +connect2 +connecticut +connection +connections +conoco +conocophillips +consola +console +construction +construtor +consult +consulta +consultant +consultants +consultas +consultation +consulting +consumer +cont +contact +contact-us +contacto +contacts +contactus +contato +contatos +contenidos +content +content2 +content6 +content7 +contents +contest +contests +context +contractor +contracts +contribute +control +control-panel +control1 +controle +controller +controlp +controlpanel +contropanel +convention +convert +converter +cook +cookie +cooking +cool +coop +cooper +cop +copenhagen +copper +copy +copyright +coral +core +core0 +core01 +core1 +core2 +core3 +core4 +coregw1 +cork +corona +corp +corp-eur +corpmail +corporate +corporativo +correio +correo +correo1 +correo2 +correos +correoweb +correu +cortafuegos +corvus +cos +cosmo +cosmos +costarica +costco +costco-wholesale +costcowholesale +cougar +council +counseling +count +counter +counterstrike +countries +country +coupang4 +coupon +coupons +courriel +courrier +cours +course +courses +court +cover +covers +coyote +cp +cp1 +cp2 +cp3 +cp4 +cpa +cpan +cpanel +cpanel1 +cpanel2 +cpanel3 +cpc +cpd +cpe +cph +cpi +cpk +cpm +cpns +cpp +cpr +cps +cpt +cptest +cpx +cq +cr +crash +crashplan +crawl +crawler +crazy +crc +crea +create +creative +credit +credito +crew +cri +cricket +crime +crimson +crl +crm +crm1 +crm2 +crm3 +crmdev +crmtest +cron +cronos +cross +crossdressers +crowd +crs +crt +crucible +cruise +crux +crypto +crystal +cs +cs01 +cs1 +cs16 +cs2 +cs3 +csa +csc +csd +csdns01 +csdns02 +cse +csf +csf1 +csf1-1 +csf1-2 +csf1-3 +csf1-4 +csg +csi +csit +csl +csm +cso +csp +csr +css +css1 +css2 +csscdr +csscha +cst +csv +ct +ctc +ctd +cte +cti +ctl +ctp +ctrl +cts +ctt +ctx +cu +cuba +cube +cuda +cultura +culture +cumulus +cup +cups +curie +curriculum +cursos +cust +cust-adsl +cust1 +cust10 +cust100 +cust101 +cust102 +cust103 +cust104 +cust105 +cust106 +cust107 +cust108 +cust109 +cust11 +cust110 +cust111 +cust112 +cust113 +cust114 +cust115 +cust116 +cust117 +cust118 +cust119 +cust12 +cust120 +cust121 +cust122 +cust123 +cust124 +cust125 +cust126 +cust13 +cust14 +cust15 +cust16 +cust17 +cust18 +cust19 +cust2 +cust20 +cust21 +cust22 +cust23 +cust24 +cust25 +cust26 +cust27 +cust28 +cust29 +cust3 +cust30 +cust31 +cust32 +cust33 +cust34 +cust35 +cust36 +cust37 +cust38 +cust39 +cust4 +cust40 +cust41 +cust42 +cust43 +cust44 +cust45 +cust46 +cust47 +cust48 +cust49 +cust5 +cust50 +cust51 +cust52 +cust53 +cust54 +cust55 +cust56 +cust57 +cust58 +cust59 +cust6 +cust60 +cust61 +cust62 +cust63 +cust64 +cust65 +cust66 +cust67 +cust68 +cust69 +cust7 +cust70 +cust71 +cust72 +cust73 +cust74 +cust75 +cust76 +cust77 +cust78 +cust79 +cust8 +cust80 +cust81 +cust82 +cust83 +cust84 +cust85 +cust86 +cust87 +cust88 +cust89 +cust9 +cust90 +cust91 +cust92 +cust93 +cust94 +cust95 +cust96 +cust97 +cust98 +cust99 +custom +customer +customercare +customers +customerservice +cv +cvs +cvs-caremark +cvscaremark +cvsup +cvsweb +cw +cwa +cwc +cwcx +cws +cx +cxzy +cy +cyan +cyber +cyberpanel +cybozu +cyc +cyclone +cyclops +cygnus +cyprus +cz +czat +czech +d +d-app +d-click +d-image +d-view +d0 +d1 +d10 +d11 +d12 +d2 +d3 +d4 +d5 +d6 +d7 +d8 +d9 +da +da1 +da17 +da2 +da25 +da3 +da4 +da5 +dac +daemon +dag +dai +daily +daisy +daj +dakar +dal +dali +dallas +dam +dan +dance +dangan +daniel +dante +dao +daohang +dap +daphne +dar +dark +darkorbit +darkstar +dart +darwin +das +dash +dashboard +dashboard2 +data +data1 +data2 +data3 +database +database01 +database02 +database1 +database2 +databases +datacenter +datastore +datasync +date +dating +datos +daum +dav +dave +david +davinci +day +dayton +daytona +db +db0 +db01 +db02 +db03 +db04 +db1 +db2 +db3 +db4 +db5 +db6 +db7 +db8 +db9 +dba +dbadmin +dbase +dbm +dbs +dbserver +dbtest +dc +dc01 +dc1 +dc2 +dc3 +dcc +dce +dchub +dcp +dcpan +dcs +dd +ddc +ddd +ddh +ddm +ddn +ddns +dds +ddt +de +de1 +de2 +deal +dealer +dealers +deals +dean +deb +debian +debug +dec +deco +ded +dedicated +deep +deepolis +def +default +defender +defiant +degreeworks +deimos +del +delaware +delfin +delhi +deliver +delivery +dell +delo +delphi +delta +delta-air +delta1 +deltaair +deltaairlines +deluxe +dem +demeter +demo +demo01 +demo02 +demo03 +demo1 +demo10 +demo11 +demo12 +demo13 +demo14 +demo15 +demo17 +demo2 +demo3 +demo4 +demo5 +demo6 +demo7 +demo8 +demo9 +democms +demon +demonstration +demos +demosasteriskasteriskdenver +demoshop +demosite +demostration +den +deneb +deneme +denis +denmark +dennis +denver +dep +deploy +depo +deportes +depot +dept +derecho +derek +des +desa +desarrollo +descargas +desenvolvimento +design +designer +designs +desk +desktop +destek +destiny +deti +detroit +deutsch +dev +dev-chat +dev-chat-service +dev-www +dev0 +dev01 +dev02 +dev03 +dev1 +dev10 +dev100 +dev11 +dev12 +dev13 +dev14 +dev15 +dev16 +dev17 +dev18 +dev19 +dev2 +dev3 +dev4 +dev40 +dev5 +dev6 +dev7 +dev8 +dev9 +deva +devadmin +devapi +devblog +devdb +devel +devel2 +develop +developer +developers +development +devforum +device +devil +devilhost +devm +devphp +devphp64 +devportal +devs +devserver +devshop +devsite +devsql +devtest +devweb +devwiki +devwowza +devwww +dewey +dex +dexter +df +dfs +dg +dh +dhcp +dhcp-bl +dhcp-in +dhcp1 +dhcp2 +dhcp3 +dhcp4 +dhl +di +dia +diablo +dial +dialin +dialog +dialuol +dialup +diamond +diana +diane +diary +dias +diaspora +dic +dict +dictionary +diendan +diet +dieta +diffusion +dig +digi +digilander +digilib +digital +digitalmedia +digitaltv +dilbert +dima +dimdim +ding +dingo +dining +dino +dion +dione +dionysos +dip +dip0 +diplom +dir +dirac +direct +direct2 +director +directories +directorio +directory +dis +disc +disco +discount +discountfinder +discover +discovery +discovirtual +discuss +discussion +discussions +disk +disney +dispatch +display +dist +distance +distributer +distributers +distribution +distributor +distributors +ditu +diversity +diy +diz +dj +django +djh +djj +dk +dl +dl1 +dl2 +dl3 +dl4 +dl5 +dlc +dlib +dls +dm +dmail +dmc +dme +dmm +dms +dmt +dmz +dn +dn2 +dna +dnews +dnn +dns +dns-2 +dns0 +dns01 +dns02 +dns03 +dns04 +dns1 +dns10 +dns11 +dns12 +dns13 +dns14 +dns2 +dns2138 +dns3 +dns4 +dns4512 +dns4527 +dns5 +dns6 +dns7 +dns8 +dns9 +dnsadmin +dnsmaster +dnsseed +dnstest +do +doc +docs +doctor +document +documentacion +documentation +documenti +documentos +documents +docushare +dod +dodo +dog +dogs +dok +doktoranci +dokumenty +dokuwiki +dolphin +dolphin10 +dom +domain +domain-controller +domain-cp +domainadmin +domaincontrol +domaincontroller +domaincontrolpanel +domaincp +domaincpanel +domaindnszones +domainmanagement +domainmanager +domainpanel +domains +domcontrol +domen +domeny +dominio +dominios +domino +domino2 +dominoweb +domolink +domreg +don +donald +donate +donkey +doom +door +doors +dop +dora +dorado +dorm +dos +doska +dot +dota +dotfigure +dotnet +dotproject +dougal +douglas +dove +dow +dow-chemical +dowchemical +down +download +download1 +download2 +download3 +download4 +downloads +downtown +dp +dpa +dpanel +dpi +dpm +dppd +dpr +dps +dpstar +dpt +dq +dr +drac +draco +draft +dragon +drakensang +drama +drc +dream +dresden +drive +driver +drivers +drm +drmail +droid +drop +dropbox +drp +druk +drupal +drupal7 +drweb +ds +ds01 +ds1 +ds10 +ds2 +ds3 +dsa +dsc +dse +dsi +dsl +dsl-w +dsp +dspace +dspam +dss +dst +dt +dtc +dti +dtm +dts +du +dubai +dublin +duck +duke +duma +dummy +dump +dupont +dv +dvd +dvr +dw +dwb +dwgk +dwh +dws +dx +dy +dyn +dynamic +dynamicIP +dynamics +dynip +dz +dzb +e +e-com +e-commerce +e-learning +e-mail +e-resultats +e-shop +e0 +e1 +e10 +e11 +e2 +e3 +e4 +e5 +e7 +ea +eac +eaccess +ead +eagle +earth +eas +east +easy +eat +eb +ebank +ebanking +ebay +ebe +ebh +ebill +ebiz +eblast +ebony +ebook +ebooks +ebs +ebusiness +ec +ec2 +eca +ecampus +ecard +ecards +ecc +ecdl +ece +echanges +echarge +echo +echo360 +eclass +eclipse +ecm +ecms +eco +ecology +ecom +ecomm +ecommerce +econ +econom +economia +economics +economie +economy +ecp +ecs +ects +ecuador +ed +eda +edc +edd +eden +edergi +edge +edge1 +edge2 +edi +edinburgh +edison +edit +edition +editor +editorial +edm +edm2 +edmonton +edms +edoas +edoc +edocs +edp +eds +edt +edu +edu1 +edu2 +edu3 +educ +educacion +education +edukacja +eduroam +edward +ee +eec +eedition +eee +eem +ef +eform +eforms +eg +ege +egitim +egloo +ego +egov +egresados +egroupware +egw +egypt +eh +ehr +ehs +ei +eic +einstein +eip +eis +ejemplo +ejemplos +ejournal +ek +ekaterinburg +ekb +eko +ekonomi +ektron +el +elab +elan +elara +elastic +elasticsearch +elastix +elc +elearn +elearning +elearning2 +elec +elecciones +election +elections +electra +electro +electron +electronica +electronics +elektra +elektro +elena +elephant +elf +elgg +elib +elibrary +elite +elk +elm +elmer +elms +elpaso +elrond +els +elsa +elvis +em +email +email1 +email2 +email3 +emailadmin +emailer +emailing +emailmarketing +emails +emarketing +emba +embed +embratel +emc +eme +emeeting +emerald +emergency +emhril +emis +emkt +emm +emma +emo +emp +empire +empleo +empleos +emploi +employee +employees +employment +emprego +empresa +empresas +ems +emu +en +en2 +enable +enc +encoder +encore +encrypted +encuesta +encuestas +endeavor +endor +endpoint +energia +energie +energy +enet +enews +enformatik +eng +eng01 +eng1 +engage +engelsiz +engine +engineer +engineering +english +eniac +enigma +enlace +enlaces +enq +enquete +enquetes +enroll +enrutador +ens +ent +ent1 +ent2 +enter +enterprise +enterpriseenrollment +enterprisegp +enterprisegpholdings +enterpriseregistration +entertainment +entrepreneurship +entry +env +envios +environment +eo +eoffice +eol +eole +eonet +eos +ep +epa +epaper +epay +epayment +epc +epg +epi +epic +epm +epo +eportal +eportfolio +epos +epost +eposta +epp +eprint +eprints +eproc +eps +epsilon +epub +eq +er +era +erasmus +erato +erc +eric +eris +ernie +eroom +eros +erotic +erp +erp2 +err +error +errorlog +errors +es +es1 +esa +esales +esb +esc +esd +eservice +eservices +eset +esf +eshop +eski +esl +esm +esmtp +esn +eso +esp +espace +espana +espanol +especiales +espresso +ess +essai +essen +est +estadisticas +estate +estonia +estore +estudiantes +esupport +esx +esx01 +esx02 +esx1 +esx2 +esx3 +esx4 +esx5 +esxi +et +eta +etb +etc +etest +etherpad +ethics +etna +ets +etu +eu +eu1 +eugene +euler +eup +eur +eureka +euro +euro2012 +europa +europe +euterpe +ev +eva +eval +evaluacion +evaluation +evasys +eve +event +event2 +eventi +eventos +events +eventum +everest +evm +evo +evolution +ew +eweb +ews +ex +ex1 +exam +example +examples +exams +exc +excalibur +exceptionto +exch +exchange +exchange1 +exchange2 +exclusive +exec +exit +exmail +exo +exodus +exp +experience +expert +experts +explore +explorer +expo +export +exposure +express +expresso +expressscripts +ext +ext2 +extend +extendcp +extension +extensions +extern +external +extmail +extra +extranet +extranet2 +extras +extreme +extweb +exxon +exxonmobile +eye +eyny +ez +ezine +ezproxy +f +f1 +f2 +f3 +f4 +f5 +f6 +fa +fac +face +facebook +facilities +factory +facturacion +faculty +fad +fai +failover +fair +falcon +fallback +family +fan +fanclub +fang +fanli +fannie-mae +fanniemae +fanshop +fantasy +fanyi +fao +fap +faq +farabi +faraday +farm +farmerama +fas +fashion +fast +faststats +fat +fate +faust +fax +fax2 +faxserver +fb +fb-canvas +fbapp +fbapps +fbdev +fbe +fbl +fbs +fbx +fc +fc2 +fca +fcc +fcs +fd +fdc +fdm +fds +fe +fe1 +features +fed +federation +fedex +fedora +feed +feedback +feedburner +feedproxy +feeds +fef +felix +femdom +feng +fenix +fensike +fermi +ferrari +fes +festival +fetish +ff +fff +fg +fgc +fgw +fh +fhg +fhg2 +fhg3 +fi +fiat +fiber +fibertel +fichiers +fido +field +fiesta +fil +file +file1 +file2 +filemaker +filemanager +filer +files +files1 +files2 +files3 +filesender +fileserv +fileserver +fileshare +filestore +filetransfer +filex +filez +filip +film +films +filr +filter +fin +finaid +finance +financeiro +finances +financial +financialaid +finanse +finanzas +find +finder +findnsave +finearts +finger +finland +fiona +fios +fip +fire +firebird +firefly +firewall +firewall2 +firma +firmware +firmy +first +fis +fish +fisher +fisheye +fishing +fisica +fisip +fit +fitness +fix +fixes +fizik +fj +fk +fl +flame +flash +flashchat +flc +fld +fleet +flex +flight +flights +flint +flirt +flora +florence +florida +flow +flower +flowers +flux +flv +flv1 +flv2 +fly +fm +fmail +fmc +fmf +fmp +fms +fms1 +fn +fns +fo +focus +fod +fog +fogbugz +folder +folders +folio +fond +font +fonts +foo +foobar +food +football +for +force +ford +fordmotor +foreign +forest +forestdnszones +forestry +forex +forge +form +formacion +formation +formations +formazione +formosa +forms +formula +formularios +foro +foro2 +foros +forschung +fort +fortress +fortuna +fortune +fortworth +forum +forum1 +forum2 +forum3 +forums +forumtest +forward +fotki +foto +fotografia +fotos +foundation +foundry +fourier +fox +foxtrot +fp +fr +fr1 +fr2 +framework +francais +france +franchise +frank +frankfurt +franklin +fred +freddie-mac +freddiemac +free +freebsd +freebsd0 +freebsd01 +freebsd02 +freebsd1 +freebsd2 +freedom +freegift +freemail +freeware +french +fresh +fresno +friend +friends +fritz +frodo +frog +frokca +front +front1 +front2 +front3 +frontdesk +frontend +frontier +frontpage +froogle +frost +fruit +fs +fs1 +fs2 +fs3 +fs4 +fs5 +fsc +fsimg +fsm +fsp +fss +fst +ft +ftas +ftd +ftp +ftp- +ftp-eu +ftp0 +ftp01 +ftp02 +ftp1 +ftp10 +ftp11 +ftp12 +ftp13 +ftp14 +ftp15 +ftp16 +ftp2 +ftp3 +ftp4 +ftp5 +ftp6 +ftp7 +ftp8 +ftp9 +ftp_ +ftpadmin +ftpd +ftpmini +ftps +ftpsearch +ftpserver +ftptest +ftpweb +fu +fuck +fuji +fujian +fuke +fukuoka +fukushima +fun +fund +fundacion +funny +furniture +fusion +futbol +future +fw +fw-1 +fw01 +fw02 +fw1 +fw2 +fw3 +fwd +fwsm +fwsm0 +fwsm01 +fwsm1 +fx +fy +fz +fzgh +fzghc +g +g1 +g1i8 +g2 +g3 +g4 +g5 +g7 +ga +gabinetevirtual +gabriel +gabvirtual +gadget +gadgets +gaia +gaj +gal +gala +galaxy +galeri +galeria +galerias +galerie +galileo +galleries +gallery +gallery2 +gals +galway +gama +game +game1 +game2 +gamer +games +gamezone +gaming +gamma +gandalf +gansu +ganymede +gaokao +gap +gapps +garage +garant +garden +garfield +garnet +gas +gastro +gate +gate1 +gate2 +gate3 +gatekeeper +gateway +gateway1 +gateway2 +gauss +gay +gaz +gazeta +gb +gc +gcal +gcalendar +gcc +gcdn +gd +gdansk +gdi +gdocs +gds +gdsvr +ge +gea +gear +gears +geb +ged +gem +gemini +gems +gen +gender +gene +general +general-dynamics +general-motors +generaldynamics +generalelectric +generalmotors +generator +genericrev +genesis +genetics +genius +genome +gentoo +geo +geobanner +geographic +geography +geoip +geology +geoportal +george +georgia +geplanes +ger +gerenciador +german +germany +gerrit +gest +gestion +gesundheit +get +gettingstarted +gewinnspiel +gf +gforge +gfs +gfx +gg +gh +ghost +ghs +gi +gif +gift +gifts +giga +gilford +gimli +gin +gina +gip +girl +girls +gis +gis2 +git +github +gitlab +gitweb +give +giveaway +giving +gizmo +gj +gjc +gjqx +gjs +gjxy +gk +gl +gladiator +glass +glendale +global +globe +globus +gloria +glossary +glpi +gls +glxy +gm +gmail +gms +gn +go +goat +goblin +god +godaddy +godzilla +gogo +gold +golden +goldmansachs +goldmansachsgroup +goldmine +golestan +golf +goliath +gollum +gonghui +gonzo +good +goods +goofy +google +googleapps +googlee273ce3011619818 +googleffffffffa5b3bed2 +googleffffffffffa87e73 +goose +gopher +gordon +gorod +goto +goty +gourmet +gov +gp +gprs +gps +gpweb +gq +gr +gr-mbpc1 +gra +graal +grace +grad +graduate +grafik +graham +granite +grants +graph +graphic +graphics +graphics2 +graphite +graphs +grc +great +greatdeal +greece +green +greendog +greenfox +greetings +grey +grid +group +groupon +groups +groupsex +groupware +groupwise +grs +grupos +gry +gs +gs1 +gs2 +gsa +gsb +gsc +gsd +gsf +gsites +gsk +gsl +gsm +gsp +gss +gsx +gt +gta +gtc +gtcust +gti +gtm1 +gtm2 +gts +gtw +gu +guadeloupe +guangdong +guangxi +guangzhou +guanli +guard +guardian +guatemala +gudhost +guest +guia +guide +guides +guinv +guitar +gundam +guru +gus +gutenberg +gv +gvt +gw +gw01 +gw02 +gw1 +gw2 +gw3 +gw4 +gw5 +gwia +gwmail +gwmobile +gx +gx1 +gx2 +gx3 +gx4 +gy +gye +gyno +gz +gzc +gzw +h +h1 +h10 +h13 +h14 +h2 +h24 +h2o +h3 +h4 +h5 +h6 +h7 +h8 +ha +ha5 +haber +hades +hah +hai +hainan +hair +hairy +haiti +hal +halflife +hall +halo +ham +hamar-gw2 +hambur +hamburg +hamilton +hammer +hamster +handbook +handel +handjob +handy +hannibal +hans +hao +haosf +happy +hardcore +hardware +hardy +harmony +harris +harry +hartfordfinancial +hartfordfinancialservices +hasp +hastane +hathaway +hawaii +hawk +hb +hbadmin +hc +hc5 +hca +hcc +hcm +hcp +hcs +hd +hdd +he +head +health +healthcare +heart +heat +hebei +hector +hef-router +heimdall +helen +helena +helios +helium +helix +hello +helm +help +help2 +helpcrew +helpdesk +helpdesk2 +helper +helponline +helsinki +hemeroteca +henry +hentai +hep +hera +heracles +hercules +heritage +hermes +hermes2 +hero +heron +hertz +hess +hestia +hewlett-packard +hewlettpackard +hex +hf +hfc +hg +hg3 +hh +hhh +hi +hi-tech +hidden +hidden-host +hideip +hideip-uk +hideip-usa +highway +hilfe +hill +hilton +hindi +hip +hiphop +hippo +hirlevel +hiroshima +his +historia +history +hit +hitech +hive +hj +hk +hkbnpatch +hkcable +hl +hlj +hlrn +hls +hm +hml +hmp +hms +hn +ho +hobbes +hobbit +hobby +hockey +hod +hokbygget-gw +hokkaido +holding +holdingpattern +holiday +holidayoffer +holidays +hollywood +holmes +home +home-depot +home1 +home2 +homebase +homedepot +homepage +homepage1 +homepage2 +homepage3 +homepages +homer +homerun +homes +homewo +homolog +homologa +homologacao +honda +honduras +honey +honeypot +honeywell +honeywellinternational +hongkong +honolulu +honors +hope +horde +horizon +hornet +horo +horoscop +horoscope +horoskop +horse +horus +hospital +hospitality +host +host01 +host02 +host03 +host06 +host1 +host10 +host11 +host12 +host13 +host14 +host15 +host16 +host17 +host18 +host19 +host2 +host20 +host21 +host2123 +host22 +host23 +host24 +host25 +host26 +host3 +host34 +host35 +host37 +host4 +host40 +host5 +host50 +host6 +host7 +host8 +host9 +hosted +hostel +hosting +hosting01 +hosting1 +hosting2 +hosting3 +hostingcontrolpanel +hostingcp +hostmaster +hot +hotel +hoteles +hotels +hoth +hotjobs +hotline +hotspot +houqin +house +housing +houstin +houston +hovedbygget-gw +hovedbygget-gw4 +howard +howto +hoytek-gw +hoytek-gw4 +hp +hp1 +hp2 +hpc +hpc-oslo-gw +hpov +hq +hqc +hqjt +hr +hrd +hris +hrlntx +hrm +hrms +hs +hsia +hss +hstntx +hsv +ht +html +html5 +htrnl +hts +http +https +hu +hua +huan +hub +hubble +hubei +hudson +hugo +hukuk +hukum +human +humana +humanities +humanresources +hummer +humor +hunan +hunter +hurricane +hvac +hw +hx +hy +hybrid +hyderabad +hydra +hydro +hydrogen +hyperion +hypernova +hyundai +hz +hzcnc +hzctc +i +i0 +i1 +i2 +i3 +i4 +i5 +i6 +i7 +i8 +i9 +ia +iae +iah +iam +ias +iax +ib +ibank +ibc +ibk +ibm +ibmdb +ibook +ibs +ic +ica +ical +icare +icarus +icc +icdenetim +ice +icecast +iceman +ichat +icinga +icm +icms +icon +icp +icq +ics +ict +id +ida +idaho +idb +idc +ide +idea +ideal +ideas +idefix +ident +identity +idiomas +idisk +idm +ido +idol +idp +idp2 +ids +ids1 +ids2 +ie +iec +ieee +iem +iep +iern +ies +if +ifeng +ifi2-gw +ifolder +iframe +ifs +ig +igk +igor +igra +ii +iibf +iie +iii +iis +ik +iklan +iks +iktisat +il +ilahiyat +ilc +ilearn +ilias +ill +illiad +illinois +ils +im +im1 +im2 +ima +imac +image +image1 +image2 +image3 +image4 +image5 +imagenes +imagens +images +images0 +images1 +images2 +images3 +images4 +images5 +images6 +images7 +images8 +imageserver +imagine +imaging +imail +imanager +imap +imap1 +imap2 +imap3 +imap3d +imap4 +imapd +imaps +imc +imchat +imcservices +img +img-m +img0 +img01 +img02 +img03 +img04 +img05 +img06 +img07 +img08 +img1 +img10 +img11 +img12 +img13 +img14 +img15 +img2 +img22 +img3 +img4 +img5 +img6 +img7 +img8 +img9 +imga +imgb +imgc +imgcdn +imgf +imgm +imgn +imgs +imgsrv +imgt +imgup-lb +imgweb +imgx +imm +immigration +immo +immobilien +immobilier +imode +imogen +imp +impact +imperia +imperial +import +impsat +impulse +ims +imss +imtest +in +in-addr +inb +inbound +inbox +inc +incest +include +incoming +incubator +ind +index +india +indian +indiana +indianapolis +indicadores +indigo +indonesia +indus +industrial +industry +indy +inet +inews +inf +infinity +info +info1 +info2 +infocenter +infocentre +inform +informatica +informatics +informatika +information +informatique +informer +informix +infos +infosys +infoweb +infra +ing +ingenieria +ingram +ingram-micro +ingrammicro +inicio +inkubator +inmuebles +inno +innov +innova +innovacion +innovation +inotes +input +ins +inscripciones +inscription +inscriptions +inside +insider +insight +insite +insomnia +inspire +inst +instacontrol +instadomains +install +instamail +instavps +insurance +int +int1 +integra +integracao +integration +intel +intelignet +intensivemail +inter +interactive +interface +interior +intern +internacional +internal +internalhost +international +internationalassets +internationalassetsholding +internationalbusinessmachines +internet +interno +internode +interracial +interscan +interview +intl +intra +intra2 +intranet +intranet1 +intranet2 +intranet3 +intratest +intrepid +intro +inv +invalid +inventario +inventory +invest +investigacion +investor +investors +invia +invio +invite +invoice +invoices +io +ios +iota +iowa +ip +ip-ca +ip-hk +ip-uk +ip-us +ip-usa +ip1 +ip118 +ip176-194 +ip2 +ip215 +ip3 +ip4 +ip5 +ip6 +ipa +ipad +ipade +ipam +ipb +ipc +ipcom +ipfixe +iphone +iphone4s +ipkvm +ipl-a +ipl-m +iplanet +iplsin +ipltin +ipm +ipmi +ipmonitor +ipn +ipo +ipod +iportal +ipphone +ipplan +iprimus +iprint +ips +ipsec +ipsec-gw +ipsi +ipt +iptv +ipv4 +ipv6 +ipweb +iq +ir +ira +iran +irbis +irc +irc2 +ircd +ircserver +ireland +iris +irk +irkutsk +irm +irnages +irng +iro +iron +ironmail +ironport +ironport2 +irs +irvine +irving +irvnca +is +isa +isaserv +isaserver +isc +ise +iserver +isg +ishop +isi +isis +islam +island +isletme +ism +ismart +isms +ismtp +iso +isp +ispconfig +israel +iss +issue +issues +issuetracker +ist +istanbul +isync +it +itadmin +italia +italian +italy +itc +itd +ite +item +items +itest +ithelp +itl +itm +itp +its +itsm +itsupport +itunes +itunesu +itv +itwiki +iut +iv +iva +ivan +ivanovo +ivr +iw +iweb +iws +ix +izhevsk +izmir +j +j1 +j2 +j3 +ja +jabber +jack +jackson +jacksonville +jacob +jade +jaguar +jakarta +jam +jamaica +james +jan +jane +janus +japan +japanese +jas +jasmin +jason +jason008 +jasper +java +jax +jay +jazz +jb +jboss +jboss2 +jc +jcb +jcc +jcj +jd +je +jedi +jedi-en +jee +jeff +jefferson +jenkins +jerry +jesse +jet +jeu +jeux +jewelry +jf +jg +jgdw +jgxy +jh +jia +jiangxi +jiaowu +jie +jijian +jilin +jim +jimmy +jin +jingjia +jira +jiuye +jiwei +jj +jjc +jjh +jjw +jjxy +jk +jl +jm +jn +jo +job +jobs +jocuri +joe +john +johnson +johnsonandjohnson +johnsoncontrols +johnsonjohnson +join +joke +joker +jokes +joomla +jordan +joshua +journal +journals +joy +jp +jpk +jpkc +jpmorganchase +jpmorganchaseandco +jpmorganchaseco +jr +jrun +js +js1 +js2 +js3 +jsb +jsc +jsj +json +jss +jsw +jszx +jt +jud +juegos +julia +juliet +juliette +jumbo +jump +jun +junior +juniper +juno +junshi +jupiter +jura +juridico +jurnal +just +justin +jw +jwc +jwgl +jwjc +jwxt +jx +jxc +jxcg +jxjy +jxpt +jxzy +jy +jz +k +k1 +k12 +k2 +k3 +k4 +ka +kabinet +kai +kairos +kalendar +kalendarz +kalender +kaliningrad +kaltura +kaluga +kam +kamera +kamery +kangar +kanri +kansai +kansas +kansascity +kantoor +kaoshi +kappa +karaoke +karate +karen +kariera +kariyer +karriere +karta +kas +kaspersky +kassa +kat +katalog +katalogi +kate +katowice +kav +kayako +kayit +kaz +kazan +kb +kbox +kbtelecom +kc +kd +kdc1 +kdftp +kdm +ke +keeper +kehu +kelly +kemahasiswaan +kemerovo +ken +kenny +kentucky +kenya +kepegawaian +kepler +kerberos +kermit +kernel +kevin +key +keyan +keynote +keys +keyserver +keyword +keywords +kf +kg +kgb +kh +ki +kia +kid +kids +kielce +kiev +kilo +kim +king +kino +kiosk +kip +kirk +kirov +kis +kiss +kit +kitchen +kite +kiwi +kj +kjc +kjj +kk +kl +klient +klm +klm2 +klmzmi +klub +km +kmail +kms +kn +know +knowledge +knowledgebase +knoxville +ko +koa +koala +kobe +kodeks +koe +koha +kolkata +konkurs +kontakt +konto +kor +korea +korean +kostroma +kp +kpi +kps +kr +kraft +kraft-foods +kraftfoods +kraken +krakow +krang +krasnodar +krasnoyarsk +krd +kredit +kroger +kronos +krs +krypton +ks +ksc2mo +ksiegarnia +ksm +ksp +kt +ku +ku6 +kuku +kultura +kunde +kunden +kupon +kurgan +kurs +kursk +kursy +kutuphane +kuwait +kv +kvm +kvm01 +kvm1 +kvm2 +kvm3 +kvm4 +kw +kx +kxfzg +ky +kyc +kygl +kyoto +kz +kzn +l +l1 +l2 +l2tp +l2tp-ca +l2tp-hk +l2tp-uk +l2tp-us +l3 +l4d +la +la2 +lab +lab1 +lab2 +label +labo +labor +laboratories +laboratorio +laboratory +labs +lac +lady +lala +lama +lambda +lamp +lan +lana +lancaster +land +landing +landscape +lang +language +languages +lao +laposte +laptop +lara +larry +laser +laserjet +lastminute +lasvegas +launch +launchpad +laura +law +layout +lb +lb01 +lb02 +lb1 +lb2 +lb3 +lbs +lbtest +lc +lc1 +lc2 +lcs +ld +ldap +ldap0 +ldap01 +ldap02 +ldap1 +ldap2 +ldap3 +ldap4 +ldapadmin +ldapmaster +ldaps +ldaptest +ldj +lds +le +lea +lead +leader +leadership +leads +league +learn +learning +leasing +leave +lebanon +lecture +led +leda +lee +leeds +leela +legacy +legal +legend +legion +legolas +leia +lemlit +lemon +lemur +lena +lenny +lenovo +lenta +leo +leon +leonardo +leopard +les +lesbian +leto +letter +letters +lettres +lewis +lex +lexington +lexus +lf +lft +lg +lgb +lgc +lh +li +lian +lib +lib1 +lib2 +libanswers +libcal +libcat +liberty +liberty-mutual +libertymutual +libertymutualinsurance +libertymutualinsurancegroup +libguides +libopac +libproxy +libra +libraries +library +library2 +libtest +libweb +lic +licence +license +licensing +lider +life +liferay +lifestyle +liga +light +lighthouse +lightning +like +lille +lily +lima +lime +limesurvey +lims +lin +lina +lincoln +linda +line +line3 +line4 +lineage +lineage2 +lining +link +link2 +linkedin +links +linus +linux +linux0 +linux01 +linux02 +linux1 +linux11 +linux2 +linux3 +lion +lip +lipetsk +liquid +lis +lisa +list +lista +listas +liste +listen +listes +listings +lists +lists2 +listserv +listserv2 +listserver +lit +lite +lithium +liu +live +live1 +live2 +live3 +livecam +livecams +livechat +livedata +livehelp +liverpool +livestats +livestream +livesupport +livnmi +lj +lk +ll +lm +lmc +lms +lms2 +ln +lnk +lnmp +lo +load +loadbalancer +loadtest +loan +lobby +lobster +local +locale +localhost +localmail +location +locations +locator +lock +lockheed +lockheedmartin +lodz +log +log0 +log01 +log02 +log1 +log2 +logan +logfile +logfiles +logger +logging +loghost +logic +login +login1 +login2 +logistica +logistics +logo +logon +logos +logs +logserver +loisirs +loja +loki +lol +lon-cisco +london +long +longbeach +longisland +look +lookup +loopback +loopback-host +losangeles +lost +loto +lottery +lotto +lotus +louisiana +louisville +lounge +love +loves +lowes +lp +lp1 +lp2 +lp3 +lpm +lppm +lps +lpse +lq +lr +lrc +ls +lsan03 +lsc +lst +lt +ltc +ltrkar +lts +ltx +ltxc +lu +lublin +lucas +lucifer +lucky +lucy +lug +luke +lulu +luna +lupus +lux +luxembourg +lv +lviv +lvs +lvs1 +lvs2 +lw +lwj +lx +lxy +ly +lyg +lyj +lync +lyncaccess +lyncav +lyncdiscover +lyncdiscoverinternal +lyncedge +lyncrp +lyncsip +lyncweb +lyncwebconf +lynx +lyon +lyra +lyrics +lyris +lz +m +m-dev +m-test +m0 +m01 +m1 +m10 +m11 +m12 +m13 +m14 +m16 +m19 +m2 +m2m +m3 +m4 +m5 +m6 +m7 +m8 +m9 +ma +ma1 +maa +mac +mac1 +mac10 +mac11 +mac2 +mac3 +mac4 +mac5 +macduff +mach +macintosh +mad +madison +madrid +maestro +mag +mag1 +mag2 +magazin +magazine +magazines +mage +magellan +magento +maggie +magic +magma +magnesium +magnet +magnitogorsk +magnolia +mahara +mai +maia +mail +mail-1 +mail-2 +mail-4 +mail-backup +mail-gw +mail-old +mail-out +mail-relay +mail0 +mail01 +mail02 +mail03 +mail04 +mail05 +mail06 +mail07 +mail1 +mail10 +mail11 +mail12 +mail13 +mail14 +mail15 +mail16 +mail17 +mail18 +mail2 +mail20 +mail21 +mail22 +mail250 +mail3 +mail30 +mail31 +mail32 +mail33 +mail34 +mail35 +mail36 +mail37 +mail38 +mail39 +mail4 +mail5 +mail6 +mail7 +mail8 +mail9 +maila +mailadmin +mailarchive +mailb +mailbackup +mailbck +mailbox +mailboxes +mailc +mailcampaign +mailcontrol +maild +mailer +mailer1 +mailer2 +mailers +mailfilter +mailgate +mailgate1 +mailgate2 +mailgate3 +mailgateway +mailguard +mailgw +mailgw1 +mailgw2 +mailhost +mailhost2 +mailhub +mailin +mailing +mailinglist +mailings +maillist +maillists +maillog +mailman +mailmems +mailmx +mailold +mailout +mailout2 +mailrelay +mailroom +mails +mailscan +mailscanner +mailserv +mailserver +mailserver1 +mailserver2 +mailservice +mailsite +mailsrv +mailstore +mailsv +mailtest +mailweb +mailx +main +main2 +maine +maint +maintenance +mais +mak +malaysia +mali +mall +malotedigital +malta +mam +mama +mamba +mambo +mammoth +man +manage +manage-ds +manage-vps +manage2 +managedns +managedomain +managehosting +management +managemydomain +manager +managethisdomain +managevps +manageyourdomain +manchester +mandarin +mang +manga +mango +manhattan +manila +mantis +manual +manufacturing +manyi +map +mapa +mapas +mapi +maple +maps +mapserver +mapy +mar +marathon +marathonoil +marc +marconi +marge +mari +maria +marina +marine +mario +maritime +marius +mark +market +marketing +marketplace +markets +mars +mars2 +marseille +marshall +marte +martin +martinique +marvin +marx +mary +maryland +mas +masa +mason +massachusetts +massachusettsmutual +massachusettsmutuallife +massachusettsmutuallifeinsurance +massage +massmail +master +master2 +masters +mat +match +matematik +material +math +maths +matlab +matricula +matrix +matrixstats +matt +mature +maui +maven +maverick +max +maxim +maxonline +maxwell +maxx +maya +mazda +mb +mb2 +mba +mbm +mbox +mbs +mbt +mc +mc1 +mc2 +mca +mcafee +mcc +mce +mcfeely +mci +mckesson +mcm +mco +mcp +mcs +mcu +md +md1 +mda +mdaemon +mdb +mdc +mdev +mdm +mds +me +mebel +mec +mech +mechatronics +med +medco +medcohealth +medcohealthsolutions +media +media-1 +media01 +media1 +media2 +media3 +media4 +media5 +media6 +mediacenter +mediakit +medias +mediaserver +mediasite +mediawiki +medical +medicina +medicine +medios +medusa +medya +meet +meeting +meetings +mega +megaegg +megaplan +megared +megatron +mein +mel +melbourne +melody +melon +mem +member +member2 +memberall +memberlite +memberold +memberpbp +members +members2 +membership +membres +memo +memorial +memories +memory +memphis +men +menstruation +mentor +menu +merak +mercedes +merchant +merck +mercure +mercurio +mercury +meridian +merkur +merkury +merlin +mes +mesh +message +messagerie +messages +messaging +messenger +met +meta +meta01 +meta02 +meta03 +meta1 +meta2 +meta3 +metadata +metal +metalib +metc +meteo +meteor +metis +metlife +metric +metrics +metro +metropolis +mevlana +mex +mexico +mezun +mf +mf2 +mfc +mfs +mft +mg +mg1 +mg2 +mgame +mgate +mgm +mgmt +mgr +mgs +mgt +mgw +mh +mhs +mi +mia +miamfl +miami +miao +mib +mic +michael +michaelyu +michigan +mickey +micro +microsite +microsites +microsoft +mid +midas +midget +midia +midwest +mie +miembros +migrate +migration +mii +mijn +mike +miki +mil +milan +milano +military +milk +millenium +millennium +miller +milton +milwaukee +milwwi +mim +mimi +mimosa +min +mind +mine +minecraft +minerva +minfin +mini +mining +minisites +minneapolis +minnesota +minside +minsk +mint +mio +mir +mira +mirage +miranda +mirror +mirror1 +mirror2 +mirror3 +mirrors +mis +misc +miss +mississippi +missouri +mistral +mitsubishi +mix +mizar +mj +mk +mks +mkt +mktg +ml +mlc +mlib +mlm +mlogin +mls +mm +mm1 +mm2 +mma +mmail +mmc +mmf +mmi +mmm +mmp +mms +mmt +mn +mnews +mng +mngt +mntr +mo +moa +mob +moban +mobi +mobiel +mobil +mobile +mobile-test +mobile1 +mobile2 +mobileapp +mobileapps +mobiledev +mobileiron +mobilemail +mobileonline +mobiletest +mobility +moc +mod +moda +mode +model +models +modem +moderator +modify +module +modules +moe +moj +mol +molde-gsw +mole +molly +mom +momo +mon +mon1 +mon2 +monaco +money +mongo +mongoose +monica +monit +monitor +monitor1 +monitor2 +monitoramento +monitoreo +monitoring +monitoring2 +monkey +monroe +monster +montana +montreal +moo +mooc +moodle +moodle-dev +moodle-test +moodle1 +moodle2 +moodledev +moodletest +moon +moose +mordor +more +morgan +morgan-stanley +morganstanley +morpheus +mortgage +morton +mos +mosaic +moscow +moses +moss +mother +motion +moto +motor +mouse +mov +move +movie +movie1 +moviegalls1 +moviegalls2 +moviegalls3 +moviegalls4 +moviegalls5 +movies +movil +moving +mox +mozart +mp +mp1 +mp2 +mp3 +mp4 +mp5 +mp7 +mpa +mpacc +mpeg +mpg +mpi +mpls +mpp +mpr +mprod +mps +mq +mr +mr1 +mrc +mrm +mrp +mrs +mrt +mrtg +mrtg1 +mrtg2 +ms +ms-exchange +ms-sql +ms1 +ms2 +ms3 +msa +msb +msc +msdn +msdnaa +mse +msexchange +msg +msi +msk +msm +msn +msoid +msp +mss +mssnks +mssql +mssql0 +mssql01 +mssql1 +mssql2 +mssql3 +mssql4 +mssql5 +mssql6 +mssql7 +mssql8 +mssqladmin +mssqlext +mssqlint +mst +mstage +msu +msw +msy +mt +mt2 +mta +mta01 +mta1 +mta2 +mta3 +mta4 +mta5 +mta6 +mtb +mtc +mtest +mti +mtm +mtn +mtnl +mts +mtt +mtu +mtv +mu +mua +mud +multi +multimedia +mum +mumbai +mumble +mun +munin +murmansk +murray +mus +muse +museum +music +music-hn +musica +musik +musique +mustang +muz +muzeum +mv +mvc +mvs +mw +mweb +mwww +mx +mx-1 +mx-a +mx-b +mx0 +mx00 +mx01 +mx02 +mx03 +mx04 +mx05 +mx1 +mx10 +mx11 +mx12 +mx13 +mx14 +mx15 +mx2 +mx20 +mx21 +mx22 +mx3 +mx30 +mx4 +mx5 +mx6 +mx7 +mx8 +mx9 +mxbackup +mxs +my +my1 +my2 +my3 +myaccount +myadmin +myapps +mycampus +mycp +mycpanel +mydb +mydev +mydomain +myfiles +myip +mymail +myo +mypage +mypc +myphp +myportal +myshop +mysite +mysites +myspace +mysql +mysql0 +mysql01 +mysql02 +mysql03 +mysql04 +mysql05 +mysql1 +mysql10 +mysql11 +mysql2 +mysql3 +mysql4 +mysql4ext +mysql4int +mysql5 +mysql506 +mysql5ext +mysql5int +mysql6 +mysql7 +mysql8 +mysql9 +mysqladmin +mytest +myweb +mywebmail +mz +mzt +n +n1 +n2 +n3 +n4 +n6 +n7 +na +nac +nag +nagasaki +nagios +nagios2 +nalog +nam +name +name1 +name2 +names +nameserv +nameserver +nancy +nanjing +nanke +nano +nantes +nara +naruto +narvik-gw3 +nas +nas01 +nas1 +nas2 +nashville +nat +nat-pool +nat1 +nat2 +nat3 +nat4 +national +nats +nature +nauka +nautilus +nav +navi +navigator +nb +nba +nc +ncc +ncs +nd +nds +ndt +ne +nebraska +nebula +nec +negocios +nelson +nemesis +nemo +neo +neon +nepal +neptun +neptune +nero +nessus +nest +nestle +nestor +net +net1 +net2 +net9design +netacad +netadmin +netapp +netcom-gw +netdata +netflow +netgear +netherlands +netlab +netmail +netman +netmang +netmeeting +netmon +netops +netscaler +netscreen +netstat +netstats +netstorage +netvision +network +network-ip +networks +neu +neuro +nevada +nevis +new +new-www +new1 +new2 +new3 +newdesign +newdev +newforum +newftp +newhampshire +newjersey +newmail +newman +newmedia +newmexico +neworleans +news +news-corp +news1 +news2 +news3 +newscorp +newserver +newsfeed +newsfeeds +newsgroups +newsite +newsletter +newsletter2 +newsletters +newspaper +newsroom +newtest +newton +newweb +newwebmail +newwww +newyear +newyork +newyorklife +newyorklifeinsurance +newzealand +next +nextel +nextgen +nexus +nf +nfc +nfl +nfs +nfs1 +nfsen +ng +nginx +ngo +ngwnameserver +ngwnameserver2 +nh +nhce +nhko1111 +nhl +nhs +ni +niagara +nib +nic +nice +nick +nickel +nico +nicole +nieruchomosci +nieuw +nieuwsbrief +nigeria +night +nightly +nike +nikita +nil +nimbus +nina +ningxia +ninja +nirvana +nis +nissan +nit +nitrogen +niu +nj +nk +nl +nl2 +nlp +nm +nmail +nmc +nms +nms2 +nn +nnovgorod +nntp +no +no-dns +no-dns-yet +noah +nobel +noc +noc2 +nod +nod32 +node +node01 +node1 +node2 +node3 +node4 +nokia +nomad +nombres +noname +none +nonnude +nono +noprefix +nora +nord +norma +north +northcarolina +northdakota +northeast +northrop +northrop-grumman +northropgrumman +northwest +norway +nospam +nostromo +not-set-yet +notas +note +notebook +notes +nothing +notice +noticias +notification +notify +nov +nova +novel +novel66 +novell +november +novgorod +novo +novosibirsk +now +nowa +nowe +nowy +np +npc +npm +nps +npx +nr +ns +ns- +ns-1 +ns-2 +ns0 +ns01 +ns02 +ns03 +ns04 +ns05 +ns06 +ns1 +ns10 +ns100 +ns101 +ns102 +ns103 +ns104 +ns105 +ns11 +ns110 +ns111 +ns112 +ns113 +ns114 +ns12 +ns120 +ns121 +ns122 +ns13 +ns14 +ns15 +ns16 +ns17 +ns18 +ns19 +ns1a +ns2 +ns20 +ns201 +ns202 +ns21 +ns22 +ns22266 +ns23 +ns24 +ns24331 +ns25 +ns26 +ns27 +ns28 +ns29 +ns2a +ns3 +ns30 +ns31 +ns32 +ns33 +ns34 +ns35 +ns36 +ns37 +ns38 +ns39 +ns4 +ns40 +ns41 +ns42 +ns43 +ns44 +ns45 +ns46 +ns47 +ns48 +ns49 +ns5 +ns50 +ns51 +ns52 +ns53 +ns54 +ns55 +ns56 +ns57 +ns58 +ns59 +ns6 +ns60 +ns61 +ns62 +ns63 +ns64 +ns7 +ns70 +ns71 +ns72 +ns77 +ns8 +ns80 +ns81 +ns82 +ns9 +ns91 +ns92 +ns_ +nsa +nsb +nsc +nsd +nse +nsk +nsm +nsp +nsrhost +nss +nst +nsw +nswc +nsx +nt +nt-server +nt1 +nt2 +nt4 +nt40 +ntc +ntmail +ntop +ntp +ntp0 +ntp1 +ntp2 +ntp3 +ntp4 +nts +ntserver +ntt +ntv +nu +nuclear +nucleus +nudesport +nudist +nueva +nuevo +nuke +null +nursing +nutrition +nuxeo +nv +nv-ad-hn +nv-img-hn +nw +nw1 +nws +nx +ny +nyalesund-gw +nyc +nycap +nylife +nylifeinsurance +nyx +nz +o +o2 +oa +oa1 +oa2 +oai +oak +oakland +oas +oascentral +oasis +oauth +ob +obchod +obelix +oberon +obi +obit +obits +obiwan +object +obs +observatorio +observer +observium +oc +ocean +ocn +ocs +ocsp +ocsweb +octopus +ocw +od +odessa +odin +odn +odp +ods +odyssey +oe +oec +oem +oes +of +oferta +ofertas +off +offer +offers +office +office1 +office2 +office365 +offices +official +offline +offsite +oficina +og +ogloszenia +ogr +ogrenci +ogrencikonseyi +oh +ohio +oic +oid +oidb +oil +oilfield +ois +oita +oj +ojs +ok +okc +okcyok +okinawa +oklahoma +oklahomacity +okna +ol +ola +olap +old +old-www +old1 +old2 +old3 +oldblog +olddev +oldforum +oldftp +oldmail +oldman +oldsite +oldweb +oldwebmail +oldwww +ole +oleg +olga +olimp +olive +oliver +olivier +olsztyn +olymp +olympic +olympics +olympus +om +oma +omah +omaha +omail +omega +omicron +omni +oms +omsk +on +ondemand +one +online +online2 +onlinemail +onlineshop +only +ons +ontario +onyx +oob +ooo +op +op2 +opa +opac +opal +opc +opel +open +openapi +openbsd +opencart +opendata +openemm +openerp +openfire +openhouse +openid +openmeetings +opennms +opensource +openview +openvpn +openx +opera +operation +operations +operator +opinion +opole +opros +ops +ops0 +ops01 +ops02 +ops1 +ops2 +opsview +opsware +opt +optima +optimum +optimus +optusnet +opus +or +ora +oracle +oral +orange +orb +orbit +orc +orca +orchid +order +orders +oregon +orel +orenburg +org +org-www +ori +orient +orientation +origen +origen-www +origin +origin-cdn +origin-images +origin-live +origin-m +origin-staging +origin-user +origin-www +origin2 +orion +orion2 +orlando +os +osaka +osc +oscar +osiris +oskol +oslo +oslo-gw +oslo-gw1 +oslo-gw4 +oslo-gw7 +osm +oss +ost +osx +ot +ota +otc +other +otp +otrs +otrs2 +ots +ott +ottawa +otter +otto +ou +oud +our +out +outage +outbound +outbound1 +outdoor +outgoing +outils +outlet +outlook +outmail +outreach +outside +outsourcing +ouvidoria +ov +ovh +ovpn +ovpn-uk +ovpn-us +owa +owa01 +owa02 +owa1 +owa2 +owb +owl +owncloud +ows +ox +ox-d +ox-i +ox-ui +oxford +oxnard +oxygen +oyp +oyun +oz +ozone +ozzy +p +p0rn +p1 +p2 +p2p +p3 +p4 +p5 +p6 +p7 +pa +pablo +pabx +pac +pace +pacific +pack +packages +pacs +pad +paf +page +pager +pagerank +pages +paginas +pagos +pai +paiement +painel +painelstats +paintball +pal +palladium +pallas +palm +pan +panama +panasonic +panda +pandora +panel +panelstats +panelstatsmail +pano +panopto +panorama +pantera +panther +pantyhose +pap +papa +paper +papercut +papers +paradise +paraguay +parana +parceiros +parent +parents +paris +park +parker +parking +parks +parners +parser +partage +partenaires +partner +partner2 +partnerapi +partnerpage +partners +partnerzy +parts +party +parus +pas +pasca +pascal +pass +passport +password +passwordreset +paste +pastebin +pasteur +pat +patch +patches +patent +path +pathfinder +patrick +patrimonio +paul +pav +pay +pay2 +pay3 +paygate +payment +payments +paynow +paypal +payroll +pb +pbi +pbl +pbs +pbx +pbx1 +pbx2 +pc +pc01 +pc1 +pc10 +pc101 +pc11 +pc12 +pc13 +pc14 +pc15 +pc16 +pc17 +pc18 +pc19 +pc2 +pc20 +pc21 +pc22 +pc23 +pc24 +pc25 +pc26 +pc27 +pc28 +pc29 +pc3 +pc30 +pc31 +pc32 +pc33 +pc34 +pc35 +pc36 +pc37 +pc38 +pc39 +pc4 +pc40 +pc41 +pc42 +pc43 +pc44 +pc45 +pc46 +pc47 +pc48 +pc49 +pc5 +pc50 +pc51 +pc52 +pc53 +pc54 +pc55 +pc56 +pc57 +pc58 +pc59 +pc6 +pc60 +pc7 +pc8 +pc9 +pca +pcanywhere +pcdn +pci +pcm +pcmail +pcs +pct +pd +pda +pdb +pdc +pdd +pdf +pdm +pdns +pds +pdu1 +pdu2 +pe +peace +peach +peanut +pear +pearl +pec +ped +pedro +peer +pegasus +peixun +pelican +pen +pendrell +penelope +penguin +pennsylvania +pentaho +penza +people +peoplesoft +pepsi +pepsico +per +perevod +perfil +performance +pergamum +periodicos +perl +perlbal-release +perm +perpus +perpustakaan +pers +persephone +perseus +perseus2 +perso +person +persona +personal +personals +personel +personnel +perth +peru +pes +pesquisa +pet +peter +petra +pets +peugeot +pf +pf1 +pfa +pfizer +pfsense +pg +pg1 +pg2 +pgadmin +pgp +pgs +pgsql +pgsql1 +pgsql2 +pgu +ph +phantom +pharm +pharma +pharmacy +pharos +phd +phenix +phi +phil +philadelphia +philip-morris +philipmorris +philipmorrisinternational +philips +phillips +philosophy +phnx +phobos +phoebe +phoenix +phoeniz +phone +phonebook +phones +phorum +photo +photo1 +photo2 +photo3 +photobook +photography +photon +photos +photos0 +photos1 +photos2 +photos3 +photos4 +photos5 +photos6 +photos7 +photos8 +photos9 +photoshop +phototheque +php +php4 +php5 +phpadmin +phpbb +phplist +phpmyadmin +phx +phy +phys +physics +pi +piano +piao +pic +pic1 +pic2 +picard +picasso +piclist +pics +pics2 +picture +pictures +picwww +pie +pierre +pif +pig +pigeon +pigg-life +pila +pilot +pim +pimg +pims +pin +pine +ping +ping0 +ping1 +pingan +pinger +pink +pinky +pinnacle +pinpai +pioneer +pip +pipeline +pipex-gw +pippin +piranha +piranha-all +pisces +pissing +pittsburgh +pivot +piwik +pix +pixel +pizza +pj +pje +pk +pkg +pki +pl +pla +placement +places +plala +plan +planck +planeacion +planet +planeta +planetarium +planner +planning +plano +plant +plasma +plastic +plataforma +platform +platforma +platinum +plato +platon +play +player +playground +plaza +pleiades +plesk +plesk1 +pliki +plm +plone +pls +plt +pltn13 +plugin +plugins +plum +plus +pluto +pluton +pm +pm1 +pm2 +pma +pma2 +pmail +pmb +pmc +pmd +pmg +pmi +pmo +pmp +pms +pmt +pn +pnc +pns +po +po2 +pobeda +poc +pochta +poczta +poczta2 +pod +podarki +podarok +podcast +podcasts +podpiska +poems +poetry +pogoda +point +points +poisk +poker +pol +poland +polar +polaris +police +policies +policy +polit +politics +politik +politika +poll +polladmin +polling +polls +pollux +polo +polycom +polymer +pomoc +pon +ponto +pony +pooh +pool +pools +pop +pop1 +pop2 +pop3 +pop3s +popd +popmail +pops +popular +popup +popwebmail +porn +porno +porsche +port +portafolio +portail +portal +portal1 +portal2 +portal3 +portaldev +portale +portals +portaltest +portfolio +portland +portugal +portuguese +pos +poseidon +post +post2 +posta +posta01 +posta02 +posta03 +posta2 +postales +poste +poster +postfix +postfixadmin +postgres +postgresql +postman +postmaster +postoffice +potala +pov +power +poze +poznan +pozycjonowanie +pp +ppa +ppc +ppl +ppm +ppp +ppp1 +ppp10 +ppp11 +ppp12 +ppp13 +ppp14 +ppp15 +ppp16 +ppp17 +ppp18 +ppp19 +ppp2 +ppp20 +ppp21 +ppp3 +ppp4 +ppp5 +ppp6 +ppp7 +ppp8 +ppp9 +pppoe +ppr +pps +pptp +pr +pr0n +pr1 +pr2 +praca +practice +prague +pravo +praxis +prc +prd +pre +pre-prod +pre-production +pre-www +pregnant +prelive +prelive-admin +prem +premier +premiere +premium +prensa +prep +prepaid +preprod +pres +presence +present +presentation +president +press +presse +pressroom +presta +prestashop +prestige +prev +preview +preview1 +preview2 +prewww +pri +price +pricing +pride +priem +prikol +prima +primary +prime +primo +primus +prince +print +printer +printers +printing +printserv +printserver +printshop +prism +prisma +priv +privacy +private +prm +pro +proba +probe +problemtracker +process +proctor-gamble +proctorandgamble +procurement +procyon +prod +prod-empresarial +prod-infinitum +prod1 +prod2 +prodigy +product +production +productos +products +prof +professional +professor +profi +profil +profile +profiles +profit +profkom +prog +program +programs +programy +progress +proj +proje +project +project1 +project2 +projects +projekt +projekty +projet +projeto +projetos +projets +promedia +prometheus +promo +promociones +promos +promotion +promotions +pronto +proof +property +proposal +proposals +prospect +prospero +protect +proteus +proto +protocollo +proton +proton-multi +prototype +prov +prova +proveedores +provider +providers +provincia +provision +provisioning +provost +proxies +proxy +proxy01 +proxy02 +proxy1 +proxy2 +proxy3 +proxy4 +proxy5 +proxy6 +proxy7 +proyectos +prs +prtg +prudential +prudential-financial +prudentialfinancial +prueba +pruebas +prx +ps +ps1 +ps2 +ps3 +psa +psc +psd +psi +psicologia +pskov +psm +psp +psql +pss +psy +psych +psycho +psychology +pt +pta +ptc +pti +ptk +ptld +ptm +ptr +pts +pub +pub2 +public +public1 +public2 +publica +publicaciones +publicapi +publications +publicidad +publicitate +publinet +publish +publisher +publishing +publix +publixsupermarkets +pubs +pubsub +puck +pulsar +pulse +puma +pumpkin +puppet +puppetmaster +purchase +purchasing +pure +purple +push +pushmail +puskom +pustaka +puzzle +pv +pvc +pw +pw20024358 +pwc +pwd +px +pxe +py +python +pz +q +q1 +q3 +qa +qa1 +qa2 +qa3 +qab +qam +qb +qc +qd +qeyo +qg +qgzx +qh +qhd +qing +qinghai +qis +qj +qk +qlikview +qm +qmail +qmailadmin +qms +qotd +qp +qq +qqmail +qr +qrcode +qs +qt +qtss +quad +quake +quality +quan +quantum +quarantine +quark +quartz +quebec +queen +queens +queries +query +quest +questionnaire +questions +quick +quiz +quizadmin +quote +quotes +quran +qw +qy +qz +qzlx +r +r01 +r02 +r1 +r1soft +r2 +r25 +r2d2 +r3 +r4 +r7 +ra +rabbit +rabota +race +racktables +rad +rad2 +radar +radio +radio2 +radios +radius +radius1 +radius2 +radius3 +radon +radyo +raf +ragnarok +rai +rail +rails +rain +rainbow +rakuten +ram +ramses +ran +rancid +random +rank +ranking +raovat +rap +rape +raphael +rapid +rapidsite +raptor +ras +rat +rate +rating +raven +ray +raytheon +rb +rbl +rbs +rbt +rc +rc1 +rcc +rcs +rd +rdc +rdg +rdns +rdns1 +rdns2 +rdp +rds +rdv +rdweb +re +reach +read +reader +reading +real +real1 +real2 +realestate +reality +realserver +realtime +realtor +realty +reboot +rec +receiver +recette +recherche +recipes +record +records +recovery +recruit +recruiter +recruiting +recruitment +recrutement +rector +recursos +recycling +red +red2 +red5 +redaccion +redaktion +redbull +redes +redesign +redhat +redir +redirect +redirector +redirects +redis +redmine +redmine2 +reestr +ref +refer +referat +reference +referencement +reg +reg1 +reg2 +regi +regie +regina +region +regione +regions +regis +regist +register +registrar +registrasi +registration +registro +registry +regs +regulus +rehber +rei +rejestracja +reklam +reklama +rekrutacja +relais +relatorio +relaunch +relax +relay +relay01 +relay02 +relay03 +relay1 +relay2 +relay3 +relay4 +relay5 +release +release-chat +release-chat-service +release-commondata +release0000 +releasephp +relief +religion +rem +remax +remedy +remix +remont +remote +remote1 +remote2 +remoteaccess +remotesupport +remoto +remstats +remus +ren +renault +rencontre +rencontres +renew +renewal +reno +renshi +rent +rental +renwen +rep +repair +reply +repo +report +reporter +reportes +reporting +reports +reports2 +repos +repositorio +repository +reprints +repro +request +res +res1 +research +reseller +resellers +reservas +reservation +reservations +reserve +reserved +reset +residence +resim +resnet +resolve +resolver +resolver1 +resolver2 +resource +resources +resp +response +responsive +ressources +rest +restaurant +restricted +result +resultats +results +resume +resumenes +retail +retailer +retracker +retro +return +reunion +rev +reverse +review +reviews +revista +revistas +rewards +rews +rex +rf +rfb +rfid +rg +rh +rhea +rhino +rho +rhodeisland +ri +ria +ric +ricard +ricardo +rich +richmond +ricoh +rid +rideofthemonth +rides +riga +rigel +ring +rio +ripe +ris +rise +risk +rite-aid +riteaid +river +riverside +rj +rk +rl +rm +rma +rmc +rmi +rms +rmt +rn +rnail +rnd +rnicrosoft +ro +roadrunner +rob +robert +roberto +robin +robinhood +robo +robot +robotics +rock +rocky +rod +roger +rogers +rogue +roi +roku +roma +roman +romania +rome +romeo +romulus +room +rooms +root +rootservers +rosa +rose +rostov +roundcube +route +router +router-b +router-uk +router-us +router1 +router2 +routing +roy +royal +rp +rpa +rpc +rpg +rpm +rproxy +rps +rpt +rqd +rr +rrd +rrhh +rs +rs1 +rs2 +rs3 +rsa +rsc +rse +rsj +rsm +rss +rst +rsvp +rsync +rt +rt1 +rt2 +rt3 +rtc +rtelnet +rtg +rti +rtmp +rtr +rtr01 +rtr1 +rts +rtx +ru +ru1 +ru2 +rubicon +rubin +ruby +rugby +run +rune +rural +rus +russia +russian +rv +rw +rwhois +rww +rwxy +rx +ryan +ryazan +rz +s +s-dtap +s-dtap2 +s0 +s01 +s02 +s03 +s04 +s06 +s1 +s10 +s100 +s101 +s102 +s103 +s104 +s105 +s106 +s107 +s108 +s109 +s11 +s110 +s111 +s112 +s113 +s114 +s115 +s116 +s117 +s118 +s119 +s12 +s120 +s121 +s122 +s123 +s124 +s125 +s126 +s127 +s128 +s129 +s13 +s130 +s131 +s132 +s133 +s134 +s135 +s136 +s137 +s138 +s139 +s14 +s140 +s141 +s142 +s143 +s144 +s148 +s15 +s156 +s157 +s16 +s17 +s18 +s19 +s194 +s2 +s20 +s200 +s201 +s202 +s203 +s204 +s205 +s206 +s207 +s208 +s209 +s21 +s210 +s211 +s212 +s213 +s214 +s215 +s216 +s217 +s218 +s219 +s22 +s220 +s221 +s222 +s225 +s226 +s227 +s23 +s24 +s25 +s26 +s27 +s28 +s29 +s3 +s30 +s31 +s32 +s33 +s34 +s35 +s36 +s37 +s38 +s39 +s4 +s40 +s41 +s42 +s43 +s44 +s45 +s46 +s47 +s48 +s49 +s5 +s50 +s51 +s52 +s53 +s54 +s55 +s56 +s57 +s58 +s59 +s6 +s60 +s61 +s62 +s63 +s64 +s65 +s66 +s67 +s68 +s69 +s7 +s71 +s72 +s73 +s75 +s77 +s78 +s79 +s8 +s80 +s81 +s82 +s83 +s84 +s85 +s89 +s9 +s91 +sa +sa2 +saas +sac +sacramento +sacs +sad +sadmin +sae +saf +safari +safe +safety +safeway +saga +sage +sai +sail +sakai +sakura +sal +sale +sales +salon +salsa +salt +saltlake +salud +sam +samara +samba +sametime +saml +sample +samples +samson +samsung +samuel +samurai +san +sanantonio +sand +sandbox +sandbox1 +sandbox2 +sandd-dev-commondata +sandiego +sanfrancisco +sanguo +sanjose +sante +santiago +sao +sap +sapphire +sapporo +saprouter +sar +sara +sarah +saransk +saratov +sarg +saruman +sas +saskatchewan +sat +satellite +saturn +saturne +saturno +saulcy-gw +sauron +sav +sava +savannah +save +save-big +savebig +savenow +savvis-admin-commondata +savvis-dev-commondata +sawmill +sb +sba +sbc +sbe +sbl +sbs +sc +sc1 +sc2 +sca +scan +scanner +scarab +scarlet +scc +sccm +scd +scdn +sce +sch +schedule +scheduler +schedules +scholar +scholarships +school +schools +sci +science +scm +sco +scom +scooter +score +scores +scorpio +scorpion +scotland +scott +scotty +scout +scp +scr +scratch +screen +screenshot +scribe +script +scripts +scs +sd +sd1 +sd2 +sd3 +sda +sdb +sdc +sdh +sdk +sdm +sdo +sdp +sds +se +sea +seafight +seal +search +search1 +search2 +sears +sears-holdings +searsholdings +seat +seattle +sec +secmail +second +secondary +secret +secure +secure1 +secure2 +secure3 +secure4 +secure5 +secured +secureftp +securelab +securemail +secureweb +securid +security +sed +sede +see +seed +seer +seg +sega +seguridad +seguro +sei +select +selene +selenium +self +selfcare +selfservice +sell +seller +sem +seminar +seminars +sems +senat +senate +send +sender +sendgrid +sendmail +sendy +senegal +senior +sensor +sentinel +sentry +seo +seoul +sep +sequoia +ser +serenity +sergey +sergio +seri +serial +serv +serv-refi +serv1 +serv2 +server +server01 +server02 +server03 +server04 +server05 +server06 +server07 +server1 +server10 +server11 +server12 +server13 +server14 +server15 +server16 +server17 +server18 +server19 +server2 +server20 +server21 +server22 +server23 +server24 +server25 +server26 +server27 +server28 +server29 +server3 +server30 +server31 +server32 +server33 +server34 +server35 +server36 +server37 +server38 +server39 +server4 +server40 +server41 +server42 +server43 +server44 +server45 +server46 +server47 +server5 +server50 +server51 +server52 +server55 +server6 +server7 +server8 +server9 +servers +serveur +service +service1 +service2 +servicedesk +services +services2 +servicio +servicios +servicos +servidor +servis +servizi +serwer +serwis +ses +sesame +seshat +set +seth +settings +setup +seven +sex +sexshop +sexy +sf +sfa +sfc +sfl +sfr +sfs +sft +sftp +sftp2 +sfx +sfzx +sg +sg1 +sga +sgb +sgc +sgd +sge +sgi +sgp +sgs +sgw +sh +sh1 +sh2 +shadow +shanghai +shanxi +share +shared +sharefile +sharepoint +shareware +sharing +shark +sharp +shell +shenji +shenzhen +shib +shibboleth +shipping +shitting +shiva +shoes +shop +shop1 +shop2 +shop3 +shoppers +shopping +shops +shoptest +short +shortcut +shortcuts +shortlinks +shouji +shoutcast +show +showcase +showroom +shrek +shs +shu +shuzai +si +si1d +sia +siac +siam +siap +sib +sic +sid +sie +siebel +siemens +sierra +sierra-db +sif +sife +sig +siga +sigma +sign +signature +signin +signup +signups +sii +silicon +silo +silver +sim +simba +simcdnws +simg +simon +simpeg +simple +sims +sin +sina +singapore +sip +sip1 +sip2 +sip3 +sipexternal +sipinternal +sir +sirio +sirius +sis +sistema +sistemas +sit +site +site1 +site2 +site3 +site4 +site5 +siteadmin +sitebuilder +sitedefender +sitelife +sitemap +sitenews +sites +sitetest +sitios +six +sj +sj1 +sj2 +sjb +sjc +sjz +sk +skb +skc +sketchup +ski +skidki +skin +sklad +sklep +skoda +sks +skt +sky +skyline +skynet +skype +skyrama +skywalker +sl +sl2 +sla +slackware +slash +slashinvoice +slave +slave1 +slb +slc +slim +slm +slmail +sls +slut +slx +sm +sm1 +sm2 +sma +smail +smalltits +smart +smarterstats +smarthost +smartrelay +smartstats +smarty +smb +smc +sme +smetrics +smf +smg +smi +smile +smith +smithers +smk +sml +smm +smoke +smokeping +smolensk +smp +smpp +smpt +smr +sms +sms1 +sms2 +smsgate +smsgateway +smsgw +smt +smtp +smtp-gw +smtp-in +smtp-out +smtp-out-01 +smtp-relay +smtp0 +smtp01 +smtp02 +smtp03 +smtp04 +smtp05 +smtp06 +smtp1 +smtp10 +smtp11 +smtp12 +smtp13 +smtp14 +smtp15 +smtp16 +smtp2 +smtp3 +smtp4 +smtp5 +smtp6 +smtp7 +smtp8 +smtp9 +smtpa +smtpauth +smtpext +smtpgw +smtphost +smtpin +smtpmail +smtpmax +smtpout +smtpout2 +smtprelay +smtps +smtptest +smu +sn +snail +snake +snap +sng +snies +sniffer +sniper +snmp +snmpd +snoopy +snort +snow +sns +so +soa +soap +soc +socal +soccer +sochi +social +socialize +socialmedia +society +sociology +socios +socket +socks +socrates +sodium +sofia +soft +software +sogo +sogou +soho +sok +sol +solar +solaris +solarwinds +soleil +solid +solo +solomon +solr +soluciones +solusvm +solution +solutions +som +soma +sonar +sondage +song +songs +sonic +sonic2 +sonicwall +sonoivu +sony +sophia +sophos +soporte +sorbete +sorry +sos +soso +sotttt +sou +sound +source +sourcecode +sources +sourcesafe +south +southcarolina +southdakota +southeast +southwest +sovet +sp +sp-test +sp1 +sp2 +spa +space +spaces +spacewalk +spain +spam +spam01 +spam02 +spam1 +spam2 +spamd +spamfilter +spamfilter1 +spamfilter2 +spamwall +spanish +spanking +spare +sparemx +spark +sparkhost +spartan +spb +spc +spe +spec +special +specials +spectrum +speech +speed +speedtest +speedtest1 +speedtest2 +speedtest3 +speedtest4 +speedy +spell +spf +sph +sphinx +spi +spica +spiceworks +spider +spiderman +spielwiese +spike +spin +spirit +spitfire +splash +splunk +spm +spo +spock +spokane +spokes +sponsor +sponsors +spor +sport +sports +spot +spp +spravka +spreadsheet +spreadsheets +spring +springfield +sprint +sprint-nextel +sprintnextel +sprout +sps +spss +spt +sptest +sputnik +spy +sq +sqa +sql +sql0 +sql01 +sql02 +sql1 +sql2 +sql3 +sql4 +sql5 +sql6 +sql7 +sqladmin +sqlserver +sqmail +square +squid +squirrel +squirrelmail +sr +sr1 +sr2 +sra +src +sri +srm +srntp +srs +srt +srv +srv0 +srv01 +srv02 +srv03 +srv04 +srv1 +srv10 +srv11 +srv12 +srv13 +srv14 +srv16 +srv2 +srv20 +srv21 +srv3 +srv4 +srv5 +srv6 +srv7 +srv8 +srv9 +srvc02 +srvc03 +srvc07 +srvc08 +srvc12 +srvc13 +srvc17 +srvc18 +srvc22 +srvc23 +srvc27 +srvc28 +srvc32 +srvc33 +srvc37 +srvc38 +srvc42 +srvc43 +srvc47 +srvc48 +srvc52 +srvc53 +srvc57 +srvc58 +srvc62 +srvc63 +srvc67 +srvc68 +srvc72 +srvc73 +srvc77 +srvc78 +srvc82 +srvc83 +srvc87 +srvc88 +srvc92 +srvc93 +srvc97 +srvc98 +ss +ss1 +ss2 +ssa +ssb +ssc +ssd +ssg +ssh +ssh1 +ssh2 +ssi +ssl +ssl-vpn +ssl0 +ssl01 +ssl1 +ssl2 +ssl3 +ssl4 +ssltest +sslvpn +ssm +ssmtp +sso +sso2 +ssotest +ssp +sss +sst +st +st01 +st1 +st2 +st3 +st4 +sta +stable +stadtplan +staff +staff2 +staffmail +stage +stage1 +stage2 +stages +staging +staging-chat +staging-chat-service +staging-commondata +staging1 +staging2 +staging40 +stagingphp +stalker +stamp +stan +standard +standards +standby +star +starfish +stargate +stark +stars +start +startup +starwars +stary +stash +stat +stat1 +stat2 +statefarm +statefarminsurance +statefarminsuranceco +statefarminsurancecos +static +static-m +static0 +static01 +static1 +static2 +static3 +static4 +static5 +static6 +static7 +static8 +statics +station +statistica +statistiche +statistics +statistik +stats +stats1 +stats2 +status +statystyki +stavanger-gw4 +stavropol +stb +stblogs +stc +std +stealth +steel +stefan +stella +stem +step +steve +stf +stg +sti +stingray +stiri +stk +stl +stlouis +stm +stmp +stock +stockholm +stocks +stolav-gw2 +stolav-gw4 +stone +stop +stor +storage +storage1 +storage2 +store +store1 +store2 +storefront +storelocator +stores +stories +storm +story +stp +str +strapon +strasbourg +strateji +strawberrysoup +stream +stream01 +stream02 +stream1 +stream2 +stream3 +stream4 +stream5 +streamer +streaming +streaming1 +streaming2 +streams +street +strong +stronghold +strongmail +strony +stroy +struts +sts +sts1 +stu +stud +student +student1 +student2 +studentaffairs +studenti +studentmail +students +studentweb +studio +studios +studmail +studsovet +study +studyabroad +stuff +stumail +stun +stuttgart +stwww +style +styles +styx +su +sub +subaru +subdomain +submit +submitimages +suboffer +subs +subscribe +subscription +subscriptions +subversion +success +suche +sud +sugar +sugarcrm +suggest +suggestqueries +suivi +summer +summerschool +summit +sun +sun0 +sun01 +sun02 +sun1 +sun2 +sunny +sunoco +sunrise +sunset +sunshine +sup +super +superadmin +superman +supernova +supervalu +supervision +suport +suporte +supplier +suppliers +supply +support +support1 +support2 +support3 +supporto +surat +surf +surgut +survey +survey2 +surveys +surveytool +sus +suse +sushi +suspended +sustainability +suzhou +suzuki +sv +sv01 +sv02 +sv1 +sv10 +sv2 +sv3 +sv4 +sv5 +sv6 +sv7 +sv8 +svc +sven +svi +sviluppo +svm +svn +svn01 +svn1 +svn2 +svpn +svr +svr1 +svs +svt +sw +sw0 +sw01 +sw1 +sw2 +sw3 +swa +swan +sweden +sweet +swf +swift +swiss +switch +switch1 +switch2 +switch3 +switch4 +switch5 +switch6 +switch7 +switch8 +switzerland +swj +sword +sws +swt +sx +sxy +sy +sybase +sydney +syktyvkar +syllabus +symantec +symccloud +sympa +symphony +symposium +sync +sync1 +sync2 +syndication +synergy +sys +sysadmin +sysadmins +sysaid +sysback +sysco +syslog +syslogs +sysmon +system +systems +syzx +sz +szb +szczecin +szkolenia +sztz +szukaj +t +t-dtap +t1 +t2 +t3 +t4 +t5 +t6 +t7 +t8 +ta +tab +tableau +tablet +tac +tacoma +tag +tags +taipei +taiwan +takvim +talent +tales +talk +talkgadget +talos +tam +tambov +tampa +tan +tandem +tango +tank +tao +taobao +tap +tara +tardis +target +tarif +tas +task +tasks +tatooine +tattoo +tau +taurus +tax +taxi +taz +tb +tbms +tc +tcc +tccgalleries +tcdn +tci +tcl +tcm +tcs +td +tdb +tdc +tde +tdm +tds +te +tea +teach +teacher +teachers +team +teamcity +teams +teamspeak +teamwork +tec +tech +tech2 +techhelp +techmang +techno +technology +techsupport +tecnologia +ted +teddy +tede +teen +teens +tehran +teknik +teknobyen-gw2 +tel +tele +tele2 +telechargement +telecom +telefon +telefonia +telephone +telephony +teleservices +telewerk +telework +telnet +tema +temp +temp1 +temp2 +temp3 +temp4 +template +templates +temple +tempo +tempus +tender +tenders +tenlcdn +tennessee +tennis +teo +tera +term +terminal +terminalserver +terminator +terminus +terms +termserv +terra +terry +tes +tesla +test +test-admin +test-www +test01 +test02 +test03 +test1 +test10 +test11 +test12 +test123 +test13 +test14 +test15 +test16 +test17 +test18 +test19 +test2 +test20 +test22 +test23 +test2k +test3 +test4 +test5 +test6 +test7 +test8 +test9 +test99 +testadmin +testajax +testapi +testapp +testasp +testaspnet +testbed +testblog +testcf +testcms +testcrm +testdb +testdev +testdns +testdrive +teste +tester +testes +testforum +testing +testing2 +testjsp +testlab +testlink +testlinux +testm +testmail +testnet +testnet-seed +testo +testphp +testportal +tests +testserver +testshop +testsite +testsql +teststore +testtest +testvb +testweb +testwiki +testwp +testwww +testxp +testy +teszt +tethys +tex +texas +text +textile +tf +tf1 +tf2 +tfs +tftp +tg +tgp +th +th-core +thai +thailand +thanhtra +thankyou +thc +the +theater +theatre +thebe +theme +themes +themis +theta +think +thomas +thor +thor-mx960 +thot +thumb +thumbnails +thumbs +thumbs2 +thunder +ti +tiaa-cref +tianjin +tibet +tic +tice +tick +ticker +ticket +ticketing +tickets +tienda +tiger +tile +tim +time +time-warner +time1 +time2 +timeclock +timehost +timeline +timer +timeserver +timesheet +timesheets +timetable +timewarner +timewarnercable +tims +tina +tiny +tip +tips +tis +titan +titania +titanic +titanium +titus +tivoli +tj +tjj +tk +tl +tlbr +tlc +tlkp +tls +tlt +tm +tmail +tmc +tmg +tmn +tmp +tms +tn +tnt +to +toad +tock +today +todo +togo +token +tokyo +toledo +tolyatti +tom +tomas +tomato +tomcat +tomer +tomsk +tongji +tony +tool +toolbar +toolbars +toolbox +toolkit +tools +tools2 +toons +top +top100 +topaz +topic +topics +toplayer +tops +topup +tor +torg +tornado +toro +toronto +torrent +torrents +torun +tot +total +totem +toto +touch +toulouse +tour +tourism +tourisme +tours +tower +town +toy +toyota +toys +tp +tpl +tpm +tps +tr +tra +trac +trace +track +tracker +tracking +trackit +trade +trading +traf +traffic +trailer +trailers +train +training +training1 +training2 +traktor +tranny +trans +transfer +transfers +transfert +transit +translate +translation +translator +transparencia +transport +trash +travail +travaux +travel +traveler +travelers +travelers-cos +travelerscos +traveller +trc +trd-gw +trd-gw1 +trd-gw7 +tree +treinamento +trend +trends +trial +trinidad +trinity +trio +trip +tristan +triton +trixbox +trk +tromso-gw2 +tromso-gw4 +tron +trs +true-ip-ga8-rtr +true-ip-ork-rtr +trunk +trust +trustees +try +ts +ts01 +ts1 +ts10 +ts2 +ts20 +ts3 +ts4 +ts5 +tsa +tsc +tsg +tsgw +tsi +tsl +tsm +tsp +tss +tst +tsunami +tsweb +tt +ttc +tts +ttt +tu +tuan +tuanwei +tube +tucows +tucson +tuku +tula +tulsa +tumb +tumblr +tumen +tuna +tunet +tuning +tunnel +tur +turbine +turbo +turing +turismo +turizm +turkey +turtle +turystyka +tutor +tutorial +tutorials +tutos +tux +tuyensinh +tv +tv1 +tv2 +tver +tvguide +tw +twc +tweets +twiki +twitter +two +tx +txt +ty +tyb +typo +typo3 +tyr +tyson +tysonfoods +tyumen +tyxy +tz +tzb +u +u1 +u2 +u3 +ua +uae +uag +uat +uat-online +ub +ubs +ubuntu +uc +ucc +ucenter +ucs +ud +uddi +ue +uf +ufa +ufo +ug +ugc +uh +ui +uis +uk +uk1 +uk2 +ukr +ukraine +ukwebmail +ul +ulan-ude +ultima +ultra +ulyanovsk +ulysse +um +uma +umail +umc +umfrage +umfragen +umi +ums +umu +un +underwear +unesco +uni +unicorn +unifi +uniform +uninett-gw +union +united +unitedhealth +unitedhealth-group +unitedhealthgroup +unitedkingdom +unitedparcelservice +unitedstates +unitedtechnologies +unity +univ +universal +universe +university +unix +unixware +uno +unreal +unsub +unsubscribe +uo +up +up1 +upc +upd +update +update1 +update2 +updates +upgrade +upl +upload +upload1 +upload2 +uploader +uploads +ups +ups2 +upsilon +uptime +ur +ura +ural +uran +urano +uranus +urban +urchin +url +urp +uruguay +us +us1 +us2 +us3 +us4 +usa +usability +usage +usb +usc +usedcars +usenet +user +users +userweb +uslugi +usosweb +uss +usuarios +ut +utah +util +utilities +utility +utils +utm +utv +uu +uucp +uv +ux +uy +uz +uzem +v +v1 +v12 +v2 +v2-ag +v3 +v4 +v5 +v6 +v7 +v9 +va +vacances +vacancy +vader +vadim +vae +val +valencia +valero +valero-energy +valeroenergy +valhalla +validation +validations +validclick +value +van +vancouver +vanilla +vantive +var +varnish +vas +vasco +vault +vb +vc +vc1 +vc2 +vc3 +vcenter +vcma +vconf +vcp +vcs +vcse +vd +vdc +vdi +vdo +vdp +vdr +vds +ve +vector +veeam +vega +vegas +vela +velocity +vend +vendor +vendors +venezuela +venice +ventas +ventura +venture +venus +vera +veranstaltungen +verdi +verify +verizon +verizonbusiness +verizoncommunications +verizonfios +verizonresidential +verizonwireless +vermont +verona +version +version2 +vertigo +verwaltung +vesta +vesti +vestibular +vestnik +vet +veterans +vf +vg +vh +vh1 +vh2 +vhost +vhost1 +vhs +vi +via +viajes +vibe +vic +vicon +victor +victoria +victory +vid +vid1 +vid2 +vid3 +video +video-m +video-stats +video1 +video2 +video3 +video4 +videochat +videoconf +videoconferencia +videos +videos2 +videoteca +vidthumb +vidyo +viejo +vienna +vietnam +view +viewer +viking +village +vince +vincent +vino +vintage +violet +vip +vip1 +vip2 +vip3 +viper +virgin +virginia +virgo +virt +virt-gw +virt1 +virt2 +virtual +virtual1 +virtual2 +virus +viruswall +vis +visa +visio +vision +visit +vista +vita +vital +viva +vivaldi +vjud +vk +vl +vlab +vlad +vladimir +vladivostok +vle +vlon +vm +vm0 +vm01 +vm02 +vm1 +vm11 +vm2 +vm3 +vm4 +vm5 +vm6 +vma +vmail +vms +vmscanus +vmserver +vmware +vmware2 +vn +vnc +vnet +vns +vo +vod +vod1 +vod101 +vod102 +vod2 +vod5 +vodafone +vodka +voeux +voice +voicemail +voices +void +voip +voip1 +voip2 +voip3 +vol +volga +volgograd +volkswagen +volleyball +vologda +volta +voltage-pp-0000 +voltaire +volunteer +volunteers +volvo +voodoo +voronezh +vortex +vote +voting +voucher +voyage +voyager +voyages +voyeur +vp +vpdn +vpgk +vpn +vpn0 +vpn01 +vpn02 +vpn1 +vpn2 +vpn3 +vpn4 +vpn5 +vpnc +vpngw +vpnserver +vpnssl +vpproxy +vprofile +vps +vps01 +vps02 +vps1 +vps10 +vps11 +vps12 +vps13 +vps14 +vps15 +vps2 +vps3 +vps4 +vps5 +vps6 +vps7 +vps8 +vps9 +vpscp +vr +vrn +vs +vs01 +vs1 +vs2 +vs3 +vsa +vsecure +vserver +vsp +vss +vt +vtc +vtest +vtiger +vu +vulcan +vv +vvv +vvvww +vw +vybory +vz +w +w01 +w1 +w10 +w11 +w2 +w3 +w3cache +w4 +w5 +w6 +w7 +w8 +w9 +wa +wac +wads +wagner +wahlen +waimai +wais +wakayama +wal-mart +walgreen +walgreens +walker +wall +wallace +wallet +wallpaper +wallpapers +walmart +walmartdoctors +walmartonline +walmartstores +walt-disney +waltdisney +wam +wan +wap +wap1 +wap2 +wapmail +war +warehouse +warez +warp +warranty +warren +warrior +warszawa +was +washington +watch +watchdog +watcher +water +watson +wave +wb +wbsnhes +wbt +wc +wc3 +wcf +wcg +wcm +wcp +wcs +wd +wdc-mare +wds +we +weather +web +web-dev +web0 +web01 +web02 +web03 +web04 +web05 +web06 +web07 +web08 +web09 +web1 +web10 +web101 +web11 +web12 +web13 +web14 +web15 +web16 +web17 +web18 +web19 +web2 +web20 +web21 +web22 +web23 +web24 +web25 +web26 +web27 +web3 +web4 +web5 +web6 +web7 +web8 +web9 +webaccess +webadmin +webadvisor +webalizer +webapi +webapp +webapps +webapps2 +webauth +webboard +webcache +webcal +webcalendar +webcall +webcam +webcam1 +webcam2 +webcams +webcast +webcdn +webchat +webclasseur +webclient +webcon +webconf +webconference +webcp +webct +webdata +webdav +webdb +webdemo +webdesign +webdev +webdev2 +webdisk +webdoc +webdocs +webedit +webeoc +weber +webex +webexpand +webext +webfarm +webfiles +webform +webftp +webgame +webgis +webhard +webhelp +webhost +webhost1 +webhosting +webinar +webinars +weblib +weblog +weblogic +weblogs +webm +webmai +webmail +webmail-old +webmail01 +webmail02 +webmail1 +webmail2 +webmail3 +webmail4 +webmail5 +webmailtest +webmaker +webmakerl +webmaster +webmasters +webmeeting +webmin +weboffice +webopac +webpac +webplus +webportal +webprint +webprod +webproxy +webring +webrnail +webs +websearch +webserv +webserver +webserver1 +webserver2 +webservice +webservices +webshare +webshop +website +websitecpanel +websites +webspace +websphere +websrv +websrv1 +websrvr +webstat +webstats +webster +webstore +websurvey +websvn +websvr +webteam +webtest +webtools +webtrends +webtv +webvpn +webwork +wechat +wed +wedding +weddings +weekend +weekly +wei +weibo +weihnachten +weixin +welcome +welfare +wellington +wellness +wellpoint +wells-fargo +wellsfargo +wendy +wenku +wenwen +werbung +wes +west +westchester +westvirginia +wetter +wf +wg +wh +wha +whale +whatsup +whiskey +white +whitelabel +whm +whmcs +who +whois +wholesale +whs +wi +wichita +widget +widgets +wien +wifi +wii +wiki +wiki2 +wikidev +wikis +wikitest +wild +wildcat +wililiam +willow +wilson +win +win01 +win02 +win1 +win10 +win11 +win12 +win13 +win14 +win15 +win16 +win17 +win18 +win19 +win2 +win20 +win2000 +win2003 +win22 +win24 +win2k +win2k3 +win3 +win32 +win4 +win5 +win6 +win7 +win8 +win9 +wind +windows +windows01 +windows02 +windows1 +windows2 +windows2000 +windows2003 +windowsupdate +windowsxp +wine +wingate +winnt +winproxy +wins +winserve +winter +wintest +winupdate +winxp +wip +wire +wireless +wis +wisconsin +wisdom +wise +wish +wizard +wj +wk +wl +wlan +wlan-switch +wlc +wls +wm +wm2 +wmail +wms +wmt +wmv +wns1 +wns2 +wo +wolf +woman +wombat +women +wonder +wood +woody +woofti +word +wordpress +work +work2 +workflow +working +workplace +works +workshop +workspace +world +worldcup +wotan +wow +wowza +wp +wp1 +wp2 +wpad +wpb +wpdemo +wptest +wr +write +writers +writing +wroclaw +ws +ws1 +ws10 +ws11 +ws12 +ws13 +ws2 +ws3 +ws4 +ws5 +ws6 +ws7 +ws8 +ws9 +wsb +wsc +wsj +wsn +wsp +wss +wstest +wsus +wt +wtest +wts +wu +wuhan +wusage +wuv +wuvx +wuwv +wuwx +wuwy +wuxi +wuxv +wuxw +wuxy +wuyw +wv +wvux +wvw +wvxy +ww +ww0 +ww01 +ww02 +ww03 +ww1 +ww2 +ww3 +ww4 +ww5 +ww6 +ww7 +ww8 +ww9 +wwa +wwb +wwp +wws +wwu +wwuv +wwux +wwuy +wwv +wwvu +wwvv +wwvvv +wwvwv +wwvx +wwvy +www +www- +www-01 +www-02 +www-1 +www-2 +www-3 +www-4 +www-5 +www-a +www-admin +www-b +www-c +www-cache +www-dev +www-devel +www-int +www-live-direct +www-live-redirect +www-new +www-old +www-org +www-origin +www-prod +www-staging +www-test +www-uat +www0 +www01 +www02 +www03 +www04 +www05 +www06 +www07 +www1 +www10 +www11 +www12 +www13 +www14 +www15 +www16 +www17 +www18 +www19 +www2 +www20 +www21 +www22 +www23 +www24 +www25 +www26 +www27 +www28 +www29 +www2s +www3 +www30 +www31 +www32 +www33 +www34 +www35 +www36 +www37 +www4 +www40 +www41 +www42 +www43 +www5 +www520 +www5b +www5d +www5f +www6 +www66 +www7 +www78 +www7a +www7b +www8 +www9 +www99 +www_ +wwwa +wwwalt +wwwb +wwwc +wwwcache +wwwchat +wwwd +wwwdev +wwwe +wwwf +wwwfilter +wwwftp +wwwg +wwwh +wwwi +wwwl +wwwm +wwwmail +wwwnew +wwwold +wwws +wwwstg +wwwt +wwwtest +wwwv +wwww +wwwww +wwwx +wwx +wwxu +wwxv +wwxy +wwy +wwyu +wwyv +wwyx +wx +wxtest +wxy +wy +wydawnictwo +wyoming +wyx +wz +x +x-ray +x1 +x2 +x3 +x4 +x5 +xa +xanthus +xavier +xb +xbox +xc +xcb +xchange +xcp +xd +xe +xen +xen1 +xen2 +xena +xenapp +xenon +xeon +xerox +xew +xf +xfer +xfz +xg +xgb +xgc +xh +xhtml +xhtrnl +xi +xia +xian +xiao +xiaoban +xiaobao +xiaoyou +xin +xing +xinjiang +xinli +xiu +xj +xk +xkb +xl +xljk +xlogan +xlzx +xm +xmail +xmas +xml +xml2 +xmlfeed +xmlrpc +xmpp +xms +xn +xp +xpam +xq +xray +xs +xsc +xserve +xsh +xszz +xt +xtxp +xuebao +xx +xxb +xxgk +xxx +xxzx +xy +xyh +xyy +xyz +xz +y +ya +yaho +yahoo +yakutsk +yamato +yanbak133 +yandex +yang +yankee +yar +yaroslavl +yb +yc +ycbf1 +ycbf2 +ycbf3 +ycbf8 +yd +ydb +ydyo +ye +yellow +yellowpages +yeni +yes +yh +yj +yjs +yjsc +yjsh +yjsy +yjszs +ykt +yl +ylc +ym +yn +yoda +yoga +yokohama +york +you +youjizz +young +your +youraccount +yourmail +youth +youtrack +youtube +yoyaku +yoyo +yp +ys +ysu1-catalyst4506e-0 +yt +yu +yuan +yukon +yulanyou +yule +yum +yun +yunfu +yuyue +yw +yx +yxy +yy +yz +z +z-log +z1 +z2 +z3 +z3950 +za +zabbix +zabbix2 +zags +zakaz +zakon +zakupki +zap +zaphod +zazcloud1 +zazcloud2 +zazcloud3 +zb +zbx +zc +zcc +zcgl +zcs +zd +zdrowie +zebra +zelda +zen +zend +zenith +zenoss +zenwsimport +zephir +zephyr +zera +zero +zeta +zeus +zf +zg +zh +zh-cn +zhao +zhaopin +zhaosheng +zhidao +zhu +zhuanti +zim +zimbra +zinc +zion +zip +zixun +zj +zjj +zk +zlog +zm +zmail +zn +zombie +zone +zoo +zoom +zoomumba +zope +zp +zpanel +zpush +zr +zs +zsb +zsjy +zt +zt2 +zulu +zurich +zw +zwj +zx +zy +zyz +zz +zzb +zzz diff --git a/wordlists/mssql-betterdefaultpasslist.txt b/wordlists/mssql-betterdefaultpasslist.txt new file mode 100644 index 00000000..3081e368 --- /dev/null +++ b/wordlists/mssql-betterdefaultpasslist.txt @@ -0,0 +1,66 @@ +sa:sa +sa:admin +sa:superadmin +sa:password +sa:default +admin:admin +sa:RPSsql12345 +sa:$ei$micMicro +ARIS9:N'*ARIS!1dm9n#' +ADONI:BPMS +gts:opengts +sa:PracticeUser1 +sa:42Emerson42Eme +sa:sqlserver +sa:Cardio.Perfect +sa:vantage12! +admin:netxms +ADMIN:AIMS +FB:AIMS +sa:$easyWinArt4 +sa:DBA!sa@EMSDB123 +sa:V4in$ight +sa:Pass@123 +admin:trinity +LENEL:MULTIMEDIA +sa:SilkCentral12!34 +stream:stream-1 +sa:cic +cic:cic +sa:cic!23456789 +cic:cic!23456789 +sa:Administrator1 +sa:M3d!aP0rtal +sa:splendidcrm2005 +admin:gnos +sa:Dr8gedog +sa:dr8gedog +sa:Password123 +sa:DBA!sa@EMSDB123 +sa:SECAdmin1 +sa:skf_admin1 +sa:SecurityMaster08 +secure:SecurityMaster08 +sa: +wasadmin:wasadmin +maxadmin:maxadmin +mxintadm:mxintadm +maxreg:maxreg +sa:capassword +I2b2metadata:i2b2metadata +I2b2demodata:i2b2demodata +I2b2workdata:i2b2workdata +I2b2metadata2:i2b2metadata2 +I2b2demodata2:i2b2demodata2 +I2b2workdata2:i2b2workdata2 +I2b2hive:i2b2hive +mcUser:medocheck123 +aadbo:pwddbo +wwdbo:pwddbo +aaAdmin:pwAdmin +wwAdmin:wwAdmin +aaPower:pwPower +wwPower:wwPower +aaUser:pwUser +wwUser:wwUser +sa:#SAPassword! diff --git a/wordlists/mysql-betterdefaultpasslist.txt b/wordlists/mysql-betterdefaultpasslist.txt new file mode 100644 index 00000000..193e36fe --- /dev/null +++ b/wordlists/mysql-betterdefaultpasslist.txt @@ -0,0 +1,23 @@ +root:mysql +root:root +root:chippc +admin:admin +root: +root:nagiosxi +root:usbw +cloudera:cloudera +root:cloudera +root:moves +moves:moves +root:testpw +root:p@ck3tf3nc3 +mcUser:medocheck123 +root:mktt +root:123 +dbuser:123 +asteriskuser:amp109 +asteriskuser:eLaStIx.asteriskuser.2oo7 +root:raspberry +root:openauditrootuserpassword +root:vagrant +root:123qweASD# diff --git a/wordlists/oracle-betterdefaultpasslist.txt b/wordlists/oracle-betterdefaultpasslist.txt new file mode 100644 index 00000000..224ea025 --- /dev/null +++ b/wordlists/oracle-betterdefaultpasslist.txt @@ -0,0 +1,716 @@ +ARISBP95:ARISBP +SYSTEM:oracle +system:Admin123@pdborcl +system:Admin123@orcl +system:Admin123 +i2db:eurekify +system:eurekify +system:password +system:netiq123 +admin02:xt4a9z +P2012DEV:p2012dev +QCONFIG:Precision +PRECISIONSPUSER:psl +PRECISIONSPMGR:psl +PRECISIONGLUSER:Precision +PRECISIONGLMGR:Precision +TRAX:trax +I2b2metadata:i2b2metadata +I2b2demodata:i2b2demodata +I2b2workdata:i2b2workdata +I2b2metadata2:i2b2metadata2 +I2b2demodata2:i2b2demodata2 +I2b2workdata2:i2b2workdata2 +I2b2hive:i2b2hive +sys:password +GardenUser:GardenUser +System:System +GardenAdmin:GardenAdmin +SuperUser:System +Sys:oracle +System:manager +SYS:werfen +SYSTEM:werfen +DBSNMP:werfen +SYSMAN:werfen +ql:ql +PPSNEPL:pharmacy +FDB_DIF:FDB_DIF123 +ORD_SERVER:ODS +WKADMIN:WKADMIN +WKUSER:WKUSER +INTERNAL:ORACLE +Administrator:Administrator +orcladmin:welcome +#INTERNAL:ORACLE +#INTERNAL:SYS_STNT +ABM:ABM +ADAMS:WOOD +ADLDEMO:ADLDEMO +ADMIN:JETSPEED +ADMIN:WELCOME +admin02:xt4a9z +ADMINISTRATOR:ADMIN +Administrator:Administrator +ADMINISTRATOR:ADMINISTRATOR +AHL:AHL +AHM:AHM +AK:AK +ALHRO:XXX +ALHRW:XXX +ALR:ALR +AMS:AMS +AMV:AMV +ANDY:SWORDFISH +ANONYMOUS: +ANONYMOUS:Anonymous +ANONYMOUS:ANONYMOUS +AP:AP +APPLMGR:APPLMGR +APPLSYS:APPLSYS +APPLSYS:APPS +APPLSYS:FND +APPLSYSPUB:APPLSYSPUB +APPLSYSPUB:FNDPUB +APPLSYSPUB:PUB +APPLYSYSPUB: +APPLYSYSPUB:D2E3EF40EE87221E +APPLYSYSPUB:FNDPUB +APPLYSYSPUB:PUB +APPS:APPS +APPS_MRC:APPS +APPUSER:APPPASSWORD +AQ:AQ +AQDEMO:AQDEMO +AQJAVA:AQJAVA +AQUSER:AQUSER +AR:AR +ARISBP95:ARISBP +ASF:ASF +ASG:ASG +ASL:ASL +ASO:ASO +ASP:ASP +AST:AST +ATM:SAMPLEATM +AUDIOUSER:AUDIOUSER +AURORA$JIS$UTILITY$: +AURORA$JIS$UTILITY$:INVALID +AURORA$JIS$UTILITY$:INVALID_ENCRYPTED_PASSWORD +AURORA$ORB$UNAUTHENTICATED: +AURORA$ORB$UNAUTHENTICATED:INVALID +AURORA$ORB$UNAUTHENTICATED:INVALID_ENCRYPTED_PASSWORD +AX:AX +AZ:AZ +BC4J:BC4J +BEN:BEN +BIC:BIC +BIL:BIL +BIM:BIM +BIS:BIS +BIV:BIV +BIX:BIX +BLAKE:PAPER +BLEWIS:BLEWIS +BOM:BOM +BRIO_ADMIN:BRIO_ADMIN +BRUGERNAVN:ADGANGSKODE +BRUKERNAVN:PASSWORD +BSC:BSC +BUG_REPORTS:BUG_REPORTS +CALVIN:HOBBES +CATALOG:CATALOG +CCT:CCT +CDEMO82:CDEMO82 +CDEMO82:CDEMO83 +CDEMO82:UNKNOWN +CDEMOCOR:CDEMOCOR +CDEMORID:CDEMORID +CDEMOUCB:CDEMOUCB +CDOUGLAS:CDOUGLAS +CE:CE +CENTRA:CENTRA +CENTRAL:CENTRAL +CIDS:CIDS +CIS:CIS +CIS:ZWERG +CISINFO:CISINFO +CISINFO:ZWERG +CLARK:CLOTH +CLKANA:. +CLKANA: +CLKRT:. +CLKRT: +CN:CN +COMPANY:COMPANY +COMPIERE:COMPIERE +CQSCHEMAUSER:PASSWORD +CQUSERDBUSER:PASSWORD +CRP:CRP +CS:CS +CSC:CSC +CSD:CSD +CSE:CSE +CSF:CSF +CSI:CSI +CSL:CSL +CSMIG:CSMIG +CSP:CSP +CSR:CSR +CSS:CSS +CTXDEMO:CTXDEMO +CTXSYS:. +CTXSYS: +CTXSYS:CHANGE_ON_INSTALL +CTXSYS:CTXSYS +CTXSYS:UNKNOWN +CUA:CUA +CUE:CUE +CUF:CUF +CUG:CUG +CUI:CUI +CUN:CUN +CUP:CUP +CUS:CUS +CZ:CZ +DATA_SCHEMA:LASKJDF098KSDAF09 +DBI:MUMBLEFRATZ +DBSNMP:DBSNMP +DBSNMP:werfen +DBVISION:DBVISION +DCM:. +DCM: +DDIC:199220706 +DEMO:DEMO +DEMO8:DEMO8 +DEMO9:DEMO9 +DES:DES +DES2K:DES2K +DEV2000_DEMOS:DEV2000_DEMOS +DIANE:PASSWO1 +DIP:DIP +DISCOVERER_ADMIN:DISCOVERER_ADMIN +DISCOVERER5:. +DISCOVERER5: +DMSYS:DMSYS +DPF:DPFPASS +DSGATEWAY:. +DSGATEWAY: +DSGATEWAY:DSGATEWAY +DSSYS:DSSYS +DTSP:DTSP +EAA:EAA +EAM:EAM +EARLYWATCH:SUPPORT +EAST:EAST +EC:EC +ECX:ECX +EJB:EJB +EJSADMIN:EJSADMIN +EJSADMIN:EJSADMIN_PASSWORD +EMP:EMP +ENG:ENG +ENI:ENI +ESTOREUSER:ESTORE +EVENT:EVENT +EVM:EVM +EXAMPLE:EXAMPLE +EXFSYS:EXFSYS +EXTDEMO:EXTDEMO +EXTDEMO2:EXTDEMO2 +FA:FA +FDB_DIF:FDB_DIF123 +FEM:FEM +FII:FII +FINANCE:FINANCE +FINPROD:FINPROD +FLM:FLM +FND:FND +FOO:BAR +FPT:FPT +FRM:FRM +FROSTY:SNOWMAN +FTE:FTE +FV:FV +GardenAdmin:GardenAdmin +GardenUser:GardenUser +GL:GL +GMA:GMA +GMD:GMD +GME:GME +GMF:GMF +GMI:GMI +GML:GML +GMP:GMP +GMS:GMS +GPFD:GPFD +GPLD:GPLD +GR:GR +HADES:HADES +HCPARK:HCPARK +HLW:HLW +HR: +HR:33EBE1C63D5B7FEF +HR:CHANGE_ON_INSTALL +HR:HR +HR:UNKNOWN +HRI:HRI +HVST:HVST +HXC:HXC +HXT:HXT +I2b2demodata:i2b2demodata +I2b2demodata2:i2b2demodata2 +I2b2hive:i2b2hive +I2b2metadata:i2b2metadata +I2b2metadata2:i2b2metadata2 +I2b2workdata:i2b2workdata +I2b2workdata2:i2b2workdata2 +i2db:eurekify +IBA:IBA +IBE:IBE +IBP:IBP +IBU:IBU +IBY:IBY +ICDBOWN:ICDBOWN +ICX:ICX +IDEMO_USER:IDEMO_USER +IEB:IEB +IEC:IEC +IEM:IEM +IEO:IEO +IES:IES +IEU:IEU +IEX:IEX +IFSSYS:IFSSYS +IGC:IGC +IGF:IGF +IGI:IGI +IGS:IGS +IGW:IGW +IMAGEUSER:IMAGEUSER +IMC:IMC +IMEDIA:IMEDIA +IMT:IMT +INTERNAL:ORACLE +INTERNAL:SYS_STNT +INV:INV +IPA:IPA +IPD:IPD +IPLANET:IPLANET +ISC:ISC +ITG:ITG +JA:JA +JAKE:PASSWO4 +JE:JE +JG:JG +JILL:PASSWO2 +JL::JL: +JL:JL +JMUSER:JMUSER +JOHN:JOHN +JONES:STEEL +JTF:JTF +JTM:JTM +JTS:JTS +JWARD:AIROPLANE +KWALKER:KWALKER +L2LDEMO:L2LDEMO +LBACSYS:LBACSYS +LIBRARIAN:SHELVES +MANPROD:MANPROD +MARK:PASSWO3 +MASCARM:MANAGER +MASTER:PASSWORD +MDDATA:MDDATA +MDDEMO:MDDEMO +MDDEMO_CLERK:CLERK +MDDEMO_CLERK:MGR +MDDEMO_MGR:MDDEMO_MGR +MDDEMO_MGR:MGR +MDSYS:MDSYS +ME:ME +MFG:MFG +MGR:MGR +MGWUSER:MGWUSER +MIGRATE:MIGRATE +MILLER:MILLER +MMO2:MMO2 +MMO2:MMO3 +MMO2:UNKNOWN +MODTEST:YES +MOREAU:MOREAU +MRP:MRP +MSC:MSC +MSD:MSD +MSO:MSO +MSR:MSR +MTS_USER:MTS_PASSWORD +MTSSYS:MTSSYS +MWA:MWA +MXAGENT:MXAGENT +NAMES:NAMES +NEOTIX_SYS:NEOTIX_SYS +NNEUL:NNEULPASS +NOM_UTILISATEUR:MOT_DE_PASSE +NOME_UTILIZADOR:SENHA +NOMEUTENTE:PASSWORD +NUME_UTILIZATOR:PAROL +OAIHUB902:. +OAIHUB902: +OAS_PUBLIC: +OAS_PUBLIC:9300C0977D7DC75E +OAS_PUBLIC:OAS_PUBLIC +OCITEST:OCITEST +OCM_DB_ADMIN:. +OCM_DB_ADMIN: +OCM_DB_ADMIN:OCM_DB_ADMIN +ODM:ODM +ODM_MTR:MTRPW +ODS:ODS +ODS_SERVER:ODS_SERVER +ODSCOMMON:ODSCOMMON +OE:CHANGE_ON_INSTALL +OE:OE +OE:UNKNOWN +OEM_REPOSITORY: +OEM_REPOSITORY:1FF89109F7A16FEF +OEMADM:OEMADM +OEMREP:OEMREP +OKB:OKB +OKC:OKC +OKE:OKE +OKI:OKI +OKO:OKO +OKR:OKR +OKS:OKS +OKX:OKX +OLAPDBA:OLAPDBA +OLAPSVR:INSTANCE +OLAPSVR:OLAPSVR +OLAPSYS:MANAGER +OLAPSYS:OLAPSYS +OMWB_EMULATION:ORACLE +ONT:ONT +OO:OO +OPENSPIRIT:OPENSPIRIT +OPI:OPI +ORACACHE:. +ORACACHE: +ORACACHE:ORACACHE +ORACLE:ORACLE +ORADBA:ORADBAPASS +ORANGE: +ORANGE:3D9B7E34A4F7D4E9 +ORAPROBE:ORAPROBE +ORAREGSYS:ORAREGSYS +ORASSO:ORASSO +ORASSO_DS:ORASSO_DS +ORASSO_PA:ORASSO_PA +ORASSO_PS:ORASSO_PS +ORASSO_PUBLIC:ORASSO_PUBLIC +ORASTAT:ORASTAT +orcladmin:welcome +ORCLADMIN:WELCOME +ORD_SERVER:ODS +ORDCOMMON:ORDCOMMON +ORDPLUGINS:ORDPLUGINS +ORDSYS:ORDSYS +OSE$HTTP$ADMIN:Invalid +OSE$HTTP$ADMIN:INVALID +OSE$HTTP$ADMIN:Invalid:password +OSM:OSM +OSP22:OSP22 +OSSAQ_HOST:. +OSSAQ_HOST: +OSSAQ_PUB:. +OSSAQ_PUB: +OSSAQ_SUB:. +OSSAQ_SUB: +OTA:OTA +OUTLN:OUTLN +OWA:OWA +OWA_PUBLIC:OWA_PUBLIC +OWF_MGR:. +OWF_MGR: +OWF_MGR:OWF_MGR +OWNER:OWNER +OZF:OZF +OZP:OZP +OZS:OZS +P2012DEV:p2012dev +PA:PA +PANAMA:PANAMA +PATROL:PATROL +PAUL:PAUL +PERFSTAT:PERFSTAT +PERSTAT:PERSTAT +PJM:PJM +PLANNING:PLANNING +PLEX:PLEX +PLSQL:SUPERSECRET +PM:CHANGE_ON_INSTALL +PM:PM +PM:UNKNOWN +PMI:PMI +PN:PN +PO:PO +PO7:PO7 +PO8:PO8 +POA:POA +POM:POM +PORTAL:. +PORTAL: +PORTAL_APP:. +PORTAL_APP: +PORTAL_DEMO:. +PORTAL_DEMO: +PORTAL_DEMO:PORTAL_DEMO +PORTAL_PUBLIC:. +PORTAL_PUBLIC: +PORTAL_SSO_PS:PORTAL_SSO_PS +PORTAL30:PORTAL30 +PORTAL30:PORTAL31 +PORTAL30_ADMIN:PORTAL30_ADMIN +PORTAL30_DEMO:PORTAL30_DEMO +PORTAL30_PS:PORTAL30_PS +PORTAL30_PUBLIC:PORTAL30_PUBLIC +PORTAL30_SSO:PORTAL30_SSO +PORTAL30_SSO_ADMIN:PORTAL30_SSO_ADMIN +PORTAL30_SSO_PS:PORTAL30_SSO_PS +PORTAL30_SSO_PUBLIC:PORTAL30_SSO_PUBLIC +POS:POS +POWERCARTUSER:POWERCARTUSER +PPSNEPL:pharmacy +PRECISIONGLMGR:Precision +PRECISIONGLUSER:Precision +PRECISIONSPMGR:psl +PRECISIONSPUSER:psl +PRIMARY:PRIMARY +PSA:PSA +PSB:PSB +PSP:PSP +PUBSUB:PUBSUB +PUBSUB1:PUBSUB1 +PV:PV +QA:QA +QCONFIG:Precision +QDBA:QDBA +ql:ql +QP:QP +QS:CHANGE_ON_INSTALL +QS:QS +QS:UNKNOWN +QS_ADM:CHANGE_ON_INSTALL +QS_ADM:QS_ADM +QS_ADM:UNKNOWN +QS_CB:CHANGE_ON_INSTALL +QS_CB:QS_CB +QS_CB:UNKNOWN +QS_CBADM:CHANGE_ON_INSTALL +QS_CBADM:QS_CBADM +QS_CBADM:UNKNOWN +QS_CS:CHANGE_ON_INSTALL +QS_CS:QS_CS +QS_CS:UNKNOWN +QS_ES:CHANGE_ON_INSTALL +QS_ES:QS_ES +QS_ES:UNKNOWN +QS_OS:CHANGE_ON_INSTALL +QS_OS:QS_OS +QS_OS:UNKNOWN +QS_WS:CHANGE_ON_INSTALL +QS_WS:QS_WS +QS_WS:UNKNOWN +RE:RE +REP_MANAGER:DEMO +REP_OWNER:DEMO +REP_OWNER:REP_OWNER +REP_USER:DEMO +REPADMIN:REPADMIN +REPORTS:REPORTS +REPORTS_USER:OEM_TEMP +RG:RG +RHX:RHX +RLA:RLA +RLM:RLM +RMAIL:RMAIL +RMAN:RMAN +RRS:RRS +SAMPLE:SAMPLE +SAP:06071992 +SAP:6071992 +SAP:SAPR3 +SAPR3:SAP +SCOTT:TIGER +SCOTT:TIGGER +SDOS_ICSAP:SDOS_ICSAP +SECDEMO:SECDEMO +SERVICECONSUMER1:SERVICECONSUMER1 +SH:CHANGE_ON_INSTALL +SH:SH +SH:UNKNOWN +SI_INFORMTN_SCHEMA:SI_INFORMTN_SCHEMA +SITEMINDER:SITEMINDER +SLIDE:SLIDEPW +SPIERSON:SPIERSON +SSP:SSP +STARTER:STARTER +STRAT_USER:STRAT_PASSWD +SuperUser:System +SWPRO:SWPRO +SWUSER:SWUSER +SYMPA:SYMPA +SYS:0RACL3 +SYS:0RACL38 +SYS:0RACL38I +SYS:0RACL39 +SYS:0RACL39I +SYS:0RACLE +SYS:0RACLE8 +SYS:0RACLE8I +SYS:0RACLE9 +SYS:0RACLE9I +SYS:CHANGE_ON_INSTALL +SYS:D_SYSPW +SYS:MANAG3R +SYS:MANAGER +SYS:ORACL3 +Sys:oracle +SYS:ORACLE +SYS:ORACLE8 +SYS:ORACLE8I +SYS:ORACLE9 +SYS:ORACLE9I +sys:password +SYS:SYS +SYS:SYSPASS +SYS:werfen +SYSADM:SYSADM +SYSADMIN:. +SYSADMIN: +SYSADMIN:SYSADMIN +SYSMAN:OEM_TEMP +SYSMAN:SYSMAN +SYSMAN:werfen +SYSTEM:0RACL3 +SYSTEM:0RACL38 +SYSTEM:0RACL38I +SYSTEM:0RACL39 +SYSTEM:0RACL39I +SYSTEM:0RACLE +SYSTEM:0RACLE8 +SYSTEM:0RACLE8I +SYSTEM:0RACLE9 +SYSTEM:0RACLE9I +system:Admin123 +system:Admin123@orcl +system:Admin123@pdborcl +SYSTEM:CHANGE_ON_INSTALL +SYSTEM:D_SYSPW +SYSTEM:D_SYSTPW +system:eurekify +SYSTEM:MANAG3R +System:manager +SYSTEM:MANAGER +system:netiq123 +SYSTEM:ORACL3 +SYSTEM:oracle +SYSTEM:ORACLE +SYSTEM:ORACLE8 +SYSTEM:ORACLE8I +SYSTEM:ORACLE9 +SYSTEM:ORACLE9I +system:password +System:System +SYSTEM:SYSTEM +SYSTEM:SYSTEMPASS +SYSTEM:werfen +TAHITI:TAHITI +TALBOT:MT6CH5 +TDOS_ICSAP:TDOS_ICSAP +TEC:TECTEC +TEST:PASSWD +TEST:TEST +TEST_USER:TEST_USER +TESTPILOT:TESTPILOT +THINSAMPLE:THINSAMPLEPW +TIBCO:TIBCO +TIP37:TIP37 +TRACESVR:TRACE +TRAVEL:TRAVEL +TRAX:trax +TSDEV:TSDEV +TSUSER:TSUSER +TURBINE:TURBINE +UDDISYS:. +UDDISYS: +ULTIMATE:ULTIMATE +UM_ADMIN:UM_ADMIN +UM_CLIENT:UM_CLIENT +USER:USER +USER_NAME:PASSWORD +USER0:USER0 +USER1:USER1 +USER2:USER2 +USER3:USER3 +USER4:USER4 +USER5:USER5 +USER6:USER6 +USER7:USER7 +USER8:USER8 +USER9:USER9 +USUARIO:CLAVE +UTILITY:UTILITY +UTLBSTATU:UTLESTAT +VEA:VEA +VEH:VEH +VERTEX_LOGIN:VERTEX_LOGIN +VIDEOUSER:VIDEOUSER +VIF_DEVELOPER:VIF_DEV_PWD +VIRUSER:VIRUSER +VPD_ADMIN:AKF7D98S2 +VRR1:UNKNOWN +VRR1:VRR1 +VRR1:VRR2 +WEBCAL01:WEBCAL01 +WEBDB:WEBDB +WEBREAD:WEBREAD +WEBSYS:MANAGER +WEBUSER:YOUR_PASS +WEST:WEST +WFADMIN:WFADMIN +WH:WH +WIP:WIP +WIRELESS:. +WIRELESS: +WK_PROXY: +WK_PROXY:3F9FBD883D787341 +WK_SYS: +WK_SYS:79DF7A1BD138CF11 +WK_TEST:WK_TEST +WKADMIN:WKADMIN +WKPROXY:CHANGE_ON_INSTALL +WKPROXY:UNKNOWN +WKPROXY:WKPROXY +WKSYS:CHANGE_ON_INSTALL +WKSYS:WKSYS +WKUSER:WKUSER +WMS:WMS +WMSYS:WMSYS +WOB:WOB +WPS:WPS +WSH:WSH +WSM:WSM +WWW:WWW +WWWUSER:WWWUSER +XADEMO:XADEMO +XDB:CHANGE_ON_INSTALL +XDP:XDP +XLA:XLA +XNC:XNC +XNI:XNI +XNM:XNM +XNP:XNP +XNS:XNS +XPRT:XPRT +XTR:XTR diff --git a/wordlists/postgres-betterdefaultpasslist.txt b/wordlists/postgres-betterdefaultpasslist.txt new file mode 100644 index 00000000..753c7ff8 --- /dev/null +++ b/wordlists/postgres-betterdefaultpasslist.txt @@ -0,0 +1,8 @@ +dcmadmin:passw0rd +postgres:amber +postgres:postgres +postgres:password +postgres:admin +admin:admin +admin:password +postgres:123 diff --git a/wordlists/root-userpass.txt b/wordlists/root-userpass.txt new file mode 100755 index 00000000..f51609f9 --- /dev/null +++ b/wordlists/root-userpass.txt @@ -0,0 +1,51 @@ +root: +root:!root +root:Cisco +root:NeXT +root:QNX +root:admin +root:attack +root:ax400 +root:bagabu +root:blablabla +root:blender +root:brightmail +root:calvin +root:changeme +root:changethis +root:default +root:fibranne +root:honey +root:jstwo +root:kn1TG7psLu +root:letacla +root:mpegvideo +root:nsi +root:par0t +root:pass +root:password +root:pixmet2003 +root:resumix +root:root +root:rootme +root:rootpass +root:t00lk1t +root:tini +root:toor +root:trendimsa1.0 +root:tslinux +root:uClinux +root:vertex25 +root:owaspbwa +root:permit +root:ascend +root:ROOT500 +root:cms500 +root:fivranne +root:davox +root:letmein +root:powerapp +root:dbps +root:ibm +root:monitor +root:turnkey diff --git a/wordlists/routers-userpass.txt b/wordlists/routers-userpass.txt new file mode 100644 index 00000000..884f5ef2 --- /dev/null +++ b/wordlists/routers-userpass.txt @@ -0,0 +1,415 @@ +root: +admin: +guest: +root:root +root:password +root:1234 +root:12345 +root:123456 +root:3ep5w2u +root:admin +root:Admin +root:admin_1 +root:alpine +root:ascend +root:attack +root:blender +root:calvin +root:changeme +root:Cisco +root:cms500 +root:davox +root:default +root:fivranne +root:ggdaseuaimhrke +root:iDirect +root:letacla +root:Mau'dib +root:pass +root:permit +root:ROOT500 +root:tini +root:tslinux +root:wyse +ro:ro +router:router +rwa:rwa +rw:rw +ubnt:ubnt +guest:guest +guest:User +admin:0 +admin:0000 +admin:1111 +admin:123 +admin:1234 +admin:123456 +admin:1234admin +admin:2222 +admin:22222 +admin2:changeme +admin:3477 +admin:3ascotel +admin:9999 +admin:access +admin:admin +admin:Admin +Admin:admin +admin:admin123 +admin:adminttd +admin:adslolitec +admin:adslroot +admin:adtran +admin:AitbISP4eCiG +admin:articon +admin:asante +admin:ascend +admin:Ascend +admin:asd +admin:atc123 +admin:atlantis +admin:backdoor +admin:barricade +admin:barricadei +admin:bintec +admin:BRIDGE +admin:cableroot +admin:changeme +admin:cisco +admin:_Cisco +admin:comcomcom +admin:conexant +admin:default +admin:diamond +admin:enter +admin:epicrouter +admin:extendnet +admin:giraff +admin:hagpolm1 +admin:hello +admin:help +admin:hp.com +admin:Intel +admin:ironport +admin:isee +acc:acc +adfexc:adfexc +adm: +admin:kont2004 +admin:letmein +admin:leviton +admin:linga +admin:michelangelo +admin:microbusiness +admin:MiniAP +admin:motorola +admin:mu +admin:my_DEMARC +admin:netadmin +admin:NetCache +admin:NetICs +admin:noway +admin:OCS +admin:operator +admin:P@55w0rd! +admin:password +admin:p-assword +admin:PASSWORD +admin:passwort +admin:pento +admin:pfsense +admin:private +admin:Protector +admin:public +admin:pwp +admin:radius +admin:rmnetlm +admin:root +admin:secure +admin:setup +admin:sitecom +admin:smallbusiness +admin:smcadmin +admin:SMDR +admin:speedxess +admin:SUPER +admin:superuser +admin:switch +admin:Symbol +admin:synnet +admin:sysAdmin +admin:system +admin:TANDBERG +admin:visual +admin:w2402 +admin:xad$|#12 +admin:xad$l#12 +admin:zoomadsl +system:change_on_install +system/manager:sys/change_on_install +system:password +system:sys +adminttd:adminttd +adminuser:OCS +adminview:OCS +adminstat:OCS +adminstrator:changeme +Administrator:3ware +Administrator:admin +administrator:administrator +ADMINISTRATOR:ADMINISTRATOR +administrator:changeme +Administrator:changeme +Administrator:ganteng +Administrator:letmein +Administrator:password +Administrator:pilou +Administrator:smcadmin +ADMN:admn +ami: +anonymous:any@ +anonymous:Exabyte +Any:12345 +apc:apc +at4400:at4400 +bbsd-client:changeme2 +bbsd-client:NULL +bciim:bciimpw +bcim:bcimpw +bcms:bcmspw +bcnas:bcnaspw +bcnas:pcnaspw +blue:bluepw +browse:browsepw +browse:looker +cablecom:router +cablemodem:robotics +cac_admin:cacadmin +cas:cascade +ccrusr:ccrusr +cellit:cellit +cgadmin:cgadmin +cisco: +cisco:cisco +Cisco:Cisco +citel:citel +client:client +cmaker:cmaker +comcast:1234 +corecess:corecess +craft: +craft:craft +craft:craftpw +craft:crftpw +CSG:SESAME +cusadmin:highspeed +cust:custpw +customer: +customer:none +dadmin:dadmin01 +davox:davox +debug:d.e.b.u.g +debug:synnet +deskalt:password +deskman:changeme +desknorm:password +deskres:password +device:device +dhs3mt:dhs3mt +dhs3pms:dhs3pms +diag:danger +diag:switch +disttech:4tas +D-Link:D-Link +draytek:1234 +DTA:TJM +e250:e250changeme +e500:e500changeme +echo:echo +echo:User +enable: +eng:engineer +enquiry:enquirypw +field:support +GEN1:gen1 +GEN2:gen2 +GlobalAdmin:GlobalAdmin +halt:tlah +helpdesk:OCS +hsa:hsadb +hscroot:abc123 +HTTP:HTTP +hydrasna: +iclock:timely +images:images +inads:inads +inads:indspw +init:initpw +installer:installer +install:llatsni +install:secret +intel:intel +intermec:intermec +intermec:intermec1QTPS +IntraStack:Asante +IntraSwitch:Asante +jagadmin: +JDE:JDE +kermit:kermit +l2:l2 +l3:l3 +locate:locatepw +login:0 +login:1111 +login:8429 +login:access +login:admin +login:password +lp:lp +LUCENT01:UI-PSWD-01 +LUCENT02:UI-PSWD-02 +m1122:m1122 +mac: +maint:maint +maint:maintpw +maint:ntacdmax +maint:rwmaint +manage:!manage +manager:admin +manager:change_on_install +manager:friend +Manager:friend +manager:manager +Manager:Manager +manager:sys +manuf:xxyyzz +MDaemon:MServer +mediator:mediator +MICRO:RSX +mlusr:mlusr +monitor:monitor +mtch:mtch +mtcl: +mtcl:mtcl +naadmin:naadmin +NAU:NAU +netangr:attack +netman: +netman:netman +netopia:netopia +netrangr:attack +netscreen:netscreen +NETWORK:NETWORK +NICONEX:NICONEX +nms:nmspw +nokai:nokai +nokia:nokia +none:0 +none:admin +operator: +operator:1234 +operator:$chwarzepumpe +operator:operator +op:op +op:operator +patrol:patrol +PBX:PBX +PFCUser:240653C9467E45 +piranha:piranha +piranha:q +pmd: +poll:tech +Polycom:SpIp +PRODDTA:PRODDTA +PSEAdmin:$secure$ +public: +public:public +radware:radware +rapport:r@p8p0r+ +rcust:rcustpw +readonly:lucenttech2 +readwrite:lucenttech1 +recovery:recovery +replicator:replicator +RMUser1:password +sa: +scmadmin:scmchangeme +scout:scout +secret:secret +secure:secure +security:security +service:smile +setup:changeme +setup:changeme! +setup:setup +smc:smcadmin +spcl:0 +storwatch:specialist +stratacom:stratauser +super:5777364 +superadmin:secret +superman:21241036 +superman:talent +super:super +super.super: +super.super:master +super:surt +superuser: +superuser:123456 +superuser:admin +supervisor:PlsChgMe! +supervisor:PlsChgMe1 +supervisor:supervisor +support:h179350 +support:support +support:supportpw +su:super +Sweex:Mysweex +sysadm:Admin +sysadm:anicust +sysadmin:PASS +sysadmin:password +sysadmin:sysadmin +sysadm:PASS +sysadm:sysadm +SYSADM:sysadm +sys:uplink +target:password +teacher:password +tech: +tech:ANYCOM +tech:field +tech:ILMI +tech:tech +telco:telco +telecom:telecom +tellabs:tellabs#1 +temp1:password +test:test +tiara:tiaranet +tiger:tiger123 +topicalt:password +topicnorm:password +topicres:password +user: +USERID:PASSW0RD +user:pass +user:password +User:Password +user:public +user:tivonpw +user:user +vcr:NetVCR +VNC:winterm +volition:volition +vt100:public +VTech:VTech +webadmin:1234 +webadmin:webadmin +websecadm:changeme +wlse:wlsedb +wradmin:trancell +write:private +xd:xd +xxx:cascade +ZXDSL:ZXDSL \ No newline at end of file diff --git a/wordlists/snmp-default.txt b/wordlists/snmp-default.txt new file mode 100644 index 00000000..0575e194 --- /dev/null +++ b/wordlists/snmp-default.txt @@ -0,0 +1,196 @@ + +0 +0392a0 +1234 +2read +3com +3Com +3COM +4changes +access +adm +admin +Admin +administrator +agent +agent_steal +all +all private +all public +anycom +ANYCOM +apc +bintec +blue +boss +c +C0de +cable-d +cable_docsispublic@es0 +cacti +canon_admin +cascade +cc +changeme +cisco +CISCO +cmaker +comcomcom +community +core +CR52401 +crest +debug +default +demo +dilbert +enable +entry +field +field-service +freekevin +friend +fubar +guest +hello +hideit +host +hp_admin +ibm +IBM +ilmi +ILMI +intel +Intel +intermec +Intermec +internal +internet +ios +isdn +l2 +l3 +lan +liteon +login +logon +lucenttech +lucenttech1 +lucenttech2 +manager +master +microsoft +mngr +mngt +monitor +mrtg +nagios +net +netman +network +nobody +NoGaH$@! +none +notsopublic +nt +ntopia +openview +operator +OrigEquipMfr +ourCommStr +pass +passcode +password +PASSWORD +pr1v4t3 +pr1vat3 +private + private +Private +PRIVATE +private@es0 +Private@es0 +private@es1 +Private@es1 +proxy +publ1c +public + public +Public +PUBLIC +public@es0 +public@es1 +public/RO +read +read-only +readwrite +read-write +red +regional + +rmon +rmon_admin +ro +root +router +rw +rwa +sanfran +san-fran +scotty +secret +Secret +SECRET +Secret C0de +security +Security +SECURITY +seri +server +snmp +SNMP +snmpd +snmptrap +snmp-Trap +SNMP_trap +SNMPv1/v2c +SNMPv2c +solaris +solarwinds +sun +SUN +superuser +supervisor +support +switch +Switch +SWITCH +sysadm +sysop +Sysop +system +System +SYSTEM +tech +telnet +TENmanUFactOryPOWER +test +TEST +test2 +tiv0li +tivoli +topsecret +traffic +trap +user +vterm1 +watch +watchit +windows +windowsnt +workstation +world +write +writeit +xyzzy +yellow diff --git a/wordlists/ssh-betterdefaultpasslist.txt b/wordlists/ssh-betterdefaultpasslist.txt new file mode 100644 index 00000000..9db5bc9e --- /dev/null +++ b/wordlists/ssh-betterdefaultpasslist.txt @@ -0,0 +1,131 @@ +root:calvin +root:root +root:toor +administrator:password +NetLinx:password +administrator:Amx1234! +amx:password +amx:Amx1234! +admin:1988 +admin:admin +Administrator:Vision2 +cisco:cisco +c-comatic:xrtwk318 +root:qwasyx21 +admin:insecure +pi:raspberry +user:user +root:default +root:leostream +leo:leo +localadmin:localadmin +fwupgrade:fwupgrade +root:rootpasswd +admin:password +root:timeserver +admin:motorola +cloudera:cloudera +root:p@ck3tf3nc3 +apc:apc +device:apc +eurek:eurek +netscreen:netscreen +admin:avocent +root:linux +sconsole:12345 +root:5up +cirros:cubswin:) +root:uClinux +root:alpine +root:dottie +root:arcsight +root:unitrends1 +vagrant:vagrant +root:vagrant +m202:m202 +demo:fai +root:fai +root:ceadmin +maint:password +root:palosanto +root:ubuntu1404 +root:cubox-i +debian:debian +root:debian +root:xoa +root:sipwise +debian:temppwd +root:sixaola +debian:sixaola +myshake:shakeme +stackato:stackato +root:screencast +root:stxadmin +root:nosoup4u +root:indigo +root:video +default:video +default: +ftp:video +nexthink:123456 +ubnt:ubnt +root:ubnt +sansforensics:forensics +elk_user:forensics +osboxes:osboxes.org +root:osboxes.org +sans:training +user:password +misp:Password1234 +hxeadm:HXEHana1 +acitoolkit:acitoolkit +osbash:osbash +enisa:enisa +geosolutions:Geos +pyimagesearch:deeplearning +root:NM1$88 +remnux:malware +hunter:hunter +plexuser:rasplex +root:openelec +root:rasplex +root:plex +root:openmediavault +root:ys123456 +root:libreelec +openhabian:openhabian +admin:ManagementConsole2015 +public:publicpass +admin:hipchat +nao:nao +support:symantec +root:max2play +admin:pfsense +root:root01 +root:nas4free +USERID:PASSW0RD +Administrator:p@ssw0rd +root:freenas +root:cxlinux +admin:symbol +admin:Symbol +admin:superuser +admin:admin123 +root:D13HH[ +root:blackarch +root:dasdec1 +root:7ujMko0admin +root:7ujMko0vizxv +root:Zte521 +root:zlxx. +root:compass +hacker:compass +samurai:samurai +ubuntu:ubuntu +root:openvpnas +misp:Password1234 +root:wazuh +student:password123 +root:roottoor +centos:reverse +root:reverse diff --git a/wordlists/ssh-password.txt b/wordlists/ssh-password.txt new file mode 100644 index 00000000..ac17824e --- /dev/null +++ b/wordlists/ssh-password.txt @@ -0,0 +1,2 @@ +password +p@55w0rd diff --git a/wordlists/ssh-user.txt b/wordlists/ssh-user.txt new file mode 100644 index 00000000..4df966c9 --- /dev/null +++ b/wordlists/ssh-user.txt @@ -0,0 +1,5 @@ +root +sysop +admin +admnistrator +superuser diff --git a/wordlists/telnet-betterdefaultpasslist.txt b/wordlists/telnet-betterdefaultpasslist.txt new file mode 100644 index 00000000..6ffa3a3c --- /dev/null +++ b/wordlists/telnet-betterdefaultpasslist.txt @@ -0,0 +1,146 @@ +root:calvin +administrator:password +NetLinx:password +administrator:Amx1234! +amx:password +amx:Amx1234! +admin:1988 +admin:admin +Administrator:Vision2 +cisco:cisco +root:fidel123 +user:user +root:default +localadmin:localadmin +Root:wago +Admin:wago +User:user +Guest:guest +root:rootpasswd +admin:password +adtec:none +root:timeserver +root:password +Admin:Su +root:admin +admin:motorola +Admin:5001 +User:1001 +GE:GE +Admin:Pass +device:apc +apc:apc +root:anni2013 +root:xc3511 +root:dreambox +root:vizxv +admin:1111111 +admin:smcadmin +admin:4321 +888888:888888 +666666:666666 +ubnt:ubnt +admin:22222 +adminttd:adminttd +root:!root +admin:epicrouter +tech:tech +manager:manager +smc:smcadmin +netscreen:netscreen +netopia:netopia +root:888888 +root:xmhdipc +root:juantech +root:123456 +root:54321 +support:support +root:root +root:12345 +root:pass +admin:admin1234 +root:1111 +admin:1111 +root:666666 +root:1234 +root:klv123 +Administrator:admin +service:service +guest:guest +guest:12345 +admin1:password +administrator:1234 +root:klv1234 +root:Zte521 +root:hi3518 +root:jvbzd +root:anko +root:zlxx. +root:7ujMko0vizxv +root:7ujMko0admin +root:system +root:ikwb +root:dreambox +root:user +root:realtek +root:00000000 +admin:1234 +admin:12345 +default:OxhlwSG8 +admin:tlJwpbo6 +default:S2fGqNFs +admin:meinsm +supervisor:supervisor +admin:123456 +root:zlxx +dm:telnet +webguest:1 +Liebert:Liebert +User:User +admin:avocent +root:linux +admin:system +user:public +admin:private +guest:guest +admin:admin +root:root +qbf77101:hexakisoctahedron +ftpuser:password +USER:USER +Basisk:Basisk +sconsole:12345 +root:5up +root:cat1029 +MayGion:maygion.com +admin:cat1029 +admin:ZmqVfoSIP +default:antslq +admin:microbusiness +admin:jvc +root:GM8182 +root:uClinux +Alphanetworks:wrgg19_c_dlwbr_dir300 +Alphanetworks:wrgn49_dlob_dir600b +Alphanetworks:wrgn23_dlwbr_dir600b +Alphanetworks:wrgn22_dlwbr_dir615 +Alphanetworks:wrgnd08_dlob_dir815 +Alphanetworks:wrgg15_di524 +Alphanetworks:wrgn39_dlob.hans_dir645 +Alphanetworks:wapnd03cm_dkbs_dap2555 +Alphanetworks:wapnd04cm_dkbs_dap3525 +Alphanetworks:wapnd15_dlob_dap1522b +Alphanetworks:wrgac01_dlob.hans_dir865 +Alphanetworks:wrgn23_dlwbr_dir300b +Alphanetworks:wrgn28_dlob_dir412 +Alphanetworks:wrgn39_dlob.hans_dir645_V1 +root:oelinux123 +mg3500:merlin +root:cxlinux +root:1001chin +root:china123 +admin:symbol +admin:Symbol +admin:superuser +admin:admin123 +root:20080826 diff --git a/wordlists/tomcat-betterdefaultpasslist.txt b/wordlists/tomcat-betterdefaultpasslist.txt new file mode 100644 index 00000000..49f181e9 --- /dev/null +++ b/wordlists/tomcat-betterdefaultpasslist.txt @@ -0,0 +1,79 @@ +admin: +admin:admanager +admin:admin +admin:admin +ADMIN:ADMIN +admin:adrole1 +admin:adroot +admin:ads3cret +admin:adtomcat +admin:advagrant +admin:password +admin:password1 +admin:Password1 +admin:tomcat +admin:vagrant +both:admanager +both:admin +both:adrole1 +both:adroot +both:ads3cret +both:adtomcat +both:advagrant +both:tomcat +cxsdk:kdsxc +j2deployer:j2deployer +manager:admanager +manager:admin +manager:adrole1 +manager:adroot +manager:ads3cret +manager:adtomcat +manager:advagrant +manager:manager +ovwebusr:OvW*busr1 +QCC:QLogic66 +role1:admanager +role1:admin +role1:adrole1 +role1:adroot +role1:ads3cret +role1:adtomcat +role1:advagrant +role1:role1 +role1:tomcat +role:changethis +root:admanager +root:admin +root:adrole1 +root:adroot +root:ads3cret +root:adtomcat +root:advagrant +root:changethis +root:owaspbwa +root:password +root:password1 +root:Password1 +root:r00t +root:root +root:toor +tomcat: +tomcat:admanager +tomcat:admin +tomcat:admin +tomcat:adrole1 +tomcat:adroot +tomcat:ads3cret +tomcat:adtomcat +tomcat:advagrant +tomcat:changethis +tomcat:password +tomcat:password1 +tomcat:s3cret +tomcat:s3cret +tomcat:tomcat +xampp:xampp +server_admin:owaspbwa +admin:owaspbwa +demo:demo diff --git a/wordlists/vnc-betterdefaultpasslist.txt b/wordlists/vnc-betterdefaultpasslist.txt new file mode 100644 index 00000000..275acd5e --- /dev/null +++ b/wordlists/vnc-betterdefaultpasslist.txt @@ -0,0 +1,41 @@ +123456 +FELDTECH_VNC +vnc_pcc +elux +Passwort +visam +password +Amx1234! +1988 +admin +Vision2 +ADMIN +TOUCHLON +EltakoFVS +Wyse#123 +muster +passwd11 +qwasyx21 +Administrator +ripnas +eyevis +fidel123 +Admin#1 +default +sigmatek +hapero +1234 +pass +raspberry +user +solarfocus +AVStumpfl +m9ff.QW +maryland-dstar +pass1 +pass2 +instrument +beijer +vnc +yesco +protech diff --git a/wordlists/windows-betterdefaultpasslist.txt b/wordlists/windows-betterdefaultpasslist.txt new file mode 100644 index 00000000..74dcae5e --- /dev/null +++ b/wordlists/windows-betterdefaultpasslist.txt @@ -0,0 +1,27 @@ +Administrator:FELDTECH +secure:SecurityMaster08 +admin:trinity +administrator:Wyse#123 +user:Wyse#123 +admin:admin +Administrator:Administrator +sonos:sonos +demo:m9ff.QW +wasadmin:wasadmin +maxadmin:maxadmin +mxintadm:mxintadm +maxreg:maxreg +root: +admin:admin +admin:12345 +admin:1234 +admin:123456 +instrument:instrument +admin: +nmt:1234 +admin:password +IEUser:Passw0rd! +openhabian:openhabian +vagrant:vagrant +Administrator:vagrant +john:Password123!