├── .coveragerc
├── .github
└── workflows
│ ├── pypi-publish.yml
│ └── python-package.yml
├── .gitignore
├── .readthedocs.yml
├── LICENSE
├── README.md
├── docs
├── coverage.svg
├── mkdocs.yml
├── pages
│ ├── base
│ │ ├── commands.md
│ │ ├── consoles.md
│ │ ├── index.md
│ │ └── models.md
│ ├── classes
│ │ ├── command.md
│ │ ├── console.md
│ │ ├── datastore.md
│ │ ├── entity.md
│ │ ├── index.md
│ │ └── module.md
│ ├── css
│ │ └── extra.css
│ ├── design.md
│ ├── examples
│ │ └── dronesploit.md
│ ├── img
│ │ ├── class-hierarchy.png
│ │ ├── classes.png
│ │ ├── command-key-completion.png
│ │ ├── command-validation.png
│ │ ├── command-value-completion.png
│ │ ├── console-prompt.png
│ │ ├── dronesploit.png
│ │ ├── icon.png
│ │ ├── logo.png
│ │ ├── my-sploit-start.png
│ │ ├── packages.png
│ │ └── under-construction.png
│ ├── index.md
│ └── quickstart.md
└── requirements.txt
├── pyproject.toml
├── pytest.ini
├── requirements.txt
├── src
└── sploitkit
│ ├── VERSION.txt
│ ├── __info__.py
│ ├── __init__.py
│ ├── __main__.py
│ ├── base
│ ├── __init__.py
│ ├── commands
│ │ ├── __init__.py
│ │ ├── general.py
│ │ ├── module.py
│ │ ├── project.py
│ │ ├── recording.py
│ │ ├── root.py
│ │ ├── session.py
│ │ └── utils.py
│ ├── config.conf
│ └── models
│ │ ├── __init__.py
│ │ ├── notes.py
│ │ ├── organization.py
│ │ ├── systems.py
│ │ └── users.py
│ └── core
│ ├── __init__.py
│ ├── application.py
│ ├── command.py
│ ├── components
│ ├── __init__.py
│ ├── completer.py
│ ├── config.py
│ ├── defaults.py
│ ├── files.py
│ ├── jobs.py
│ ├── layout.py
│ ├── logger.py
│ ├── recorder.py
│ ├── sessions.py
│ ├── store.py
│ └── validator.py
│ ├── console.py
│ ├── entity.py
│ ├── model.py
│ └── module.py
├── tests
├── __utils__.py
├── test_base.py
├── test_components.py
├── test_console.py
├── test_entity.py
├── test_model.py
├── test_module.py
└── test_scenarios.py
└── testsploit
├── README
├── commands
└── commands.py
├── main.py
├── modules
└── modules.py
├── requirements.txt
└── workspace
└── store.db
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | cover_pylib = false
3 | source = sploitkit
4 | omit =
5 | */site-packages/*
6 | tests/*
7 |
8 | [report]
9 | exclude_lines =
10 | pragma: no cover
11 | class IPAddressField
12 | class MACAddressField
13 | class Trigger
14 |
--------------------------------------------------------------------------------
/.github/workflows/pypi-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will deploy the Python package to PyPi.org
2 |
3 | name: deploy
4 |
5 | env:
6 | package: sploitkit
7 |
8 | on:
9 | push:
10 | branches:
11 | - main
12 | paths:
13 | - '**/VERSION.txt'
14 | workflow_run:
15 | workflows: ["build"]
16 | types: [completed]
17 |
18 | jobs:
19 | deploy:
20 | runs-on: ubuntu-latest
21 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
22 | steps:
23 | - uses: actions/checkout@v3
24 | with:
25 | fetch-depth: 0
26 | - name: Cleanup README
27 | run: |
28 | sed -ri 's/^(##*)\s*:.*:\s*/\1 /g' README.md
29 | awk '{if (match($0,"## Supporters")) exit; print}' README.md > README
30 | mv -f README README.md
31 | - run: python3 -m pip install --upgrade build && python3 -m build
32 | - name: Upload ${{ env.package }} to PyPI
33 | uses: pypa/gh-action-pypi-publish@release/v1
34 | with:
35 | password: ${{ secrets.PYPI_API_TOKEN }}
36 | verbose: true
37 | verify_metadata: false
38 |
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3 |
4 | name: build
5 |
6 | env:
7 | package: sploitkit
8 |
9 | on:
10 | push:
11 | branches: [ "main" ]
12 | pull_request:
13 | branches: [ "main" ]
14 |
15 | jobs:
16 | build:
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | os: [ubuntu-latest]
22 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python ${{ matrix.python-version }}
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 | - name: Install pandoc
30 | run: sudo apt-get install -y pandoc
31 | - name: Install ${{ env.package }}
32 | run: |
33 | python -m pip install --upgrade pip
34 | python -m pip install pytest pytest-cov pytest-pythonpath coverage
35 | pip install -r requirements.txt
36 | pip install .
37 | - name: Test ${{ env.package }} with pytest
38 | run: |
39 | pytest --cov=$package
40 | coverage:
41 | needs: build
42 | runs-on: ubuntu-latest
43 | env:
44 | cov_badge_path: docs/coverage.svg
45 | steps:
46 | - uses: actions/checkout@v3
47 | - name: Set up Python ${{ matrix.python-version }}
48 | uses: actions/setup-python@v4
49 | with:
50 | python-version: "3.10"
51 | - name: Install pandoc
52 | run: sudo apt-get install -y pandoc notification-daemon
53 | - name: Install ${{ env.package }}
54 | run: |
55 | python -m pip install --upgrade pip
56 | python -m pip install pytest pytest-cov pytest-pythonpath
57 | pip install -r requirements.txt
58 | pip install .
59 | - name: Make coverage badge for ${{ env.package }}
60 | run: |
61 | pip install genbadge[coverage]
62 | pytest --cov=$package --cov-report=xml
63 | genbadge coverage -i coverage.xml -o $cov_badge_path
64 | - name: Verify Changed files
65 | uses: tj-actions/verify-changed-files@v17
66 | id: changed_files
67 | with:
68 | files: ${{ env.cov_badge_path }}
69 | - name: Commit files
70 | if: steps.changed_files.outputs.files_changed == 'true'
71 | run: |
72 | git config --local user.email "github-actions[bot]@users.noreply.github.com"
73 | git config --local user.name "github-actions[bot]"
74 | git add $cov_badge_path
75 | git commit -m "Updated coverage.svg"
76 | - name: Push changes
77 | if: steps.changed_files.outputs.files_changed == 'true'
78 | uses: ad-m/github-push-action@master
79 | with:
80 | github_token: ${{ secrets.github_token }}
81 | branch: ${{ github.ref }}
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Temp files
2 | *~
3 | *.backup
4 | .DS_Store
5 |
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | env/
16 | build/
17 | .build/
18 | develop-eggs/
19 | dist/
20 | downloads/
21 | eggs/
22 | .eggs/
23 | lib64/
24 | parts/
25 | sdist/
26 | update.sh
27 | reinstall.sh
28 | version.py
29 |
30 | var/
31 | *.egg-info/
32 | .installed.cfg
33 | *.egg
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 | MANIFEST
41 |
42 | # Installer logs
43 | pip-log.txt
44 | pip-delete-this-directory.txt
45 |
46 | # Unit test / coverage reports
47 | htmlcov/
48 | .tox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *,cover
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Sphinx documentation
61 | docs/_build/
62 |
63 | # PyBuilder
64 | target/
65 |
66 | # Project artifacts
67 | .idea
68 | .vagrant
69 | .test
70 | .pytest_cache
71 | tmp
72 | TODO
73 | script.py
74 | tool.py
75 | testsploit/workspace/*
76 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: "ubuntu-22.04"
5 | tools:
6 | python: "3.11"
7 |
8 | mkdocs:
9 | configuration: docs/mkdocs.yml
10 |
11 | python:
12 | install:
13 | - requirements: docs/requirements.txt
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | SploitKit
3 | Make a Metasploit-like console.
4 |
5 | [](https://pypi.python.org/pypi/sploitkit/)
6 | [](https://python-sploitkit.readthedocs.io/en/latest/?badge=latest)
7 | [](https://github.com/dhondta/python-sploitkit/actions/workflows/python-package.yml)
8 | [](#)
9 | [](https://pypi.python.org/pypi/sploitkit/)
10 | [](https://snyk.io/test/github/dhondta/python-sploitkit?targetFile=requirements.txt)
11 | [](https://pypi.python.org/pypi/sploitkit/)
12 |
13 |
14 | This toolkit is aimed to easilly build framework consoles in a Metasploit-like style. It provides a comprehensive interface to define CLI commands, modules and models for its storage database.
15 |
16 | ```
17 | pip install sploitkit
18 | ```
19 |
20 | ## :sunglasses: Usage
21 |
22 | From this point, `main.py` has the following code:
23 |
24 | ```python
25 | #!/usr/bin/python3
26 | from sploitkit import FrameworkConsole
27 |
28 |
29 | class MySploitConsole(FrameworkConsole):
30 | #TODO: set your console attributes
31 | pass
32 |
33 |
34 | if __name__ == '__main__':
35 | MySploitConsole(
36 | "MySploit",
37 | #TODO: configure your console settings
38 | ).start()
39 | ```
40 |
41 | And you can run it from the terminal:
42 |
43 | 
44 |
45 | ## :ballot_box_with_check: Features
46 |
47 | Sploitkit provides a base set of entities (consoles, commands, modules, models).
48 |
49 | Multiple base console levels already exist (for detailed descriptions, see [the console section](../console/index.html)):
50 |
51 | - [`FrameworkConsole`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/console.py): the root console, started through `main.py`
52 | - [`ProjectConsole`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/project.py): the project console, for limiting the workspace to a single project, invoked through the `select [project]` command
53 | - [`ModuleConsole`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/module.py): the module console, started when a module is invoked through the `use [module]` command
54 |
55 | This framework provides more than 20 base commands, distributed in sets of functionalities (for detailed descriptions, see [the command section](../command/index.html)):
56 |
57 | - [*general*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/general.py): commands for every level (e.g. `help`, `show`, `set`)
58 | - [*module*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/module.py): base module-level commands (e.g. `use`, `run`, `show`)
59 | - [*project*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/project.py): base project-level commands (e.g. `select`, `load`, `archive`)
60 | - [*recording*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/recording.py): recording commands, for managing `.rc` files (`record`, `replay`)
61 | - [*root*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/root.py): base root-level commands (`help`)
62 | - [*utils*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/utils.py): utility commands (`shell`, `pydbg`, `memory`)
63 |
64 | It also holds some base models for its storage:
65 |
66 | - [*users*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/models/notes.py): for user-related data (`User`, `Email`, `Password`)
67 | - [*systems*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/models/systems.py): for system-related data (`Host`, `Port`, `Service`)
68 | - [*organization*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/models/organization.py): for organization-related data (`Organization`, `Unit`, `Employee`)
69 | - [*notes*](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/models/notes.py): for linking notes to users, hosts or organizations
70 |
71 | No module is provided with the framework as it is case-specific.
72 |
73 | ## :pencil2: Customization
74 |
75 | Sploitkit defines multiple types of entities for various purposes. The following entities can be subclassed:
76 |
77 | - [`Console`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/console.py): a new console for a new level of interaction (e.g. [`ProjectConsole`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/project.py)) ; the "`root`" level is owned by the [`FrameworkConsole`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/console.py), [`Console`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/console.py) shall be used to create new subconsoles, to be called by commands from the root console (see an example [here](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/module.py) for the module-level commands with [`ModuleConsole(Console)` and `Use(Command)`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/commands/module.py))
78 | - [`Command`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/command.py): a new command associated with any or defined consoles using the `level` attribute
79 | - [`Module`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/module.py): a new module associated to a console
80 | - [`Model`, `BaseModel`, `StoreExtension`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/core/model.py): respectively for new models, their association tables and store additional methods (e.g. [`User(Model)`, `Email(Model)`, `UserEmail(BaseModel)`, `UsersStorage(StoreExtension)`](https://github.com/dhondta/python-sploitkit/blob/main/sploitkit/base/models/users.py))
81 |
82 |
83 | ## :clap: Supporters
84 |
85 | [](https://github.com/dhondta/python-sploitkit/stargazers)
86 |
87 | [](https://github.com/dhondta/python-sploitkit/network/members)
88 |
89 |
90 |
--------------------------------------------------------------------------------
/docs/coverage.svg:
--------------------------------------------------------------------------------
1 | coverage: 53.73% coverage coverage 53.73% 53.73%
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_author: dhondta
2 | site_name: "SploitKit - Devkit for building Metasploit-like consoles"
3 | repo_url: https://github.com/dhondta/python-sploitkit
4 | copyright: Copyright © 2019-2023 Alexandre D'Hondt
5 | docs_dir: pages
6 | nav:
7 | - Introduction: index.md
8 | - 'Getting started': quickstart.md
9 | - Design: design.md
10 | - Classes:
11 | - classes/index.md
12 | - Entity: classes/entity.md
13 | - Console: classes/console.md
14 | - Command: classes/command.md
15 | - Module: classes/module.md
16 | - Datastore: classes/datastore.md
17 | - 'Base entities':
18 | - base/index.md
19 | - Consoles: base/consoles.md
20 | - Commands: base/commands.md
21 | - Models: base/models.md
22 | - 'Real-life examples':
23 | - 'DroneSploit': examples/dronesploit.md
24 | extra:
25 | generator: false
26 | social:
27 | - icon: fontawesome/solid/paper-plane
28 | link: mailto:alexandre.dhondt@gmail.com
29 | name: Contact Alex
30 | - icon: fontawesome/brands/github
31 | link: https://github.com/dhondta
32 | name: Alex on GitHub
33 | - icon: fontawesome/brands/linkedin
34 | link: https://www.linkedin.com/in/alexandre-d-2ab2aa14/
35 | name: Alex on LinkedIn
36 | - icon: fontawesome/brands/twitter
37 | link: https://twitter.com/alex_dhondt
38 | name: Alex on Twitter
39 | extra_css:
40 | - css/extra.css
41 | theme:
42 | name: material
43 | palette:
44 | - scheme: default
45 | toggle:
46 | icon: material/brightness-7
47 | name: Switch to dark mode
48 | - scheme: slate
49 | toggle:
50 | icon: material/brightness-4
51 | name: Switch to light mode
52 | features:
53 | - navigation.indexes
54 | - navigation.top
55 | - toc.integrate
56 | logo: img/logo.png
57 | favicon: img/icon.png
58 | use_directory_urls: false
59 | markdown_extensions:
60 | - admonition
61 | - codehilite:
62 | linenums: true
63 | - pymdownx.details
64 | - pymdownx.superfences
65 | - toc:
66 | permalink: true
67 |
--------------------------------------------------------------------------------
/docs/pages/base/commands.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/base/commands.md
--------------------------------------------------------------------------------
/docs/pages/base/consoles.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/base/consoles.md
--------------------------------------------------------------------------------
/docs/pages/base/index.md:
--------------------------------------------------------------------------------
1 | This section presents the entities provided with Sploitkit.
2 |
--------------------------------------------------------------------------------
/docs/pages/base/models.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/base/models.md
--------------------------------------------------------------------------------
/docs/pages/classes/command.md:
--------------------------------------------------------------------------------
1 | *Commands* are associated to *consoles* through the `level` attribute and are evaluated in the REPL of a console using their `run(...)` method. Completion and validation can be tuned using appropriate methods like explained hereafter.
2 |
3 | Note that they are two possible formats : `COMMAND VALUE` or `COMMAND KEY VALUE`.
4 |
5 | ## Styling
6 |
7 | It is possible to define the style of *commands*, that is, how the class name is rendered when called in a *console*. Currently, four styles are supported :
8 |
9 | **Name** | **Command class name** | **Rendered command name**
10 | --- | :---: | :---:
11 | *Lowercase* | `MyCommand` | `mycommand`
12 | *Powershell* | `MyCommand` | `My-Command`
13 | *Slugified* (default) | `MyCommand` | `my-command`
14 | *Uppercase* | `MyCommand` | `MYCOMMAND`
15 |
16 | Command styling can be set using the `set_style(...)` class method.
17 |
18 | ??? example "**Example**: Setting commands style"
19 |
20 | :::python
21 | from sploitkit import Command
22 |
23 | Command.set_style("powershell")
24 |
25 |
26 |
27 | ## Definition
28 |
29 | A *command* always subclasses the `Command` generic entity class and can be dissected as follows :
30 |
31 | 1. **Docstring** : This will be used for command's description in help messages (callable through the `help()` method). Note that this docstring is parsed like for any entity (as it is a feature of the `Entity` class), meaning that metadata fields will be parsed and stored in a `_metadata` class attribute.
32 | 2. **Class attributes** : They tune the applicability and nature of the command.
33 | 3. **Instance methods** : They define the logic of the command, i.e. `run()`.
34 |
35 | Here is the list of tunable class attributes :
36 |
37 | **Attribute** | **Type** | **Default** | **Description**
38 | --- | :---: | :---: | ---
39 | `aliases` | `list`(`str`) | `[]` | the list of aliases for the command
40 | `alias_only` | `bool` | `False` | whether only the aliases defined in the related list should be considered or also the converted command class name
41 | `applies_to` | `list`(`str`) | `[]` | a list of *modules* this command applies to
42 | `except_levels` | `list`(`str`) | `[]` | a list of non-applicable levels
43 | `keys` | `list`(`str`) or `dict` | `[]` | a list of possible keys or a dictionary of keys and associated values (this implies the second format with key-value)
44 | `level` | `str` | "`general`" | command's level ; "`general`" means that it applies to all console levels
45 | `single_arg` | `bool` | `False` | handle everything after the command as a single argument
46 | `values` | `list`(`str`) |
47 |
48 |
49 | ??? example "**Example**: Making a command in Powershell style with an alias and applicable to the *module* level"
50 |
51 | :::python
52 | from sploitkit import Command
53 |
54 | Command.set_style("powershell")
55 |
56 | class GetSomething(Command):
57 | """ Get something """
58 | aliases = ["gs"]
59 | level = "module"
60 | [...]
61 |
62 |
63 |
64 | ## Completion
65 |
66 | Completion is defined according to the command format and the related method signature is adapted accordingly. So, if a command is value-only, it *can* own a `complete_values()` method with no argument. If a command has both a key and a value, it *can* own a `complete_keys()` method taking no argument and a `complete_values(key)` method that can be tuned according to the key entered in the incomplete command.
67 |
68 | By default, the `Command` class has both `complete_keys` and `complete_values` methods implemented, relying on the signature of the `run(...)` method to determine command's format. Completion is handled according to the format :
69 |
70 | - `COMMAND VALUE` : Then only `complete_values` is used, handling the `values` class attribute as a list.
71 | - `COMMAND KEY VALUE` : This one uses
72 |
73 | - `complete_keys`, handling the `keys` class attribute as a list in priority, otherwise the `values` class attribute as a dictionary whose keys are the equivalent to the `keys` class attribute
74 | - `complete_values`, handling the `values` class attribute as a dictionary whose values for the key given in argument (if not given, all the values aggregated from all the keys) give the completion list
75 |
76 | ??? example "**Example**: Default completion for key-values (second command format)"
77 |
78 | :::python
79 | class DoSomething(Command):
80 | values = {"key1": ["1", "2", "3"],
81 | "key2": ["4", "5", "6"],
82 | "key3": ["7", "8", "9"]}
83 |
84 | def run(self, key=None, value=None):
85 | print(key, value)
86 |
87 | This command will yield a completion list of :
88 |
89 | - `["key1", "key2", "key3"]` when entering "`do-something `" (or "`do-something `" and a part of the possible key, without a trailing whitespace) and pressing the tab key twice
90 |
91 | 
92 |
93 | - `["4", "5", "6"]`when entering "`do-something key2 `" and pressing the tab key twice
94 |
95 | 
96 |
97 |
98 |
99 | ## Validation
100 |
101 | Validation can be especially useful as, within the CLI application, an error is dynamically displayed while typing a command, relying on command's `validate()` method. Like the completion methods, this is defined according to the signature of the `run(...)` method.
102 |
103 | By default, the `Command` class has a `validate` method that relies on both `complete_keys` and `complete_values` methods to check inputs against valid keys and values.
104 |
105 | ??? example "**Example**: Key-value validation"
106 |
107 | According to the previous example, a validation error is raised as the given value is not part of the possible values for the given key :
108 |
109 | 
110 |
111 |
112 |
--------------------------------------------------------------------------------
/docs/pages/classes/console.md:
--------------------------------------------------------------------------------
1 | A *console* is a Read-Eval-Process-Loop (REPL) environment that holds a set of enabled commands, always starting from a root console. Each child console becomes bound to its parent when started so that it can also use its configuration settings.
2 |
3 | ## Components
4 |
5 | Basically, a console holds the central logic of the CLI through multiple components :
6 |
7 | - *Files Manager* : It manages files from the *WORKSPACE* (depending on the context, that is, the root level or another one setting the workspace elsewhere, e.g. as for a project).
8 | - *Global State* : It holds the key-values to be shared amongst the console levels and modules and their associated commands.
9 | - *Datastore* : It aims to persistently save data.
10 | - *Jobs Pool* : It manages jobs to be run from the console.
11 | - *Sessions Pool* : It manages the open sessions, obtained from the execution of *modules*.
12 |
13 | In order to make a custom console, two classes exist :
14 |
15 | - The generic `Console` class : for making child console levels.
16 | - The specific `FrameworkConsole` class : to be used directly or subclassed to define the root console.
17 |
18 | ??? example "**Example**: Basic application running a `FrameworkConsole`"
19 |
20 | :::python
21 | from sploitkit import FrameworkConsole
22 |
23 | if __name__ == '__main__':
24 | FrameworkConsole("MySploit").start()
25 |
26 |
27 |
28 | ## Scope and prompt
29 |
30 | A console can be tuned in the following way using some class attributes :
31 |
32 | - `level` : the console level name, for use with *commands*
33 | - `message` : a list of tokens with their styling, as of [`prompt_toolkit`](https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#coloring-the-prompt-itself)
34 | - `style` : the style definition as a dictionary for the prompt tokens
35 |
36 | ??? example "**Example**: A console subclass for defining a new level"
37 |
38 | :::python
39 | from sploitkit import Console
40 |
41 | class MyConsole(Console):
42 | level = "new_level"
43 | message = [
44 | ('class:prompt', "["),
45 | ('class:name', "console"),
46 | ('class:prompt', "]>> "),
47 | ]
48 | style = {
49 | 'prompt': "#eeeeee",
50 | 'name': "#ff0000",
51 | }
52 |
53 | 
54 |
55 |
56 |
57 | ## Entity sources
58 |
59 | Another important attribute of the `Console` class is `sources`. It is only handled for the parent console and is defined as a dictionary with three possible keys :
60 |
61 | - `banners` (default: `None`) : for customizing the startup application banner
62 | - `entities` : a list of source folders to be parsed for importing entities
63 | - `libraries` (default: "`.`") : a list of source folders to be added to `sys.path`
64 |
65 | ??? example "**Example**: Defining sources for banners, entities and libraries"
66 |
67 | :::python
68 | from sploitkit import FrameworkConsole
69 |
70 | class MyConsole(Console):
71 | ...
72 | sources = {
73 | 'banners': "banners",
74 | 'libraries': "lib",
75 | }
76 |
77 |
78 |
--------------------------------------------------------------------------------
/docs/pages/classes/datastore.md:
--------------------------------------------------------------------------------
1 | ## Datastore
2 |
3 |
4 | ### `Model`
5 |
6 |
7 | ### `StoreExtension`
8 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/pages/classes/entity.md:
--------------------------------------------------------------------------------
1 | In order to provide a convenient API, Sploitkit defines a central class aimed to declare everything that can be tuned and imported to make a new CLI framework. As explained previously, this central class is called `Entity`. This holds the generic logic, namely for :
2 |
3 | - registering entity classes (see hereafter: `Console`, `Command`, ...)
4 | - handling requirements, dynamically enabling/disabling entities
5 | - handling metadata (formatting them for display in the CLI)
6 |
7 | ## Requirements and applicability
8 |
9 | Requirements can be defined in order to dynamically enable/disable entities. These are managed through the `requirements` class attribute. Currently, a few requirement types exist :
10 |
11 | **Key** | **Description**
12 | --- | ---
13 | `config` | Dictionary of `Config`/`Option` values to be set (see hereafter).
14 | `file` | List of files that must exist in the current workspace.
15 | `python` | List of Python packages required to be installed in the environment.
16 | `state` | State variables to be set in the *Global State* (see section *Console*) ; can be defined in three ways : List of existing keys, e.g. `['VAR1', 'VAR2']`. Dictionary of state variables (exact match), e.g. `{'VAR1': {'key1':'myval1', 'key2':'myval2'}}`. Dictionary of state values, regardless of the key, e.g. `{'VAR1': {None:'myval1'}}`.
17 | `system` | List of system tools and/or packages to be installed ; can be defined in two ways : `[tool]`, e.g. `ifconfig` ; if the system command is missing, it will only tell that this tool is not present. `[package]/[tool]`, e.g. `net-tools/ifconfig` ; this allows to be more precise regarding what is missing.
18 |
19 | In parallel with the requirements, the applicability is checked, that is, if the entity has a reference with a value that exactly matches the expected one.
20 |
21 | ??? example "**Example**: Setting a *command* as applicable only for *modules* named '`do_something`'"
22 |
23 | Let us consider defining a *command* that only applies to any *module* whose name is "`do_something`". Then defining the `applies_to` attribute like hereafter allows to limit the scope of the *command* to only *modules* named so.
24 |
25 | :::python
26 | class DoIt(Command):
27 | applies_to = [("console", "module", "name", "do_something")]
28 | [...]
29 | def run(self):
30 | [...]
31 |
32 |
33 |
34 | ## Inheritance and aggregation
35 |
36 | Entities can be defined in subclasses as a tree structure so that the leaves share some information from their proxy subclasses. The precedence goes bottom-up, that is, from the leaves to the entity classes. This is especially the case for :
37 |
38 | - `config` attribute (applies to *consoles* and *modules*) : Configurations are agreggated (in a `ProxyConfig` instance) so that an option that is common to multiple entities can be defined only once and modified for all these entities at once during the execution.
39 | - Metadata (especially useful for *modules*) : metadata is aggregated (during entity import only) so that, if multiple modules inheriting from a proxy class have, for instance, the same author, this data can be declared only once in the proxy class and applied to *modules*.
40 |
41 | ## Metadata parsing
42 |
43 | Metadata of entities can be defined in three different ways (can be combined, listed hereafter in inverse order of precedence) :
44 |
45 | 1. Docstring : By default, Sploitkit provides a parsing function that follows the convention presented hereafter, resulting in a dictionary of metadata key-values. However, a custom parsing function can be input as an argument when instantiating a parent `Console`.
46 | 2. `meta` attribute : A dictionary of metadata key-values that will update the final metadata dictionary.
47 | 3. `metadata` attribute : Same as for `meta` (exists for a question of cross-compatibility with plugins of other frameworks).
48 |
49 | This leads to a `_metadata` class attribute holding the metadata dictionary. Note that, when `meta` and `metadata` class attributes are use to update `_metadata`, they are removed to only hold this last one. This is mostly a question of compatibility with modules of other frameworks (e.g. Recon-ng).
50 |
51 | Options can even be defined through the `meta` and/or `metadata` class attributes (but NOT directly `_metadata` as it is created/overwritten when parsing the docstring). Their format follows this convention : (*name*, *default_value*, *required*, *description*). It contains less fields than what is really supported (see the `Option` class in the next subsection) but, once again, it is mostly a question of compatibility with modules from other frameworks.
52 |
53 | The default docstring format (parsed through a dedicated function within Sploitkit's utils) consists of sections separated by double newlines. Parsing occurs as follows :
54 |
55 | 1. The first section is always the *description*.
56 | 2. Next sections are handled this way :
57 |
58 | - If the first line of the section follows the convention hereafter, it is parsed as a separated field (saved in the metadata dictionary as lowercase) up to the next field-value OR section's end.
59 |
60 | [Field]: [value]
61 |
62 | That is, the field name capitalized with no whitespace before the colon and whatever value, multiline.
63 |
64 | - If the first line of the section does not follow the convention, it is parsed as a *comment* and saved into the *comments* list of the metadata dictionary. Note that using the field name *comments* append the value to the *comments* list of the metadata dictionary.
65 |
66 | ??? example "**Example**: Writing a docstring for an entity"
67 |
68 | :::python
69 | class Example(object):
70 | \"""
71 | This is a test multi-line long
72 | description.
73 |
74 | This is a first comment.
75 |
76 | Author: John Doe
77 | (john.doe@example.com)
78 | Version: 1.0
79 | Comments:
80 | - subcomment 1
81 | - subcomment 2
82 |
83 | Something: lorem ipsum
84 | paragraph
85 |
86 | This is a second comment,
87 | a multi-line one.
88 | \"""
89 | [...]
90 |
91 | >>> parse_docstring(Example)
92 | {'author': 'John Doe (john.doe@example.com)',
93 | 'comments': ['This is a first comment.',
94 | ('subcomment 1', 'subcomment 2'),
95 | 'This is a second comment, a multi-line one.'],
96 | 'description': 'This is a test multi-line long description.',
97 | 'something': 'lorem ipsum paragraph',
98 | 'version': '1.0'}
99 |
100 |
101 |
102 | ## `Config` and `Option`
103 |
104 | A configuration object is an instance of the `Config` class subclassing the common type `dict` and refining its capabilities to handle special key-value objects called `Option`'s that also have a description and other attributes (e.g. `required`). This way, it is easier to associate more data than simply a value to a key, i.e. when it comes to providing help text about the option.
105 |
106 | A configuration is declared by providing a dictionary as the only positional argument and/or key-values as keyword-arguments. It is important to note that, if options are defined with the keyword-arguments, they won't of course have any other data defined but they will be easilly accessible for further tuning.
107 |
108 | ??? example "**Example**: Declaring a configuration (entity class attribute)"
109 |
110 | :::python
111 | from sploitkit import Config, Option, ROption
112 |
113 | config = Config({
114 | Option(...),
115 | ROption(...),
116 | })
117 |
118 |
119 |
120 | !!! note "`Option` and `ROption`"
121 |
122 | Two types of option exist (for a question of performance) : `Option` (the normal one) and `ROption` (aka *Resetting Option*) that triggers resetting the entity bindings (e.g. the commands applicability to the current console given the new option). So, beware that, when using the `Option` class, the modification of its value does not update bindings between entities.
123 |
124 | An example of use of the behavior of `ROption` is when a `config` requirement is used in another entity which is to be enabled/disabled according to option's value. This way, entity bindings are reset when tuning the option like when starting a console (for more details on this, see section *Console*).
125 |
126 | A configuration option object, that is, an instance of the `Option` or `ROption` class, is defined using multiple arguments :
127 |
128 | **Argument** | **Type** | **Default** | **Description**
129 | --- | :---: | :---: | ---
130 | `name` | `str` | | option's name, conventionally uppercase
131 | `description` | `str` | `None` | help text for this option
132 | `required` | `bool` | `False` | whether it shall be defined or not
133 | `choices` | `list`/`lambda` | `None` | the possible values (as a list or lazily defined through a lambda function that outputs a list), used for validation ; its value can also be `bool` (the type, not as a string !) for setting choices to false and true
134 | `set_callback` | `lambda` | `None` | a function that is triggered after setting the value
135 | `unset_callback` | `lambda` | `None` | a function that is triggered after unsetting the value
136 | `transform` | `lambda` | `None` | a function transforming the value input as for any dictionary, but for computing a new value
137 | `validate` | `lambda` | `None` | by default, a lambda function that checks for the given `choices` if defined, but can be tuned accordingly
138 |
139 | Each lambda function takes `self` as the first argument. `transform` and `validate` also takes option's value as the second argument.
140 |
141 | ??? example "Config declaration (extract from the `FrameworkConsole` class)"
142 |
143 | :::python
144 | config = Config({
145 | ...,
146 | ROption(
147 | 'DEBUG',
148 | "debug mode",
149 | False,
150 | bool,
151 | set_callback=lambda o: o.config.console._set_logging(o.value),
152 | ): "false",
153 | ...,
154 | Option(
155 | 'WORKSPACE',
156 | "folder where results are saved",
157 | True,
158 | ): "~/Notes",
159 | })
160 |
161 |
162 |
163 | ## Utility class methods
164 |
165 |
166 |
--------------------------------------------------------------------------------
/docs/pages/classes/index.md:
--------------------------------------------------------------------------------
1 | The following figure depicts the class hierarchy implementing the aforementioned design :
2 |
3 |
4 |
5 | On this figure, one can see the main attributes and methods that can be used (in black) or overridden (in red).
6 |
--------------------------------------------------------------------------------
/docs/pages/classes/module.md:
--------------------------------------------------------------------------------
1 | ## `Module` class
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/pages/css/extra.css:
--------------------------------------------------------------------------------
1 | /* Full width (only works for some themes, including 'material') */
2 | @media only screen and (min-width: 76.25em) {
3 | .md-main__inner {
4 | max-width: none;
5 | }
6 | .md-sidebar--primary {
7 | left: 0;
8 | }
9 | .md-sidebar--secondary {
10 | right: 0;
11 | margin-left: 0;
12 | -webkit-transform: none;
13 | transform: none;
14 | }
15 | }
16 |
17 | /* See https://github.com/mkdocs/mkdocs/wiki/MkDocs-Recipes */
18 | /* Add Support for Checkbox Lists */
19 | .task-list-item {
20 | list-style-type: none;
21 | }
22 |
23 | .task-list-item input {
24 | margin: 0 4px 0.25em -20px;
25 | vertical-align: middle;
26 | }
27 |
--------------------------------------------------------------------------------
/docs/pages/design.md:
--------------------------------------------------------------------------------
1 | Sploitkit's API conveniently defines the CLI framework in an Object-Oriented fashion. *Consoles* have a set of *commands* and can be associated with *modules*, which are capable of handling their context in isolation and save/restore data from a *datastore* according to user-defined *models*. Datastores can also be customized using *store extensions*.
2 |
3 | Thanks to compartmentalization in *projects*, *files*, *jobs* and *sessions*, it becomes easier to organize your work or generate reports.
4 |
5 | To sum it up, Sploitkit aims to be highly customizable while keeping the same CLI philosophy as Metasploit, while leveraging Python and the power of [`prompt_toolkit`](https://github.com/prompt-toolkit/python-prompt-toolkit) in order to enhance the user experience through command-line completion and validation.
6 |
7 | ## Main architecture
8 |
9 | This library is designed around a central class called [*entity*](classes/entity.html). An entity centralizes features such as class registry, which keeps track of relevant sub-entities like *consoles*, *commands* and *modules*. This means every entity class inherits from this main class and then defines additional features of its own.
10 |
11 | Basically, [five different "main" entity classes](classes.html) are defined :
12 |
13 | - [`Console`](classes/console.html) : for defining CLI console levels
14 | - [`Command`](classes/command.html) : for defining console commands, accessible from console levels
15 | - [`Module`](classes/module.html) : for declaring modules with specific functionalities like in Metasploit
16 | - [`Model`](classes/datastore.html) : for describing data schemas to be recorded in the datastore
17 | - [`StoreExtension`](classes/datastore.html) : for defining mixins to be used with the datastore
18 |
19 | At startup, Sploitkit loads every entity it finds in the user-defined
20 | sources, as well as a pre-defined set of generic commands (like in
21 | Metasploit or Recon-ng). This behaviour can be disabled if so desired.
22 | Instantiation begins with a `Console` and then proceeds with the loading
23 | of all the other entities. For convenience, a `FrameworkConsole`
24 | containing some some base functionalities is provided. It serves as a
25 | good starting point for newcomers to Sploitkit.
26 |
27 | !!! note "Back-referencing"
28 |
29 | Back-referencing is heavily used throughout Sploitkit:
30 |
31 | * `module.console` refers to the parent console of module
32 | * calling `self.config.console` within an `option` allows to "walk up" the chain up to the console, and to create triggers for it
33 |
34 | ## Project structure
35 |
36 | The package is structured as follows :
37 |
38 | - `base` : This contains [base entities](/base.html) to be included by default in any
39 | application. Note that if some base commands are not required, they can be disabled (see section *Classes*/`Command`).
40 | - `core` : This holds the core functionalities of Sploitkit with the class definitions for `Entity` and the main entity classes but also components for the main console.
41 | - `utils` : This contains utility modules that are not specifically part of the `base` and `core` subpackages.
42 |
43 | 
44 |
45 | 
46 |
--------------------------------------------------------------------------------
/docs/pages/examples/dronesploit.md:
--------------------------------------------------------------------------------
1 | [DroneSploit](https://github.com/dhondta/dronesploit) is a console tailored to drone hacking.
2 |
3 | ### Setup
4 |
5 | ```sh
6 | $ pip3 install dronesploit
7 | [...]
8 | ```
9 |
10 | ### Usage
11 |
12 | ```sh
13 | $ dronesploit --help
14 | usage: dronesploit [--dev] [-h] [-v]
15 |
16 | Dronesploit
17 |
18 | optional arguments:
19 | --dev development mode (default: False)
20 |
21 | extra arguments:
22 | -h, --help show this help message and exit
23 | -v, --verbose verbose mode (default: False)
24 |
25 | $ dronesploit
26 | [...]
27 | ```
28 |
29 | 
30 |
--------------------------------------------------------------------------------
/docs/pages/img/class-hierarchy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/class-hierarchy.png
--------------------------------------------------------------------------------
/docs/pages/img/classes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/classes.png
--------------------------------------------------------------------------------
/docs/pages/img/command-key-completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/command-key-completion.png
--------------------------------------------------------------------------------
/docs/pages/img/command-validation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/command-validation.png
--------------------------------------------------------------------------------
/docs/pages/img/command-value-completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/command-value-completion.png
--------------------------------------------------------------------------------
/docs/pages/img/console-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/console-prompt.png
--------------------------------------------------------------------------------
/docs/pages/img/dronesploit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/dronesploit.png
--------------------------------------------------------------------------------
/docs/pages/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/icon.png
--------------------------------------------------------------------------------
/docs/pages/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/logo.png
--------------------------------------------------------------------------------
/docs/pages/img/my-sploit-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/my-sploit-start.png
--------------------------------------------------------------------------------
/docs/pages/img/packages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/packages.png
--------------------------------------------------------------------------------
/docs/pages/img/under-construction.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/docs/pages/img/under-construction.png
--------------------------------------------------------------------------------
/docs/pages/index.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | Sploitkit is a development kit designed to quickly build CLI consoles with a style resembling that of Metasploit. It features a clear and intuitive plugin architecture that allows to build consoles with new commands or modules but also models for their internal stores. The framework is built with the DRY philosophy in mind.
4 |
5 | The idea is to make creating consoles as easy as this:
6 |
7 | ```sh
8 | $ sploitkit my-sploit
9 | $ cd my-sploit
10 | $ gedit main.py
11 | ```
12 |
13 | ```python
14 | #!/usr/bin/python3
15 | from sploitkit import FrameworkConsole
16 | from tinyscript import *
17 |
18 |
19 | class MySploitConsole(FrameworkConsole):
20 | #TODO: set your console attributes
21 | pass
22 |
23 |
24 | if __name__ == '__main__':
25 | parser.add_argument("-d", "--dev", action="store_true", help="enable development mode")
26 | parser.add_argument("-r", "--rcfile", type=ts.file_exists, help="execute commands from a rcfile")
27 | initialize()
28 | c = MySploitConsole(
29 | "MySploit",
30 | #TODO: configure your console settings
31 | dev=args.dev,
32 | )
33 | c.rcfile(args.rcfile) if args.rcfile else c.start()
34 | ```
35 |
36 | This will give the following (no banner, ASCII image or quote yet):
37 |
38 | 
39 |
40 | -----
41 |
42 | ## Setup
43 |
44 | This library is available on [PyPi](https://pypi.python.org/pypi/sploitkit/) and can be simply installed using Pip:
45 |
46 | ```sh
47 | pip install sploitkit
48 | ```
49 |
50 | -----
51 |
52 | ## Rationale
53 |
54 | This library is born from the need of quickly building toolsets tailored to various scopes which are sometimes not extensively covered in some well-known frameworks (like Metasploit).
55 |
56 | It relies on the awesome Python library [`prompt_toolkit`](https://github.com/prompt-toolkit/python-prompt-toolkit) to provide an enhanced CLI environment, adding multiple graphical elements (e.g. dropdown lists for completion and a dynamic toolbar for displaying command syntax errors) greatly improving user's experience regarding some classical tools (like e.g. or also [`rpl-attacks`](https://github.com/dhondta/rpl-attacks) or [`recon-ng`](https://github.com/lanmaster53/recon-ng), which have some limits on the usability point of view because of the [`cmd` module](https://docs.python.org/3/library/cmd.html)).
57 |
58 | I personally use this library to create CLI consoles for my job or during cybersecurity engagements or programming competitions and it proved very useful and convenient.
59 |
--------------------------------------------------------------------------------
/docs/pages/quickstart.md:
--------------------------------------------------------------------------------
1 | ## Creating a project
2 |
3 | Creating a project can be achieved by using the `sploitkit-new` tool like follows :
4 |
5 | ```sh
6 | $ sploitkit-new --help
7 | usage: sploitkit-new [-s] [-h] [-v] name
8 |
9 | SploitkitNew
10 |
11 | positional arguments:
12 | name project name
13 |
14 | optional arguments:
15 | -s, --show-todo show the TODO list (default: False)
16 |
17 | extra arguments:
18 | -h, --help show this help message and exit
19 | -v, --verbose verbose mode (default: False)
20 |
21 | ```
22 |
23 | ```sh
24 | $ sploitkit-new -s my-sploit
25 | 12:34:56 [INFO] TODO list:
26 | - [README:3] Fill in the README
27 | - [main.py:MySploitConsole:6] set your console attributes
28 | - [main.py:MySploitConsole:13] configure your console settings
29 | - [commands/template.py:CommandWithOneArg:9] compute the list of possible values
30 | - [commands/template.py:CommandWithOneArg:13] compute results here
31 | - [commands/template.py:CommandWithOneArg:17] validate the input value
32 | - [commands/template.py:CommandWithTwoArgs:27] compute the list of possible keys
33 | - [commands/template.py:CommandWithTwoArgs:31] compute the list of possible values taking the key into account
34 | - [commands/template.py:CommandWithTwoArgs:35] compute results here
35 |
36 | ```
37 |
38 | This creates a folder `my-sploit` with the following items :
39 |
40 | ```sh
41 | $ cd my-sploit/
42 | $ ll
43 | total 28K
44 | drwxrwxr-x 5 user user 4.0K 2019-12-25 12:34 .
45 | drwxr-xr-x 102 user user 4.0K 2019-12-25 12:34 ..
46 | drwxrwxr-x 2 user user 4.0K 2019-12-25 12:34 banners
47 | drwxrwxr-x 2 user user 4.0K 2019-12-25 12:34 commands
48 | -rw-rw-r-- 1 user user 279 2019-12-25 12:34 main.py
49 | drwxrwxr-x 2 user user 4.0K 2019-12-25 12:34 modules
50 | -rw-rw-r-- 1 user user 31 2019-12-25 12:34 README
51 | -rw-rw-r-- 1 user user 0 2019-12-25 12:34 requirements.txt
52 |
53 | ```
54 |
55 | -----
56 |
57 | ## Setting the root console
58 |
59 | -----
60 |
61 | ## Adding commands
62 |
63 |
64 | -----
65 |
66 | ## Adding modules
67 |
68 |
69 | -----
70 |
71 | ## Tuning the datastore
72 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | jinja2<3.1.0
2 | mkdocs>=1.3.0
3 | mkdocs-bootswatch
4 | mkdocs-material>=7.3.0
5 | mkdocs-rtd-dropdown
6 | pymdown-extensions
7 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0", "setuptools-scm"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.pytest.ini_options]
6 | pythonpath = ["src"]
7 |
8 | [tool.setuptools.dynamic]
9 | version = {attr = "sploitkit.__info__.__version__"}
10 |
11 | [tool.setuptools.packages.find]
12 | where = ["src"]
13 |
14 | [tool.setuptools.package-data]
15 | "*" = ["*.txt"]
16 |
17 | [project]
18 | name = "sploitkit"
19 | authors = [
20 | {name="Alexandre D'Hondt", email="alexandre.dhondt@gmail.com"},
21 | ]
22 | description = "Devkit for easilly building Metasploit-like framework consoles"
23 | license = {file = "LICENSE"}
24 | keywords = ["python", "development", "programming", "cli", "framework", "console", "devkit"]
25 | requires-python = ">=3.8,<4"
26 | classifiers = [
27 | "Development Status :: 4 - Beta",
28 | "Environment :: Console",
29 | "Intended Audience :: Developers",
30 | "Intended Audience :: Information Technology",
31 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
32 | "Programming Language :: Python :: 3",
33 | "Programming Language :: Python :: 3.8",
34 | "Programming Language :: Python :: 3.9",
35 | "Programming Language :: Python :: 3.10",
36 | "Programming Language :: Python :: 3.11",
37 | "Programming Language :: Python :: 3.12",
38 | "Topic :: Software Development :: Libraries :: Python Modules",
39 | ]
40 | dependencies = [
41 | "peewee>=3.17.6",
42 | "prompt_toolkit>=3.0.47",
43 | "requests",
44 | "tinyscript>=1.30.15",
45 | ]
46 | dynamic = ["version"]
47 |
48 | [project.readme]
49 | file = "README.md"
50 | content-type = "text/markdown"
51 |
52 | [project.urls]
53 | documentation = "https://python-sploitkit.readthedocs.io/en/latest/?badge=latest"
54 | homepage = "https://github.com/dhondta/python-sploitkit"
55 | issues = "https://github.com/dhondta/python-sploitkit/issues"
56 | repository = "https://github.com/dhondta/python-sploitkit"
57 |
58 | [project.scripts]
59 | sploitkit = "sploitkit.__main__:main"
60 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | python_paths = src
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | peewee>=3.17.6
2 | prompt_toolkit>=3.0.47
3 | requests
4 | tinyscript>=1.30.15
5 |
--------------------------------------------------------------------------------
/src/sploitkit/VERSION.txt:
--------------------------------------------------------------------------------
1 | 0.6.0
2 |
--------------------------------------------------------------------------------
/src/sploitkit/__info__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """Sploitkit package information.
3 |
4 | """
5 | import os
6 |
7 | __author__ = "Alexandre D'Hondt"
8 | __email__ = "alexandre.dhondt@gmail.com"
9 | __copyright__ = "© 2019-2020 A. D'Hondt"
10 | __license__ = "agpl-3.0"
11 |
12 | with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
13 | __version__ = f.read().strip()
14 |
15 |
--------------------------------------------------------------------------------
/src/sploitkit/__init__.py:
--------------------------------------------------------------------------------
1 | import re
2 | from ipaddress import ip_address
3 | from peewee import *
4 | from peewee import __all__ as _peewee
5 | from tinyscript.helpers.path import Path
6 |
7 | from .core import *
8 | from .core import __all__ as _core
9 | from .core.console import print_formatted_text
10 |
11 |
12 | __all__ = _core + _peewee
13 | __all__ += ["print_formatted_text", "IPAddressField", "MACAddressField", "Path"]
14 |
15 |
16 | # -------------------------------------- Peewee extra fields --------------------------------------
17 | class IPAddressField(BigIntegerField):
18 | """ IPv4/IPv6 address database field. """
19 | def db_value(self, value):
20 | if isinstance(value, (str, int)):
21 | try:
22 | return int(ip_address(value))
23 | except Exception:
24 | pass
25 | raise ValueError("Invalid IPv4 or IPv6 Address")
26 |
27 | def python_value(self, value):
28 | return ip_address(value)
29 |
30 |
31 | class MACAddressField(BigIntegerField):
32 | """ MAC address database field. """
33 | def db_value(self, value):
34 | if isinstance(value, int) and 0 <= value <= 0xffffffffffffffff:
35 | return value
36 | elif isinstance(value, str):
37 | if re.search(r"^([0-9a-f]{2}[:-]){5}[0-9A-F]{2}$", value, re.I):
38 | return int("".join(re.split(r"[:-]", value)), 16)
39 | raise ValueError("Invalid MAC Address")
40 |
41 | def python_value(self, value):
42 | try:
43 | return ":".join(re.findall("..", "%012x" % value))
44 | except Exception:
45 | raise ValueError("Invalid MAC Address")
46 |
47 |
--------------------------------------------------------------------------------
/src/sploitkit/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | from sploitkit.__info__ import __author__, __copyright__, __email__, __license__, __version__
4 | from tinyscript import *
5 |
6 |
7 | __name__ = "__main__"
8 | __script__ = "sploitkit"
9 | __examples__ = ["my-sploit", "my-sploit -s"]
10 | __doc__ = """
11 | This tool allows to quickly create a new Sploitkit project.
12 | """
13 |
14 |
15 | MAIN = """#!/usr/bin/python3
16 | from sploitkit import FrameworkConsole
17 | from tinyscript import *
18 |
19 |
20 | class MySploitConsole(FrameworkConsole):
21 | #TODO: set your console attributes
22 | pass
23 |
24 |
25 | if __name__ == '__main__':
26 | parser.add_argument("-d", "--dev", action="store_true", help="enable development mode")
27 | parser.add_argument("-r", "--rcfile", type=ts.file_exists, help="execute commands from a rcfile")
28 | initialize(exit_at_interrupt=False)
29 | c = MySploitConsole(
30 | "MySploit",
31 | #TODO: configure your console settings
32 | dev=args.dev,
33 | debug=args.verbose,
34 | )
35 | c.rcfile(args.rcfile) if args.rcfile else c.start()
36 | """
37 | COMMANDS = """from sploitkit import *
38 |
39 |
40 | class CommandWithOneArg(Command):
41 | \""" Description here \"""
42 | level = "module"
43 | single_arg = True
44 |
45 | def complete_values(self):
46 | #TODO: compute the list of possible values
47 | return []
48 |
49 | def run(self):
50 | #TODO: compute results here
51 | pass
52 |
53 | def validate(self, value):
54 | #TODO: validate the input value
55 | if value not in self.complete_values():
56 | raise ValueError("invalid value")
57 |
58 |
59 | class CommandWithTwoArgs(Command):
60 | \""" Description here \"""
61 | level = "module"
62 |
63 | def complete_keys(self):
64 | #TODO: compute the list of possible keys
65 | return []
66 |
67 | def complete_values(self, key=None):
68 | #TODO: compute the list of possible values taking the key into account
69 | return []
70 |
71 | def run(self):
72 | #TODO: compute results here
73 | pass
74 | """
75 | MODULES = """from sploitkit import *
76 |
77 |
78 | class MyFirstModule(Module):
79 | \""" Description here
80 |
81 | Author: your name (your email)
82 | Version: 1.0
83 | \"""
84 | def run(self):
85 | pass
86 |
87 |
88 | class MySecondModule(Module):
89 | \""" Description here
90 |
91 | Author: your name (your email)
92 | Version: 1.0
93 | \"""
94 | def run(self):
95 | pass
96 | """
97 |
98 |
99 | PROJECT_STRUCTURE = {
100 | 'README': "# {}\n\n#TODO: Fill in the README",
101 | 'main.py': MAIN,
102 | 'requirements.txt': None,
103 | 'banners': {},
104 | 'commands': {'commands.py': COMMANDS},
105 | 'modules': {'modules.py': MODULES},
106 | }
107 |
108 |
109 | def main():
110 | parser.add_argument("name", help="project name")
111 | parser.add_argument("-s", "--show-todo", dest="todo", action="store_true", help="show the TODO list")
112 | initialize(noargs_action="wizard")
113 | p = ts.ProjectPath(args.name, PROJECT_STRUCTURE)
114 | if args.todo:
115 | for k, v in p.todo.items():
116 | print("- [%s] %s" % (k, v))
117 |
118 |
--------------------------------------------------------------------------------
/src/sploitkit/base/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/src/sploitkit/base/__init__.py
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/src/sploitkit/base/commands/__init__.py
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/general.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import re
3 | import shlex
4 | from prompt_toolkit.formatted_text import ANSI
5 | from tinyscript.helpers import human_readable_size, BorderlessTable, Path
6 |
7 | from sploitkit import *
8 | from sploitkit.core.module import MetaModule
9 |
10 |
11 | projects = lambda cmd: [x.filename for x in cmd.workspace.iterpubdir()]
12 |
13 |
14 | # ---------------------------- GENERAL-PURPOSE COMMANDS ------------------------
15 | class Back(Command):
16 | """ Come back to the previous console level """
17 | except_levels = ["root", "session"]
18 |
19 | def run(self):
20 | raise ConsoleExit
21 |
22 |
23 | class Exit(Command):
24 | """ Exit the console """
25 | aliases = ["quit"]
26 | except_levels = ["session"]
27 |
28 | def run(self):
29 | raise ConsoleExit
30 |
31 |
32 | class Help(Command):
33 | """ Display help """
34 | aliases = ["?"]
35 |
36 | def run(self):
37 | print_formatted_text(Command.get_help("general", self.console.level))
38 |
39 |
40 | class Search(Command):
41 | """ Search for text in modules """
42 | except_levels = ["session"]
43 | single_arg = True
44 |
45 | def run(self, text):
46 | keywords = shlex.split(text)
47 | data = [["Name", "Path", "Description"]]
48 | for m in Module.subclasses:
49 | for k in keywords:
50 | if m.search(k):
51 | data.append([m.name, m.path, m.description])
52 | if len(data) == 1:
53 | self.logger.error("No match found")
54 | else:
55 | t = BorderlessTable(data, "Matching modules")
56 | print_formatted_text(t.table)
57 | n = len(data) - 2
58 | self.logger.info(f"{n} match{['', 'es'][n > 0]} found")
59 |
60 |
61 | class Show(Command):
62 | """ Show options, projects, modules or issues (if any) """
63 | level = "root"
64 | keys = ["files", "modules", "options", "projects"]
65 |
66 | def complete_values(self, key):
67 | if key == "files":
68 | if self.config.option("TEXT_VIEWER").value is not None:
69 | return list(map(str, self.console._files.list))
70 | return []
71 | elif key == "issues":
72 | l = []
73 | for cls, subcls, errors in Entity.issues():
74 | l.extend(list(errors.keys()))
75 | return l
76 | elif key == "modules":
77 | uncat = any(isinstance(m, MetaModule) for m in self.console.modules.values())
78 | l = [c for c, m in self.console.modules.items() if not isinstance(m, MetaModule)]
79 | return l + ["uncategorized"] if uncat else l
80 | elif key == "options":
81 | return list(self.config.keys())
82 | elif key == "projects":
83 | return projects(self)
84 | elif key == "sessions":
85 | return [str(i) for i, _ in self.console._sessions]
86 |
87 | def run(self, key, value=None):
88 | if key == "files":
89 | if value is None:
90 | data = [["Path", "Size"]]
91 | p = Path(self.config.option("WORKSPACE").value)
92 | for f in self.console._files.list:
93 | data.append([f, human_readable_size(p.joinpath(f).size)])
94 | print_formatted_text(BorderlessTable(data, "Files from the workspace"))
95 | elif self.config.option("TEXT_VIEWER").value:
96 | self.console._files.view(value)
97 | elif key == "issues":
98 | t = Entity.get_issues()
99 | if len(t) > 0:
100 | print_formatted_text(t)
101 | elif key == "modules":
102 | h = Module.get_help(value)
103 | if h.strip() != "":
104 | print_formatted_text(h)
105 | else:
106 | self.logger.warning("No module loaded")
107 | elif key == "options":
108 | if value is None:
109 | print_formatted_text(ANSI(str(self.config)))
110 | else:
111 | c = Config()
112 | c[self.config.option(value)] = self.config[value]
113 | print_formatted_text(ANSI(str(c)))
114 | elif key == "projects":
115 | if value is None:
116 | data = [["Name"]]
117 | for p in projects(self):
118 | data.append([p])
119 | print_formatted_text(BorderlessTable(data, "Existing projects"))
120 | else:
121 | print_formatted_text(value)
122 | elif key == "sessions":
123 | data = [["ID", "Description"]]
124 | for i, s in self.console._sessions:
125 | data.append([str(i), getattr(s, "description", "")])
126 | print_formatted_text(BorderlessTable(data, "Open sessions"))
127 |
128 | def set_keys(self):
129 | if Entity.has_issues():
130 | self.keys += ["issues"]
131 | else:
132 | while "issues" in self.keys:
133 | self.keys.remove("issues")
134 | if len(self.console._sessions) > 0:
135 | self.keys += ["sessions"]
136 | else:
137 | while "sessions" in self.keys:
138 | self.keys.remove("sessions")
139 |
140 | def validate(self, key, value=None):
141 | if key not in self.keys:
142 | raise ValueError("invalid key")
143 | if value is not None:
144 | if key == "files":
145 | if self.config.option("TEXT_VIEWER").value is None:
146 | raise ValueError("cannot view file ; TEXT_VIEWER is not set")
147 | if value not in self.complete_values(key):
148 | raise ValueError("invalid file")
149 | elif key == "issues":
150 | if value not in self.complete_values(key):
151 | raise ValueError("invalid error type")
152 | elif key == "modules":
153 | if value is not None and value not in self.complete_values(key):
154 | raise ValueError("invalid module")
155 | elif key == "options":
156 | if value is not None and value not in self.complete_values(key):
157 | raise ValueError("invalid option")
158 | elif key == "projects":
159 | if value is not None and value not in self.complete_values(key):
160 | raise ValueError("invalid project name")
161 |
162 |
163 | # ---------------------------- OPTIONS-RELATED COMMANDS ------------------------
164 | class Set(Command):
165 | """ Set an option in the current context """
166 | except_levels = ["session"]
167 |
168 | def complete_keys(self):
169 | return self.config.keys()
170 |
171 | def complete_values(self, key):
172 | if key.upper() == "WORKSPACE":
173 | return [str(x) for x in Path(".").home().iterpubdir()]
174 | return self.config.option(key).choices or []
175 |
176 | def run(self, key, value):
177 | self.config[key] = value
178 |
179 | def validate(self, key, value):
180 | if key not in self.config.keys():
181 | raise ValueError("invalid option")
182 | o = self.config.option(key)
183 | if o.required and value is None:
184 | raise ValueError("a value is required")
185 | if not o.validate(value):
186 | raise ValueError("invalid value")
187 |
188 |
189 | class Unset(Command):
190 | """ Unset an option from the current context """
191 | except_levels = ["session"]
192 |
193 | def complete_values(self):
194 | for k in self.config.keys():
195 | if not self.config.option(k).required:
196 | yield k
197 |
198 | def run(self, key):
199 | del self.config[key]
200 |
201 | def validate(self, key):
202 | if key not in self.config.keys():
203 | raise ValueError("invalid option")
204 | if self.config.option(key).required:
205 | raise ValueError("this option is required")
206 |
207 |
208 | class Setg(Command):
209 | """ Set a global option """
210 | except_levels = ["session"]
211 |
212 | def complete_keys(self):
213 | return self.config.keys(True)
214 |
215 | def complete_values(self, key):
216 | return self.config.option(key).choices or []
217 |
218 | def run(self, key, value):
219 | self.config.setglobal(key, value)
220 |
221 | def validate(self, key, value):
222 | try:
223 | o = self.config.option(key)
224 | if not o.glob:
225 | raise ValueError("cannot be set as global")
226 | if not o.validate(value):
227 | raise ValueError("invalid value")
228 | except KeyError:
229 | pass
230 |
231 |
232 | class Unsetg(Command):
233 | """ Unset a global option """
234 | except_levels = ["session"]
235 |
236 | def complete_values(self):
237 | return self.config._g.keys()
238 |
239 | def run(self, key):
240 | self.config.unsetglobal(key)
241 |
242 | def validate(self, key):
243 | if key not in self.config._g.keys():
244 | raise ValueError("invalid option")
245 |
246 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from prompt_toolkit.formatted_text import ANSI
3 |
4 | from sploitkit import *
5 |
6 |
7 | # ----------------------------- SUBCONSOLE DEFINITION --------------------------
8 | class ModuleConsole(Console):
9 | """ Module subconsole definition. """
10 | level = "module"
11 | message = [
12 | ('class:prompt', " "),
13 | ('class:prompt', None),
14 | ('class:prompt', "("),
15 | ('class:module', None),
16 | ('class:prompt', ")"),
17 | ]
18 | style = {
19 | 'prompt': "#eeeeee",
20 | 'module': "#ff0000",
21 | }
22 |
23 | def __init__(self, parent, module):
24 | self.attach(module, True)
25 | self.logname = module.fullpath
26 | self.message[1] = ('class:prompt', self.module.category)
27 | self.message[3] = ('class:module', self.module.base)
28 | self.opt_prefix = "Module"
29 | super(ModuleConsole, self).__init__(parent)
30 |
31 |
32 | # ---------------------------- GENERAL-PURPOSE COMMANDS ------------------------
33 | class Use(Command):
34 | """ Select a module """
35 | except_levels = ["session"]
36 |
37 | def complete_values(self):
38 | return Module.get_list()
39 |
40 | def run(self, module):
41 | new_mod, old_mod = Module.get_modules(module), self.module
42 | # avoid starting a new subconsole for the same module
43 | if old_mod is not None and old_mod.fullpath == new_mod.fullpath:
44 | return
45 | ModuleConsole(self.console, new_mod).start()
46 |
47 |
48 | # ----------------------------- MODULE-LEVEL COMMANDS --------------------------
49 | class ModuleCommand(Command):
50 | """ Proxy class (for setting the level attribute). """
51 | level = "module"
52 |
53 |
54 | class Run(ModuleCommand):
55 | """ Run module """
56 | def run(self):
57 | if self.module.check():
58 | self.module._instance.run()
59 |
60 |
61 | class Show(ModuleCommand):
62 | """ Show module-relevant information or options """
63 | keys = ["info", "options"]
64 |
65 | def complete_values(self, key):
66 | if key == "options":
67 | return list(self.config.keys())
68 | elif key == "issues":
69 | l = []
70 | for attr in ["console", "module"]:
71 | for _, __, errors in getattr(self, attr).issues(self.cname):
72 | l.extend(list(errors.keys()))
73 | return l
74 |
75 | def run(self, key, value=None):
76 | if key == "options":
77 | if value is None:
78 | print_formatted_text(ANSI(str(self.config)))
79 | else:
80 | c = Config()
81 | c[self.config.option(value), True] = self.config[value]
82 | print_formatted_text(ANSI(str(c)))
83 | elif key == "info":
84 | i = self.console.module.get_info(("fullpath|path", "description"), ("author", "email", "version"),
85 | ("comments",), ("options",), show_all=True)
86 | if len(i.strip()) != "":
87 | print_formatted_text(i)
88 | elif key == "issues":
89 | t = Entity.get_issues()
90 | if len(t) > 0:
91 | print_formatted_text(t)
92 |
93 | def set_keys(self):
94 | if self.module and self.module.has_issues(self.cname):
95 | self.keys += ["issues"]
96 | else:
97 | while "issues" in self.keys:
98 | self.keys.remove("issues")
99 |
100 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/project.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from tinyscript.helpers import confirm, Path, ProjectPath
3 |
4 | from sploitkit import *
5 |
6 |
7 | # ----------------------------- SUBCONSOLE DEFINITION --------------------------
8 | class ProjectConsole(Console):
9 | """ Project subconsole definition. """
10 | level = "project"
11 | message = [
12 | ('class:prompt', "["),
13 | ('class:project', None),
14 | ('class:prompt', "]"),
15 | ]
16 | style = {
17 | 'prompt': "#eeeeee",
18 | 'project': "#0000ff",
19 | }
20 |
21 | def __init__(self, parent, name):
22 | self.logname = name
23 | self.message[1] = ('class:project', name)
24 | self.config['WORKSPACE'] = str(Path(parent.config['WORKSPACE']).joinpath(name))
25 | super(ProjectConsole, self).__init__(parent)
26 |
27 |
28 | # ------------------------------ ROOT-LEVEL COMMANDS ---------------------------
29 | # These commands are available at the root level to reference a project (archive|create|select|...)
30 | class RootCommand(Command):
31 | """ Proxy class for setting the level attribute. """
32 | level = "root"
33 |
34 |
35 | class ProjectRootCommand(RootCommand):
36 | """ Proxy class for defining the complete_values method. """
37 | single_arg = True
38 |
39 | def complete_values(self):
40 | return [x.filename for x in self.workspace.iterpubdir()]
41 |
42 |
43 | class Archive(ProjectRootCommand):
44 | """ Archive a project to a ZIP file (it removes the project folder) """
45 | def run(self, project):
46 | projpath = Path(self.workspace).joinpath(project)
47 | folder = ProjectPath(projpath)
48 | self.logger.debug(f"Archiving project '{project}'...")
49 | ask = self.console.config.option("ENCRYPT_PROJECT").value
50 | try:
51 | folder.archive(ask=ask)
52 | self.logger.success(f"'{project}' archived")
53 | except OSError as e:
54 | self.logger.error(str(e))
55 | self.logger.failure(f"'{project}' not archived")
56 |
57 |
58 | class Delete(ProjectRootCommand):
59 | """ Delete a project """
60 | def run(self, project):
61 | self.logger.debug(f"Deleting project '{project}'...")
62 | self.workspace.joinpath(project).remove()
63 | self.logger.success(f"'{project}' deleted")
64 |
65 |
66 | class Load(ProjectRootCommand):
67 | """ Load a project from a ZIP file (it removes the ZIP file) """
68 | def complete_values(self):
69 | # this returns the list of *.zip in the workspace folder
70 | return [x.stem for x in self.workspace.iterfiles(".zip")]
71 |
72 | def run(self, project):
73 | self.logger.debug(f"Loading archive '{project}.zip'...")
74 | projpath = Path(self.workspace).joinpath(project)
75 | archive = ProjectPath(projpath.with_suffix(".zip"))
76 | ask = self.console.config.option("ENCRYPT_PROJECT").value
77 | try:
78 | archive.load(ask=ask)
79 | self.logger.success(f"'{project}' loaded")
80 | except Exception as e:
81 | self.logger.error("Bad password" if "error -3" in str(e) else str(e))
82 | self.logger.failure(f"'{project}' not loaded")
83 |
84 | def validate(self, project):
85 | if project not in self.complete_values():
86 | raise ValueError("no project archive for this name")
87 | elif project in super(Load, self).complete_values():
88 | raise ValueError("a project with the same name already exists")
89 |
90 |
91 | class Select(ProjectRootCommand):
92 | """ Select a project (create if it does not exist) """
93 | def complete_values(self):
94 | return Load().complete_values() + super(Select, self).complete_values()
95 |
96 | def run(self, project):
97 | p = self.workspace.joinpath(project)
98 | loader = Load()
99 | if project in loader.complete_values() and confirm("An archive with this name already exists ; "
100 | "do you want to load the archive instead ?"):
101 | loader.run(project)
102 | if not p.exists():
103 | self.logger.debug(f"Creating project '{project}'...")
104 | p.mkdir()
105 | self.logger.success(f"'{project}' created")
106 | ProjectConsole(self.console, project).start()
107 | self.config['WORKSPACE'] = str(Path(self.config['WORKSPACE']).parent)
108 |
109 | def validate(self, project):
110 | pass
111 |
112 |
113 | # ---------------------------- PROJECT-LEVEL COMMANDS --------------------------
114 | class Show(Command):
115 | """ Show project-relevant options """
116 | #FIXME
117 | level = "project"
118 | values = ["options"]
119 |
120 | def run(self, value):
121 | print_formatted_text(BorderlessTable(self.console.__class__.options, "Console options"))
122 |
123 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/recording.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from tinyscript.helpers import Path
3 |
4 | from sploitkit import Command
5 |
6 |
7 | # ---------------------------- GENERAL-PURPOSE COMMANDS ------------------------
8 | class RecordStatus(Command):
9 | """ Consult status for commands recording to a .rc file """
10 | # Rationale: recording status should be consultable from any console level
11 | aliases = ["record"]
12 | alias_only = True
13 | except_levels = ["session"]
14 | values = ["status"]
15 |
16 | def run(self, status):
17 | self.logger.info(f"Recording is {['disabled', 'enabled'][self.recorder.enabled]}")
18 |
19 |
20 | # ------------------------------ ROOT-LEVEL COMMANDS ---------------------------
21 | class RootProjectCommand(Command):
22 | """ Proxy class (for setting the level attribute). """
23 | level = ["root", "project"]
24 |
25 |
26 | class Record(RootProjectCommand):
27 | """ Start/stop or consult status of commands recording to a .rc file """
28 | # Rationale: recording start/stop is only triggerable from the root level
29 | keys = ["start", "stop", "status"]
30 |
31 | def complete_values(self, key=None):
32 | if key == "start":
33 | return [x.name for x in Path(self.workspace).iterfiles(".rc")]
34 |
35 | def run(self, key, rcfile=None):
36 | if key == "start":
37 | self.recorder.start(str(Path(self.workspace).joinpath(rcfile)))
38 | elif key == "stop":
39 | self.recorder.stop()
40 | elif key == "status":
41 | self.logger.info(f"Recording is {['disabled', 'enabled'][self.recorder.enabled]}")
42 |
43 | def validate(self, key, rcfile=None):
44 | if key == "start":
45 | if rcfile is None:
46 | raise ValueError("please enter a filename")
47 | if Path(self.workspace).joinpath(rcfile).exists():
48 | raise ValueError("a file with the same name already exists")
49 | elif key in ["stop", "status"]:
50 | if rcfile is not None:
51 | raise ValueError("this key takes no value")
52 |
53 |
54 | class Replay(RootProjectCommand):
55 | """ Execute commands from a .rc file """
56 | def complete_values(self, key=None):
57 | return [x.name for x in Path(self.workspace).iterfiles(".rc")]
58 |
59 | def run(self, rcfile):
60 | self.logger.debug(f"Replaying commands from file '{rcfile}'...")
61 | self.console.replay(rcfile)
62 |
63 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/root.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 |
4 |
5 | # ----------------------- GENERAL-PURPOSE ROOT-LEVEL COMMANDS ------------------
6 | class Help(Command):
7 | """ Display help (commands or individual command/module) """
8 | level = "root"
9 | keys = ["command"]
10 |
11 | def __init__(self):
12 | if len(Module.modules) > 0 and "module" not in self.keys:
13 | self.keys += ["module"]
14 |
15 | def complete_values(self, category):
16 | if category == "command":
17 | return self.console.commands.keys()
18 | elif category == "module":
19 | return sorted([x.fullpath for x in Module.subclasses])
20 |
21 | def run(self, category=None, value=None):
22 | if category is None:
23 | print_formatted_text(Command.get_help(except_levels="module"))
24 | elif category == "command":
25 | print_formatted_text(self.console.commands[value].help(value))
26 | elif category == "module":
27 | print_formatted_text(self.modules[value].help)
28 |
29 | def validate(self, category=None, value=None):
30 | if category is None and value is None:
31 | return
32 | super(Help, self).validate(category, value)
33 |
34 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/session.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 |
4 |
5 | # ----------------------------- SUBCONSOLE DEFINITION --------------------------
6 | class SessionConsole(Console):
7 | """ Session subconsole definition. """
8 | level = "session"
9 | message_reset = True
10 | message = [
11 | ('class:session', None),
12 | ('class:prompt', ">"),
13 | ]
14 | style = {
15 | 'prompt': "#eeeeee",
16 | 'session': "#00ff00",
17 | }
18 |
19 | def __init__(self, parent, session_id):
20 | session = parent._sessions[session_id]
21 | self.logname = "session-%d" % session_id
22 | self.message[0] = ('class:prompt', session.name)
23 | super(SessionConsole, self).__init__(parent, fail=False)
24 | self.config.prefix = "Module"
25 |
26 |
27 | # ---------------------------- SESSION-RELATED COMMANDS ------------------------
28 | class Background(Command):
29 | """ Put the current session to the background """
30 | level = "session"
31 |
32 | def run(self):
33 | # do something with the session
34 | raise ConsoleExit
35 |
36 |
37 | class Session(Command):
38 | """ Resume an open session """
39 | except_levels = ["session"]
40 | #requirements = {'internal': lambda s: len(s.console._sessions) > 0}
41 |
42 | def complete_values(self):
43 | return list(range(len(self.console._sessions)))
44 |
45 | def run(self, session_id):
46 | SessionConsole(self.console, session_id).start()
47 |
48 |
--------------------------------------------------------------------------------
/src/sploitkit/base/commands/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import os
3 | import stat
4 | import yaml
5 | from collections.abc import Iterable
6 | from gc import collect, get_objects, get_referrers
7 | from subprocess import call
8 | from sys import getrefcount
9 | from tinyscript.helpers import human_readable_size, parse_docstring, pprint, BorderlessTable, Capture, Path
10 |
11 | from sploitkit import *
12 | from sploitkit.core.components import BACK_REFERENCES
13 | from sploitkit.core.entity import load_entities
14 |
15 |
16 | # ---------------------------- GENERAL-PURPOSE COMMANDS ------------------------
17 | class Edit(Command):
18 | """ Edit a text file """
19 | except_levels = ["session"]
20 | requirements = {'system': ["vim"]}
21 | single_arg = True
22 |
23 | def check_requirements(self):
24 | return self.config.option("TEXT_EDITOR").value is not None
25 |
26 | def complete_values(self):
27 | p = Path(self.config.option("WORKSPACE").value)
28 | f = p.iterfiles(relative=True)
29 | return list(map(lambda x: str(x), f))
30 |
31 | def run(self, filename):
32 | f = Path(self.config.option("WORKSPACE").value).joinpath(filename)
33 | self.console._files.edit(str(f))
34 |
35 | def validate(self, filename):
36 | return
37 |
38 |
39 | class History(Command):
40 | """ Inspect commands history """
41 | except_levels = ["session"]
42 | requirements = {'system': ["less"]}
43 |
44 | def run(self):
45 | h = Path(self.config.option("WORKSPACE").value).joinpath("history")
46 | self.console._files.page(str(h))
47 |
48 |
49 | class Shell(Command):
50 | """ Execute a shell command """
51 | except_levels = ["session"]
52 | single_arg = True
53 |
54 | def complete_values(self):
55 | l = []
56 | e = stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
57 | for p in os.environ['PATH'].split(":"):
58 | if not os.path.isdir(p):
59 | continue
60 | for f in os.listdir(p):
61 | fp = os.path.join(p, f)
62 | if os.path.isfile(fp):
63 | st = os.stat(fp)
64 | if st.st_mode & e and f not in l:
65 | l.append(f)
66 | return l
67 |
68 | def run(self, cmd=None):
69 | if cmd is None:
70 | from pty import spawn
71 | spawn("/bin/bash")
72 | else:
73 | call(cmd, shell=True)
74 | print_formatted_text("")
75 |
76 | def validate(self, cmd):
77 | _ = cmd.split()
78 | if len(_) <= 1 and _[0] not in self.complete_values():
79 | raise ValueError("bad shell command")
80 |
81 |
82 | class Stats(Command):
83 | """ Display console's statistics """
84 | level = "root"
85 |
86 | def run(self):
87 | d = [["Item", "Path", "Size"]]
88 | p = self.console.app_folder
89 | d.append(["APP_FOLDER", str(p), human_readable_size(p.size)])
90 | p = self.workspace
91 | d.append(["WORKSPACE", str(p), human_readable_size(p.size)])
92 | t = BorderlessTable(d, "Statistics")
93 | print_formatted_text(t.table)
94 |
95 |
96 | # ------------------------------- DEBUGGING COMMANDS ---------------------------
97 | class DebugCommand(Command):
98 | """ Proxy class for development commands """
99 | except_levels = ["session"]
100 | requirements = {'config': {'DEBUG': True}}
101 |
102 |
103 | class Logs(DebugCommand):
104 | """ Inspect console logs """
105 | requirements = {'system': ["less"]}
106 |
107 | def run(self):
108 | self.console._files.page(self.logger.__logfile__)
109 |
110 |
111 | class Pydbg(DebugCommand):
112 | """ Start a Python debugger session """
113 | requirements = {'python': ["pdb"]}
114 |
115 | def run(self):
116 | import pdb
117 | pdb.set_trace()
118 |
119 |
120 | class State(DebugCommand):
121 | """ Display console's shared state """
122 | def run(self):
123 | for k, v in self.console.state.items():
124 | print_formatted_text(f"\n{k}:")
125 | v = v or ""
126 | if len(v) == 0:
127 | continue
128 | if isinstance(v, Iterable):
129 | if isinstance(v, dict):
130 | v = dict(**v)
131 | for l in yaml.dump(v).split("\n"):
132 | if len(l.strip()) == 0:
133 | continue
134 | print_formatted_text(" " + l)
135 | else:
136 | print_formatted_text(v)
137 | print_formatted_text("")
138 |
139 |
140 | # ------------------------------ DEVELOPMENT COMMANDS --------------------------
141 | class DevCommand(DebugCommand):
142 | """ Proxy class for development commands """
143 | def condition(self):
144 | return getattr(Console, "_dev_mode", False)
145 |
146 |
147 | class Collect(DevCommand):
148 | """ Garbage-collect """
149 | def run(self):
150 | collect()
151 |
152 |
153 | class Dict(DevCommand):
154 | """ Show console's dictionary of attributes """
155 | def run(self):
156 | pprint(self.console.__dict__)
157 |
158 |
159 | class Memory(DevCommand):
160 | """ Inspect memory consumption """
161 | keys = ["graph", "growth", "info", "leaking", "objects", "refs"]
162 | requirements = {
163 | 'python': ["objgraph", "psutil", "xdot"],
164 | 'system': ["xdot"],
165 | }
166 |
167 | def complete_values(self, key=None):
168 | if key in ["graph", "refs"]:
169 | return [str(o) for o in get_objects() if isinstance(o, Console)]
170 |
171 | def run(self, key, value=None):
172 | if value is not None:
173 | obj = list(filter(lambda o: str(o) == value, get_objects()))[0]
174 | if key == "graph":
175 | from objgraph import show_refs
176 | if value is None:
177 | p = self.console.parent
178 | show_refs(self.console if p is None else p, refcounts=True, max_depth=3)
179 | else:
180 | show_refs(obj, refcounts=True, max_depth=3)
181 | elif key == "growth":
182 | from objgraph import get_leaking_objects, show_most_common_types
183 | show_most_common_types(objects=get_leaking_objects())
184 | elif key == "info":
185 | from psutil import Process
186 | p = Process(os.getpid())
187 | print_formatted_text(p.memory_info())
188 | elif key == "leaking":
189 | from objgraph import get_leaking_objects
190 | with Capture() as (out, err):
191 | pprint(get_leaking_objects())
192 | print_formatted_text(out)
193 | elif key == "objects":
194 | data = [["Object", "#References"]]
195 | for o in get_objects():
196 | if isinstance(o, (Console, Module)):
197 | data.append([str(o), str(getrefcount(o))])
198 | t = BorderlessTable(data, "Consoles/Modules")
199 | print_formatted_text(t.table)
200 | elif key == "refs":
201 | if value is not None:
202 | print_formatted_text(getrefcount(obj), ":")
203 | pprint(get_referrers(obj))
204 |
205 | def validate(self, key, value=None):
206 | if key in ["graph", "refs"]:
207 | if value and value not in self.complete_values("graph"):
208 | raise ValueError("bad object")
209 | elif value:
210 | raise ValueError("this key takes no value")
211 |
212 |
213 | class Reload(Command):
214 | """ Inspect memory consumption """
215 | level = "root"
216 | values = ["commands", "modules", "models"]
217 |
218 | def condition(self):
219 | return getattr(Console, "_dev_mode", False)
220 |
221 | def run(self, value):
222 | load_entities([globals()[value[:-1].capitalize()]],
223 | *([self.console._root] + self.console._sources("entities")), **self.console._load_kwargs)
224 |
225 |
--------------------------------------------------------------------------------
/src/sploitkit/base/config.conf:
--------------------------------------------------------------------------------
1 | [main]
2 | projects_folder = ~/Projects
3 |
--------------------------------------------------------------------------------
/src/sploitkit/base/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/src/sploitkit/base/models/__init__.py
--------------------------------------------------------------------------------
/src/sploitkit/base/models/notes.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 | from sploitkit.base.models.organization import Organization
4 | from sploitkit.base.models.systems import Host
5 | from sploitkit.base.models.users import User
6 |
7 |
8 | class Note(Model):
9 | content = TextField()
10 |
11 |
12 | class OrganizationNote(BaseModel):
13 | organization = ForeignKeyField(Organization)
14 | note = ForeignKeyField(Note)
15 |
16 | class Meta:
17 | primary_key = CompositeKey("organization", "note")
18 |
19 |
20 | class NoteHost(BaseModel):
21 | host = ForeignKeyField(Host)
22 | note = ForeignKeyField(Note)
23 |
24 | class Meta:
25 | primary_key = CompositeKey("host", "note")
26 |
27 |
28 | class NoteUser(BaseModel):
29 | user = ForeignKeyField(User)
30 | note = ForeignKeyField(Note)
31 |
32 | class Meta:
33 | primary_key = CompositeKey("user", "note")
34 |
35 |
--------------------------------------------------------------------------------
/src/sploitkit/base/models/organization.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 | from sploitkit.base.models.users import Email, User
4 |
5 |
6 | class Organization(Model):
7 | name = CharField(primary_key=True)
8 |
9 |
10 | class Unit(Model):
11 | name = CharField(primary_key=True)
12 | organization = ForeignKeyField(Organization, backref="units")
13 |
14 |
15 | class Employee(Model):
16 | firstname = CharField()
17 | lastname = CharField()
18 | role = CharField()
19 | title = CharField()
20 |
21 | class Meta:
22 | indexes = ((("firstname", "lastname", "role"), True),)
23 |
24 | @property
25 | def fullname(self):
26 | return f"{self.firstname} {self.lastname} ({self.role})"
27 |
28 |
29 | class EmployeeUnit(BaseModel):
30 | employee = ForeignKeyField(Employee, backref="units")
31 | unit = ForeignKeyField(Unit, backref="employees")
32 |
33 | class Meta:
34 | primary_key = CompositeKey("employee", "unit")
35 |
36 |
37 | class EmployeeEmail(BaseModel):
38 | employee = ForeignKeyField(Employee, backref="emails")
39 | email = ForeignKeyField(Email, backref="employees")
40 |
41 | class Meta:
42 | primary_key = CompositeKey("employee", "email")
43 |
44 |
45 | class EmployeeUser(BaseModel):
46 | employee = ForeignKeyField(Employee, backref="users")
47 | user = ForeignKeyField(User, backref="employees")
48 |
49 | class Meta:
50 | primary_key = CompositeKey("employee", "user")
51 |
52 |
--------------------------------------------------------------------------------
/src/sploitkit/base/models/systems.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 |
4 |
5 | class Host(Model):
6 | hostname = CharField(max_length=256)
7 | ip = IPAddressField()
8 | mac = MACAddressField()
9 | os = CharField()
10 | location = CharField()
11 |
12 | class Meta:
13 | indexes = ((("hostname", "ip", "mac"), True),)
14 |
15 |
16 | class Port(Model):
17 | number = IntegerField(primary_key=True)
18 | status = BooleanField()
19 |
20 |
21 | class Service(Model):
22 | name = CharField(primary_key=True)
23 |
24 |
25 | class HostPort(BaseModel):
26 | host = ForeignKeyField(Host, backref="ports")
27 | port = ForeignKeyField(Port, backref="hosts")
28 |
29 | class Meta:
30 | primary_key = CompositeKey("host", "port")
31 |
32 |
33 | class ServicePort(BaseModel):
34 | service = ForeignKeyField(Service, backref="ports")
35 | port = ForeignKeyField(Port, backref="services")
36 |
37 | class Meta:
38 | primary_key = CompositeKey("service", "port")
39 |
40 |
--------------------------------------------------------------------------------
/src/sploitkit/base/models/users.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from sploitkit import *
3 |
4 |
5 | class User(Model):
6 | username = CharField(primary_key=True)
7 |
8 |
9 | class Email(Model):
10 | address = CharField(primary_key=True, max_length=320)
11 |
12 |
13 | class Password(Model):
14 | hash = CharField(primary_key=True)
15 | plain = CharField()
16 |
17 |
18 | class UserEmail(BaseModel):
19 | user = ForeignKeyField(User, backref="emails")
20 | email = ForeignKeyField(Email, backref="users")
21 |
22 | class Meta:
23 | primary_key = CompositeKey("user", "email")
24 |
25 |
26 | class UserPassword(BaseModel):
27 | user = ForeignKeyField(User, backref="passwords")
28 | password = ForeignKeyField(Password, backref="users")
29 |
30 | class Meta:
31 | primary_key = CompositeKey("user", "password")
32 |
33 |
34 | #TODO: to be tested
35 | #class UsersStorage(StoreExtension):
36 | # def set_user(self, username):
37 | # User.get_or_create(username=username).execute()
38 |
39 |
40 | #TODO: to be tested
41 | #class PasswordsStorage(StoreExtension):
42 | # def set_password(self, password):
43 | # Password.insert(password=password).execute()
44 |
45 |
--------------------------------------------------------------------------------
/src/sploitkit/core/__init__.py:
--------------------------------------------------------------------------------
1 | from .console import *
2 | from .console import __all__
3 |
4 |
--------------------------------------------------------------------------------
/src/sploitkit/core/application.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from prompt_toolkit.application import Application
3 | from prompt_toolkit.layout.containers import HSplit, Window
4 | from prompt_toolkit.layout.layout import Layout
5 |
6 |
7 | __all__ = ["FrameworkApp"]
8 |
9 |
10 | #TODO: find a way to embed the Console instance (started with .start()) into FrameworkApp
11 | class FrameworkApp(Application):
12 | def __init__(self, *args, **kwargs):
13 | console = kwargs.get('console')
14 | if console is None:
15 | raise Exception("No root console passed to the application")
16 | #console.__class__ = type("ConsoleTextArea", (TextArea, console.__class__), {})
17 | #console.scrollbar = True
18 | root_container = HSplit([
19 | console,
20 | ])
21 | kwargs['layout'] = Layout(root_container, focused_element=console)
22 | super(FrameworkApp, self).__init__(*args, **kwargs)
23 |
24 |
--------------------------------------------------------------------------------
/src/sploitkit/core/command.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import re
3 | from inspect import getfullargspec
4 | from tinyscript.helpers import failsafe, BorderlessTable, Path, PythonPath
5 |
6 | from .components.config import Config
7 | from .components.logger import get_logger
8 | from .entity import Entity, MetaEntity
9 |
10 |
11 | __all__ = ["Command"]
12 |
13 |
14 | COMMAND_STYLES = [
15 | "lowercase", # ClassName => classname
16 | "none", # ClassName => ClassName
17 | "powershell", # ClassName => Class-Name
18 | "slugified", # ClassName => class-name
19 | "uppercase" # ClassName => CLASSNAME
20 | ]
21 | """
22 | Usage:
23 | >>> from sploitkit import Command
24 | >>> Command.set_style("powershell")
25 | """
26 | FUNCTIONALITIES = [
27 | "general", # commands for every level
28 | "utils", # utility commands (for every level)
29 | "recording", # recording commands (for every level)
30 | "root", # base root-level commands
31 | "project", # base project-level commands
32 | "module", # base module-level commands
33 | "session", # base session-level commands
34 | ]
35 |
36 |
37 | logger = get_logger("core.command")
38 |
39 |
40 | class MetaCommand(MetaEntity):
41 | """ Metaclass of a Command. """
42 | _inherit_metadata = True
43 | style = "slugified"
44 |
45 | def __init__(self, *args):
46 | argspec = getfullargspec(self.run)
47 | s, args, defs = "{}", argspec.args[1:], argspec.defaults
48 | for a in args[:len(args)-len(defs or [])]:
49 | s += " " + a
50 | if len(defs or []) > 0:
51 | s += " ["
52 | i = []
53 | for a, d in zip(args[len(args)-len(defs):], defs):
54 | i.append(f"{a}={d}" if d is not None else a)
55 | s += " ".join(i) + "]"
56 | self.signature = s
57 | self.args, self.defaults = args, defs
58 |
59 | def help(self, alias=None):
60 | """ Help message for the command. """
61 | return self.get_info(("name", "description"), "comments")
62 |
63 | @property
64 | def config(self):
65 | """ Shortcut to bound console's config instance. """
66 | try:
67 | return self.console.config
68 | except AttributeError:
69 | return Config()
70 |
71 | @property
72 | def name(self):
73 | """ Command name, according to the defined style. """
74 | n = self.__name__
75 | if self.style == "lowercase":
76 | n = n.lower()
77 | elif self.style in ["powershell", "slugified"]:
78 | n = re.sub(r'(.)([A-Z][a-z]+)', r'\1-\2', n)
79 | n = re.sub(r'([a-z0-9])([A-Z])', r'\1-\2', n)
80 | n = n.lower() if self.style == "slugified" else n
81 | elif self.style == "uppercase":
82 | n = n.upper()
83 | return n
84 |
85 |
86 | class Command(Entity, metaclass=MetaCommand):
87 | """ Main class handling console commands. """
88 | # convention: mangled attributes should not be customized when subclassing Command...
89 | _functionalities = FUNCTIONALITIES
90 | _levels = []
91 | # ... by opposition to public class attributes that can be tuned
92 | aliases = []
93 | alias_only = False
94 | commands = {}
95 | level = "general"
96 | except_levels = []
97 | single_arg = False
98 |
99 | @property
100 | def _nargs(self):
101 | """ Get run's signature info (n = number of args, m = number of args with no default). """
102 | argspec = getfullargspec(self.run)
103 | n = len(argspec.args) - 1 # substract 1 for self
104 | return n, n - len(argspec.defaults or ())
105 |
106 | @property
107 | def app_folder(self):
108 | """ Shortcut to the current application folder. """
109 | return self.console.app_folder
110 |
111 | @property
112 | def config(self):
113 | """ Shortcut to bound console's config instance. """
114 | return self.module.config if hasattr(self, "module") and self.module is not None else \
115 | self.__class__.console.__class__.config
116 |
117 | @property
118 | def files(self):
119 | """ Shortcut to bound console's file manager instance. """
120 | return self.console.__class__._files
121 |
122 | @property
123 | def logger(self):
124 | """ Shortcut to bound console's logger instance. """
125 | return self.console.logger
126 |
127 | @property
128 | @failsafe
129 | def module(self):
130 | """ Shortcut to bound console's module class. """
131 | return self.console.module
132 |
133 | @property
134 | def modules(self):
135 | """ Shortcut to list of registered modules. """
136 | return self.console.modules
137 |
138 | @property
139 | def recorder(self):
140 | """ Shortcut to global command recorder. """
141 | return self.console.__class__._recorder
142 |
143 | @property
144 | def workspace(self):
145 | """ Shortcut to the current workspace. """
146 | return self.console.workspace
147 |
148 | @classmethod
149 | def check_applicability(cls):
150 | """ Check for Command's applicability. """
151 | a = getattr(cls, "applies_to", [])
152 | return len(a) == 0 or not hasattr(cls, "console") or cls.console.module.fullpath in a
153 |
154 | @classmethod
155 | def get_help(cls, *levels, **kwargs):
156 | """ Display commands' help(s), using its metaclass' properties. """
157 | if len(levels) == 0:
158 | levels = Command._levels
159 | if len(levels) == 2 and "general" in levels:
160 | # process a new dictionary of commands, handling levels in order
161 | _ = {}
162 | for l in levels:
163 | for n, c in cls.commands.get(l, {}).items():
164 | if c.level != "general" or all(l not in levels for l in c.except_levels):
165 | _[n] = c
166 | # then rebuild the dictionary by levels from this dictionary
167 | levels = {"general": {}, "specific": {}}
168 | for n, c in _.items():
169 | levels[["specific", "general"][c.level == "general"]][n] = c
170 | else:
171 | _, levels = levels, {}
172 | for l in _:
173 | levels[l] = cls.commands[l]
174 | # now make the help with tables of command name-descriptions by level
175 | s, i = "", 0
176 | for l, cmds in sorted(levels.items(), key=lambda x: x[0]):
177 | if len(cmds) == 0 or l in kwargs.get('except_levels', []):
178 | continue
179 | d = [["Command", "Description"]]
180 | for n, c in sorted(cmds.items(), key=lambda x: x[0]):
181 | if not hasattr(c, "console") or not c.check():
182 | continue
183 | d.append([n, getattr(c, "description", "")])
184 | if len(d) > 1:
185 | t = BorderlessTable(d, f"{l.capitalize()} commands")
186 | s += t.table + "\n"
187 | i += 1
188 | return "\n" + s.strip() + "\n" if i > 0 else ""
189 |
190 | @classmethod
191 | def register_command(cls, subcls):
192 | """ Register the command and its aliases in a dictionary according to its level. """
193 | l = subcls.level
194 | levels = [l] if not isinstance(l, (list, tuple)) else l
195 | for l in levels:
196 | Command.commands.setdefault(l, {})
197 | if l not in Command._levels:
198 | Command._levels.append(l)
199 | if not subcls.alias_only:
200 | Command.commands[l][subcls.name] = subcls
201 | for alias in subcls.aliases:
202 | Command.commands[l][alias] = subcls
203 | logger.detail(f"Registered command alias '{alias}'")
204 |
205 | @classmethod
206 | def set_style(cls, style):
207 | """ Set the style of command name. """
208 | if style not in COMMAND_STYLES:
209 | raise ValueError(f"Command style must be one of the followings: [{'|'.join(COMMAND_STYLES)}]")
210 | MetaCommand.style = style
211 |
212 | @classmethod
213 | def unregister_command(cls, subcls):
214 | """ Unregister a command class from the subclasses and the commands dictionary. """
215 | _ = subcls.level
216 | levels = [_] if not isinstance(_, (list, tuple)) else _
217 | n = subcls.name
218 | # remove every reference in commands dictionary
219 | for l in levels:
220 | for n in [n] + subcls.aliases:
221 | del Command.commands[l][n]
222 | # remove the subclass instance from the subclasses registry
223 | try:
224 | Command.subclasses.remove(subcls)
225 | except ValueError:
226 | pass
227 | # remove the subclass from the global namespace (if not Command itself)
228 | if subcls is not Command:
229 | try:
230 | del globals()[subcls.__name__]
231 | except KeyError:
232 | pass # subcls may be a proxy Command-inherited class
233 | # if the level of commands is become empty, remove it
234 | for l in levels:
235 | if len(Command.commands[l]) == 0:
236 | del Command.commands[l]
237 | logger.detail(f"Unregistered command '{l}/{n}'")
238 |
239 | @classmethod
240 | def unregister_commands(cls, *identifiers):
241 | """ Unregister items from Command based on their 'identifiers' (functionality or level/name). """
242 | for i in identifiers:
243 | _ = i.split("/", 1)
244 | try:
245 | l, n = _ # level, name
246 | except ValueError:
247 | f, n = _[0], None # functionality
248 | # apply deletions
249 | if n is None:
250 | if f not in cls._functionalities:
251 | raise ValueError(f"Unknown functionality {f}")
252 | p = Path(__file__).parent.joinpath("../base/commands/" + f + ".py").resolve()
253 | for c in PythonPath(str(p)).get_classes(Command):
254 | Command.unregister_command(c)
255 | else:
256 | try:
257 | c = Command.commands[l][n]
258 | Command.unregister_command(c)
259 | except KeyError:
260 | pass
261 |
262 | def _complete_keys(self, *args, **kwargs):
263 | """ Key completion executed method. """
264 | self.set_keys(*args, **kwargs)
265 | return self.complete_keys(*args, **kwargs)
266 |
267 | def _complete_values(self, *args, **kwargs):
268 | """ Value completion executed method. """
269 | self.set_values(*args, **kwargs)
270 | return self.complete_values(*args, **kwargs)
271 |
272 | def _validate(self, *args):
273 | """ Value completion executed method. """
274 | self.set_keys()
275 | self.set_values(*args[:1])
276 | self.validate(*args)
277 |
278 | def complete_keys(self):
279 | """ Default key completion method (will be triggered if the number of run arguments is 2). """
280 | return getattr(self, "keys", []) or list(getattr(self, "values", {}).keys())
281 |
282 | def complete_values(self, key=None):
283 | """ Default value completion method. """
284 | if self._nargs[0] == 1:
285 | if key is not None:
286 | raise TypeError("complete_values() takes 1 positional argument but 2 were given")
287 | return getattr(self, "values", [])
288 | if self._nargs[0] == 2:
289 | return getattr(self, "values", {}).get(key)
290 | return []
291 |
292 | def set_keys(self):
293 | """ Default key setting method. """
294 | pass
295 |
296 | def set_values(self, key=None):
297 | """ Default value setting method. """
298 | pass
299 |
300 | def validate(self, *args):
301 | """ Default validation method. """
302 | # check for the signature and, if relevant, validating keys and values
303 | n_in = len(args)
304 | n, m = self._nargs
305 | if n_in < m or n_in > n:
306 | pargs = "from %d to %d" % (m, n) if n != m else "%d" % n
307 | raise TypeError("validate() takes %s positional argument%s but %d were given" % \
308 | (pargs, ["", "s"][n > 0], n_in))
309 | if n == 1: # command format: COMMAND VALUE
310 | l = self.complete_values() or []
311 | if n_in == 1 and len(l) > 0 and args[0] not in l:
312 | raise ValueError("invalid value")
313 | elif n == 2: # command format: COMMAND KEY VALUE
314 | l = self.complete_keys() or []
315 | if n_in > 0 and len(l) > 0 and args[0] not in l:
316 | raise ValueError("invalid key")
317 | l = self.complete_values(args[0]) or []
318 | if n_in == 2 and len(l) > 0 and args[1] not in l:
319 | raise ValueError("invalid value")
320 |
321 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/__init__.py:
--------------------------------------------------------------------------------
1 | from .completer import *
2 | from .config import *
3 | from .defaults import *
4 | from .files import *
5 | from .jobs import *
6 | from .layout import *
7 | from .logger import *
8 | from .recorder import *
9 | from .sessions import *
10 | from .store import *
11 | from .validator import *
12 |
13 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/completer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from prompt_toolkit.completion import Completer, Completion
3 |
4 |
5 | __all__ = ["CommandCompleter"]
6 |
7 |
8 | def _filter_sort(lst, prefix=None, sort=False):
9 | if sort:
10 | lst = sorted(map(str, set(lst or [])), key=lambda s: str(s).casefold())
11 | for x in lst or []:
12 | if prefix is None or x.startswith(prefix):
13 | yield x
14 |
15 |
16 | class CommandCompleter(Completer):
17 | """ Completer for console's commands and arguments. """
18 | def get_completions(self, document, complete_event):
19 | # this completion method handles the following formats:
20 | # 1) COMMAND VALUE ; e.g. create my-project
21 | # 2) COMMAND KEY VALUE ; e.g. set LHOST 192.168.1.1
22 | # first, tokenize document.text and initialize some shorcut variables
23 | d = document.text
24 | tokens = self.console._get_tokens(d)
25 | l = len(tokens)
26 | ts = len(d) - len(d.rstrip(" ")) # trailing spaces
27 | try:
28 | cmd, t1, t2 = tokens + [None] * (3 - l)
29 | except: # occurs when l > 3 ; no need to complete anything as it corresponds to an invalid command
30 | return
31 | bc = len(document.text_before_cursor)
32 | it = len(d) - bc > 0
33 | o1 = len(cmd) + 1 - bc if cmd else 0
34 | o2 = len(cmd) + len(t1 or "") + 2 - bc if cmd and t2 else 0
35 | cmds = {k: v for k, v in self.console.commands.items()}
36 | c = cmds[cmd]._instance if cmd in cmds else None
37 | nargs = len(c.args) if c is not None else 0
38 | # then handle tokens ;
39 | # when no token is provided, just yield the list of available commands
40 | if l == 0:
41 | for x in _filter_sort(cmds.keys(), sort=True):
42 | yield Completion(x, start_position=0)
43 | # when one token is provided, handle format:
44 | # [PARTIAL_]COMMAND ...
45 | elif l == 1:
46 | # when a partial token is provided, yield the list of valid commands
47 | if ts == 0 and c not in cmds:
48 | for x in _filter_sort(cmds, cmd, True):
49 | yield Completion(x, start_position=-bc)
50 | # when a valid command is provided, yield the list of valid keys or values, depending on the type of command
51 | elif ts > 0 and c is not None:
52 | if nargs == 1: # COMMAND VALUE
53 | for x in _filter_sort(c._complete_values(), sort=True):
54 | yield Completion(x, start_position=0)
55 | # e.g. set ---> ["WORKSPACE", ...]
56 | elif nargs == 2: # COMMAND KEY VALUE
57 | for x in _filter_sort(c._complete_keys(), sort=True):
58 | yield Completion(x, start_position=0)
59 | # when two tokens are provided, handle format:
60 | # COMMAND [PARTIAL_](KEY ...|VALUE)
61 | elif l == 2 and c is not None:
62 | # when a partial value token is given, yield the list of valid ones
63 | # e.g. select my-pro ---> ["my-project", ...]
64 | if nargs == 1 and ts == 0:
65 | for x in _filter_sort(c._complete_values(), t1, True):
66 | yield Completion(x, start_position=o1)
67 | # when a partial key token is given, yield the list of valid ones
68 | # e.g. set W ---> ["WORKSPACE"]
69 | elif nargs == 2 and ts == 0:
70 | for x in _filter_sort(c._complete_keys(), t1, True):
71 | yield Completion(x, start_position=o1)
72 | # when a valid key token is given, yield the list of values
73 | # e.g. set WORKSPACE ---> ["/home/user/...", "..."]
74 | elif nargs == 2 and ts > 0 and t1 in c._complete_keys():
75 | for x in _filter_sort(c._complete_values(t1), sort=True):
76 | yield Completion(x, start_position=0)
77 | # when three tokens are provided, handle format:
78 | # COMMAND KEY [PARTIAL_]VALUE
79 | elif l == 3 and c is not None and t1 in c._complete_keys():
80 | if nargs == 2 and ts == 0:
81 | for x in _filter_sort(c._complete_values(t1), t2, True):
82 | yield Completion(x, start_position=o2)
83 | # handle no other format
84 |
85 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from .logger import *
3 |
4 |
5 | __all__ = ["Config", "Option", "ProxyConfig", "ROption"]
6 |
7 |
8 | logger = get_logger("core.components.config")
9 |
10 |
11 | class Config(dict):
12 | """ Enhanced dictionary for handling Option instances as its keys. """
13 | def __init__(self, *args, **kwargs):
14 | self._d = {}
15 | if not hasattr(Config, "_g"):
16 | Config._g = {}
17 | # this will set options for this config, that is, creating NEW Option instances based on the given ones
18 | self.update(*args, **kwargs)
19 |
20 | def __add__(self, config):
21 | """ Method for appending another config. """
22 | return ProxyConfig() + self + config
23 |
24 | def __delitem__(self, key):
25 | """ Custom method for deleting an item, for triggering an unset callback from an Option. """
26 | try:
27 | l = self.console.logger
28 | except:
29 | l = null_logger
30 | key = self._getkey(key)
31 | self[key] = getattr(self, "default", None)
32 | self.__run_callback(key, "unset")
33 | if key._reset:
34 | try:
35 | self.console.reset()
36 | except AttributeError as err:
37 | pass
38 | l.debug(f"{key.name} => null")
39 |
40 | def __getitem__(self, key):
41 | """ Custom method for getting an item, returning the original value from the current Config instance or, if the
42 | key does not exist and this instance has a parent, try to get it from the parent. """
43 | try: # search first in the private dictionary
44 | return self._getitem(key)
45 | except KeyError:
46 | pass
47 | try: # then search in the parent ProxyConfig
48 | return self.parent[key]
49 | except (AttributeError, KeyError):
50 | pass
51 | try: # finally search in the config of the parent console
52 | return self.console.parent.config[key]
53 | except (AttributeError, KeyError):
54 | pass
55 | raise KeyError(key)
56 |
57 | def __setitem__(self, key, value):
58 | """ Custom method for setting an item, keeping the original value in a private dictionary. """
59 | try:
60 | l = self.console.logger
61 | except AttributeError:
62 | l = null_logger
63 | if isinstance(key, (list, tuple)) and len(key) == 2 and isinstance(key[0], str) and isinstance(key[1], bool):
64 | key, force = key
65 | else:
66 | force = False
67 | key = self._setkey(key, value)
68 | if not force and key.old_value == key.value:
69 | try:
70 | l.debug(f"{key.name} unchanged")
71 | except AttributeError:
72 | pass
73 | return # stop here if the final value is unchanged
74 | # when the value is validated and assigned, run the callback function
75 | self.__run_callback(key, "set")
76 | if key._reset:
77 | try:
78 | self.console.reset()
79 | except AttributeError as err:
80 | pass
81 | l.success(f"{key.name} => {value if force else key.value}")
82 |
83 | def __str__(self):
84 | """ Custom string method. """
85 | data = [["Name", "Value", "Required", "Description"]]
86 | l = len(list(self.items(False)))
87 | for n, d, v, r in sorted(self.items(False), key=lambda x: x[0]):
88 | if v is None and l > 1:
89 | continue
90 | r = ["N", "Y"][r]
91 | if v == "":
92 | from tinyscript.helpers import colored
93 | n, v, r = map(lambda s: colored(s, "red", attrs=['bold']), [n, v, r])
94 | data.append([n, v, r, d])
95 | if len(data) > 1:
96 | from tinyscript.helpers import BorderlessTable
97 | try:
98 | prefix = self.console.opt_prefix
99 | except AttributeError:
100 | prefix = None
101 | return BorderlessTable(data).table if prefix is None else \
102 | BorderlessTable(data, f"{prefix} options").table
103 | return ""
104 |
105 | def __run_callback(self, key, name):
106 | """ Method for executing a callback and updating the current value with its return value if any. """
107 | logger.detail(f"{key} {name} callback triggered")
108 | retval = None
109 | if hasattr(self, "_last_error"):
110 | del self._last_error
111 | try:
112 | retval = getattr(key, f"{name}_callback")()
113 | except Exception as e:
114 | self._last_error = e
115 | if True:#not isinstance(e, AttributeError):
116 | raise
117 | if retval is not None:
118 | key.old_value = key.value
119 | if not key.validate(retval):
120 | raise ValueError(f"Invalid value '{retval}'")
121 | self._d[key.name] = (key, retval)
122 |
123 | def _getitem(self, key):
124 | """ Custom method for getting an item, returning the original value from the current Config instance. """
125 | return self._d[key.name if isinstance(key, Option) else key][1]
126 |
127 | def _getkey(self, key):
128 | """ Proxy method for ensuring that the key is an Option instance. """
129 | if not isinstance(key, Option):
130 | try:
131 | key = self.option(key)
132 | except KeyError:
133 | if not isinstance(key, tuple):
134 | key = (key,)
135 | key = Option(*key)
136 | return key
137 |
138 | def _getoption(self, key):
139 | """ Return Option instance from key. """
140 | return self._d[key.name if isinstance(key, Option) else key][0]
141 |
142 | def _setkey(self, key, value):
143 | """ Proxy method for setting a key-value as a validated Option instance. """
144 | key = tmp = self._getkey(key)
145 | # get an existing instance or the new one
146 | key = key.bind(self if not hasattr(key, "config") else key.config)
147 | if tmp is not key:
148 | del tmp # if an instance already existed, remove the new one
149 | key.config._setkey(key, value)
150 | return key
151 | # keep track of the previous value
152 | try:
153 | key.old_value = key.value
154 | except (KeyError, ValueError):
155 | key.old_value = None
156 | # then assign the new one if it is valid
157 | self._d[key.name] = (key, value)
158 | if value is not None and not key.validate(value):
159 | raise ValueError(f"Invalid value '{value}' for key '{key.name}'")
160 | super(Config, self).__setitem__(key, value)
161 | return key
162 |
163 | def copy(self, config, key):
164 | """ Copy an option based on its key from another Config instance. """
165 | self[config.option(key)] = config[key]
166 |
167 | def items(self, fail=True):
168 | """ Return (key, descr, value, required) instead of (key, value). """
169 | for o in sorted(self, key=lambda x: x.name):
170 | try:
171 | n = str(o.name)
172 | v = o.value
173 | except ValueError as e:
174 | if fail:
175 | raise e
176 | v = ""
177 | yield n, o.description or "", v, o.required
178 |
179 | def keys(self, glob=False):
180 | """ Return string keys (like original dict). """
181 | from itertools import chain
182 | l = [k for k in self._d.keys()]
183 | if glob:
184 | for k in chain(self._d.keys(), Config._g.keys()):
185 | try:
186 | getattr(l, ["remove", "append"][self.option(k).glob])(k)
187 | except KeyError:
188 | pass
189 | for k in sorted(l):
190 | yield k
191 |
192 | def option(self, key):
193 | """ Return Option instance from key, also searching for this in parent configs. """
194 | try: # search first in the private dictionary
195 | return self._getoption(key)
196 | except KeyError:
197 | pass
198 | try: # then search in the parent ProxyConfig
199 | return self.parent.option(key)
200 | except (AttributeError, KeyError):
201 | pass
202 | try: # finally search in the config of the parent console
203 | return self.console.parent.config.option(key)
204 | except (AttributeError, KeyError):
205 | pass
206 | raise KeyError(key)
207 |
208 | def options(self):
209 | """ Return Option instances instead of keys. """
210 | for k in sorted(self._d.keys()):
211 | yield self._d[k][0]
212 |
213 | def setdefault(self, key, value=None):
214 | """ Custom method for forcing the use of the modified __setitem__. """
215 | if key not in self:
216 | self._setkey(key, value) # this avoids triggering callbacks (on the contrary of self[key]) !
217 | return self[key]
218 |
219 | def setglobal(self, key, value):
220 | """ Set a global key-value. """
221 | Config._g[key] = value
222 |
223 | def unsetglobal(self, key):
224 | """ Unset a global key. """
225 | del Config._g[key]
226 |
227 | def update(self, *args, **kwargs):
228 | """ Custom method for handling update of another Config and forcing the use of the modified __setitem__. """
229 | if len(args) > 0:
230 | if len(args) > 1:
231 | raise TypeError("update expected at most 1 argument, got %d" % len(args))
232 | d = args[0]
233 | for k in (d.options() if isinstance(d, Config) else d.keys() if isinstance(d, dict) else []):
234 | k = self._setkey(k, d[k]) # this avoids triggering callbacks (on the contrary of self[key]) !
235 | k.default = d[k]
236 | # important note: this way, this will cause Option instances to be bound to THIS Config instance, with their
237 | # default attribute values (description, required, ...)
238 | for k, v in kwargs.items():
239 | k, self._setkey(k, v) # this avoids triggering callbacks (on the contrary of self[key]) !
240 | k.default = v
241 |
242 | @property
243 | def bound(self):
244 | return hasattr(self, "_console") or (hasattr(self, "module") and hasattr(self.module, "console"))
245 |
246 | @property
247 | def console(self):
248 | # check first that the console is back-referenced on an attached module instance
249 | if hasattr(self, "module") and hasattr(self.module, "console"):
250 | return self.module.console
251 | # then check for a direct reference
252 | if self.bound:
253 | c = self._console
254 | return c() if isinstance(c, type(lambda:0)) else c
255 | # finally try to get it from the parent ProxyConfig
256 | if hasattr(self, "parent"):
257 | # reference the callee to let ProxyConfig.__getattribute__ avoid trying to get the console attribute from
258 | # the current config object, ending in an infinite loop
259 | self.parent._caller = self
260 | try:
261 | return self.parent.console
262 | except AttributeError:
263 | pass
264 | raise AttributeError("'Config' object has no attribute 'console'")
265 |
266 | @console.setter
267 | def console(self, value):
268 | self._console = value
269 |
270 |
271 | class Option(object):
272 | """ Class for handling an option with its parameters while using it as key for a Config dictionary. """
273 | _instances = {}
274 | _reset = False
275 | old_value = None
276 |
277 | def __init__(self, name, description=None, required=False, choices=None, suggestions=None, set_callback=None,
278 | unset_callback=None, transform=None, validate=None, glob=True):
279 | if choices is not None and suggestions is not None:
280 | raise ValueError("choices and suggestions cannot be set at the same time")
281 | self.name = name
282 | self.description = description
283 | self.required = required
284 | self.glob = glob
285 | if choices is bool or suggestions is bool:
286 | choices = ["true", "false"]
287 | self._choices = choices if choices is not None else suggestions
288 | self.__set_func(transform, "transform")
289 | if validate is None and choices is not None:
290 | validate = lambda s, v: str(v).lower() in [str(c).lower() for c in s.choices]
291 | self.__set_func(validate, "validate")
292 | self.__set_func(set_callback, "set_callback", lambda *a, **kw: None)
293 | self.__set_func(unset_callback, "unset_callback", lambda *a, **kw: None)
294 |
295 | def __repr__(self):
296 | """ Custom representation method. """
297 | return str(self)
298 |
299 | def __str__(self):
300 | """ Custom string method. """
301 | return f"<{self.name}[{'NY'[self.required]}]>"
302 |
303 | def __set_func(self, func, name, default_func=None):
304 | """ Set a function, e.g. for manipulating option's value. """
305 | if func is None:
306 | func = default_func or (lambda *a, **kw: a[-1] if len(a) > 0 else None)
307 | if isinstance(func, type(lambda:0)):
308 | setattr(self, name, func.__get__(self, self.__class__))
309 | else:
310 | raise Exception(f"Bad {name} lambda")
311 |
312 | def bind(self, parent):
313 | """ Register this instance as a key of the given Config or retrieve the already existing one. """
314 | o, i = Option._instances, id(parent)
315 | o.setdefault(i, {})
316 | if o[i].get(self.name) is None:
317 | self.config = parent
318 | o[i][self.name] = self
319 | else:
320 | o[i][self.name].config = parent
321 | return o[i][self.name]
322 |
323 | @property
324 | def choices(self):
325 | """ Pre- or lazy-computed list of choices. """
326 | from tinyscript.helpers import is_function
327 | c = self._choices
328 | if not is_function(c):
329 | return c
330 | try:
331 | return c()
332 | except TypeError:
333 | return c(self)
334 |
335 | @property
336 | def console(self):
337 | """ Shortcut to parent config's console attribute. """
338 | return self.config.console
339 |
340 | @property
341 | def input(self):
342 | """ Original input value. """
343 | if hasattr(self, "config"):
344 | return self.config[self]
345 | else:
346 | raise Exception(f"Unbound option {self.name}")
347 |
348 | @property
349 | def module(self):
350 | """ Shortcut to parent config's console bound module attribute. """
351 | return self.console.module
352 |
353 | @property
354 | def root(self):
355 | """ Shortcut to parent config's root console attribute. """
356 | return self.console.root or self.console
357 |
358 | @property
359 | def state(self):
360 | """ Shortcut to parent console's state attribute. """
361 | return self.console.state
362 |
363 | @property
364 | def value(self):
365 | """ Normalized value attribute. """
366 | value = self.input
367 | if value == getattr(self, "default", None):
368 | value = Config._g.get(self.name, value)
369 | if self.required and value is None:
370 | raise ValueError(f"{self.name} must be defined")
371 | # try to expand format variables using console's attributes
372 | from re import findall
373 | try:
374 | kw = {}
375 | for n in findall(r'\{([a-z]+)\}', str(value)):
376 | kw[n] = self.config.console.__dict__.get(n, "")
377 | try:
378 | value = value.format(**kw)
379 | except:
380 | pass
381 | except AttributeError as e: # occurs when console is not linked to config (i.e. at startup)
382 | pass
383 | # expand and resolve paths
384 | if self.name.endswith("FOLDER") or self.name.endswith("WORKSPACE"):
385 | from tinyscript.helpers import Path
386 | # this will ensure that every path is expanded
387 | value = str(Path(value, expand=True))
388 | # convert common formats to their basic types
389 | try:
390 | if value.isdigit():
391 | value = int(value)
392 | if value.lower() in ["false", "true"]:
393 | value = value.lower() == "true"
394 | except AttributeError: # occurs e.g. if value is already a bool
395 | pass
396 | # then try to transform using the user-defined function
397 | if isinstance(self.transform, type(lambda:0)) and self.transform.__name__ == (lambda:0).__name__:
398 | value = self.transform(value)
399 | return value
400 |
401 |
402 | class ProxyConfig(object):
403 | """ Proxy class for mixing multiple Config instances, keeping original references to Option instances (as they are
404 | managed based on Config's instance identifier). """
405 | def __init__(self, *args):
406 | self.__configs = []
407 | for config in args:
408 | self.append(config)
409 |
410 | def __add__(self, config):
411 | """ Method for appending another config. """
412 | self.append(config)
413 | return self
414 |
415 | def __delitem__(self, key):
416 | """ Del method removing the giving key in every bound config instance. """
417 | for c in self.configs:
418 | del c[key]
419 |
420 | def __getattribute__(self, name):
421 | """ Custom getattribute method for aggregating Config instances for some specific methods and attributes. """
422 | # try to get it from this class first
423 | try:
424 | return super(ProxyConfig, self).__getattribute__(name)
425 | except AttributeError:
426 | pass
427 | # for these methods, create an aggregated config and get its attribute
428 | # from this new instance
429 | if name in ["items", "keys", "options"]:
430 | try:
431 | c = Config()
432 | for config in self.__configs:
433 | c.update(config)
434 | except IndexError:
435 | c = Config()
436 | return c.__getattribute__(name)
437 | # for this attribute, only try to get this of the first config
438 | if name == "console":
439 | c = self.__configs[0]
440 | if c is not getattr(self, "_caller", None):
441 | if c.bound:
442 | return c.console
443 | # for any other, get the first one found from the list of configs
444 | else:
445 | for c in self.__configs:
446 | if name != "_caller" and c is getattr(self, "_caller", None):
447 | continue
448 | try:
449 | return c.__getattribute__(name)
450 | except AttributeError:
451 | continue
452 | raise AttributeError(f"'ProxyConfig' object has no attribute '{name}'")
453 |
454 | def __getitem__(self, key):
455 | """ Get method for returning the first occurrence of a key among the list of Config instances. """
456 | # search for the first config that has this key and return the value
457 | for c in self.configs:
458 | try:
459 | return c._getitem(key)
460 | except KeyError:
461 | pass
462 | # if not found, raise KeyError
463 | raise KeyError(key)
464 |
465 | def __setattr__(self, name, value):
466 | """ Custom setattr method for handling the backref to a console. """
467 | if name == "console":
468 | if len(self.configs) > 0:
469 | self.configs[0].console = value
470 | else:
471 | super(ProxyConfig, self).__setattr__(name, value)
472 |
473 | def __setitem__(self, key, value):
474 | """ Set method setting a key-value pair in the right Config among the list of Config instances. First, it tries
475 | to get the option corresponding to the given key and if it exists, it sets the value. Otherwise, it sets a
476 | new key in the first Config among the list """
477 | try:
478 | c = self.option(key).config
479 | except KeyError:
480 | c = self.configs[0] if len(self.configs) > 0 else Config()
481 | return c.__setitem__(key, value)
482 |
483 | def __str__(self):
484 | """ String method for aggregating the list of Config instances. """
485 | c = Config()
486 | for config in self.configs:
487 | if not hasattr(c, "console"):
488 | try:
489 | c.console = config.console
490 | except AttributeError:
491 | pass
492 | c.update(config)
493 | return str(c)
494 |
495 | def append(self, config):
496 | """ Method for apending a config to the list (if it does not exist). """
497 | for c in ([config] if isinstance(config, Config) else config.configs):
498 | if c not in self.configs:
499 | self.configs.append(c)
500 | c.parent = self
501 |
502 | def get(self, key, default=None):
503 | """ Adapted get method (wrt Config). """
504 | try:
505 | return self[key]
506 | except KeyError:
507 | return default
508 |
509 | def option(self, key):
510 | """ Adapted option method (wrt Config). """
511 | # search for the first config that has this key and return its Option
512 | for c in self.configs:
513 | try:
514 | self[key]
515 | return c._getoption(key)
516 | except KeyError:
517 | pass
518 | # if not found, raise KeyError
519 | raise KeyError(key)
520 |
521 | @property
522 | def configs(self):
523 | return self.__configs
524 |
525 |
526 | class ROption(Option):
527 | """ Class for handling a reset option (that is, an option that triggers a console reset after change) with its
528 | parameters while using it as key for a Config dictionary. """
529 | _reset = True
530 |
531 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/defaults.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 |
3 | ROOT_LEVEL = "root" # console root level's name
4 |
5 |
6 | # list of folders from which related items are to be loaded
7 | SOURCES = {
8 | 'banners': None,
9 | 'entities': ["commands", "models", "modules"],
10 | 'libraries': ".",
11 | }
12 |
13 |
14 | # dictionary of back-references to be made on entities
15 | BACK_REFERENCES = {
16 | 'console': [("config", "console")],
17 | }
18 |
19 |
20 | # prompt message format
21 | PROMPT_FORMAT = [
22 | ('class:prompt', " > "),
23 | ]
24 |
25 |
26 | # prompt message style
27 | PROMPT_STYLE = {
28 | '': "#30b06f", # text after the prompt
29 | 'prompt': "#eeeeee", # prompt message
30 | 'appname': "#eeeeee underline", # application name
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/files.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import re
3 | import requests
4 | from ftplib import FTP, FTP_TLS
5 | from shutil import which
6 | from subprocess import call, PIPE
7 | from tinyscript.helpers import Path
8 |
9 |
10 | __all__ = ["FilesManager"]
11 |
12 |
13 | class FilesManager(dict):
14 | """ Files dictionary for registering files, if necessary downloading them using multiple supported schemes. """
15 | root_dir = "."
16 |
17 | def _file(self, locator, *args, **kwargs):
18 | """ Simple local file copier. """
19 | self[locator] = open(locator.split("://", 1)[1], 'rb')
20 |
21 | def _ftp(self, locator, *args, **kwargs):
22 | """ Simple FTP downloader. """
23 | scheme = locator.split("://", 1)[0]
24 | client = [FTP, FTP_TLS][scheme == "ftps"]()
25 | client.connect(kwargs.pop("host", ""), kwargs.pop("port", 21))
26 | if scheme == "ftps":
27 | client.auth()
28 | usr, pswd = kwargs.pop("user", ""), kwargs.pop("passwd", "")
29 | if usr != "" and pswd != "":
30 | client.login(usr, passwd)
31 | #client.retrbinary(kwargs.pop("cmd", None), kwargs.pop("callback", None))
32 | #FIXME
33 | _ftps = _ftp
34 |
35 | def _http(self, url, *args, **kwargs):
36 | """ Simple HTTP downloader. """
37 | self[url] = requests.get(url, *args, **kwargs).content
38 | _https = _http
39 |
40 | def edit(self, filename):
41 | """ Edit a file using the configured text editor. """
42 | #FIXME: edit by calling the locator and manage its local file (e.g. for a URL, point to a temp folder)
43 | ted = self.console.config['TEXT_EDITOR']
44 | if which(ted) is None:
45 | raise ValueError(f"'{ted}' does not exist or is not installed")
46 | p = Path(self.console.config['WORKSPACE']).joinpath(filename)
47 | if not p.exists():
48 | p.touch()
49 | call([ted, str(p)], stderr=PIPE)
50 |
51 | def get(self, locator, *args, **kwargs):
52 | """ Get a resource. """
53 | if locator in self.keys() and not kwargs.pop("force", False):
54 | return self[locator]
55 | scheme, path = locator.split("://")
56 | if scheme in ["http", "https"]:
57 | r = requests.get(locator, *args, **kwargs)
58 | self[locator] = r.content
59 | if r.status_code == 403:
60 | raise ValueError("Forbidden")
61 | elif scheme in ["ftp", "ftps"]:
62 | client = [FTP, FTP_TLS][schem == "ftps"]()
63 | client.connect(kwargs.pop("host", ""), kwargs.pop("port", 21))
64 | if scheme == "ftps":
65 | client.auth()
66 | usr, pswd = kwargs.pop("user", ""), kwargs.pop("passwd", "")
67 | if usr != "" and pswd != "":
68 | client.login(usr, passwd)
69 | client.retrbinary(kwargs.pop("cmd", None), kwargs.pop("callback", None))
70 | #FIXME
71 | elif scheme == "file":
72 | with open(path, 'rb') as f:
73 | self[locator] = f.read()
74 | else:
75 | raise ValueError(f"Unsupported scheme '{scheme}'")
76 |
77 | def page(self, *filenames):
78 | """ Page a list of files using Less. """
79 | tvw = self.console.config['TEXT_VIEWER']
80 | if which(tvw) is None:
81 | raise ValueError(f"'{tvw}' does not exist or is not installed")
82 | filenames = list(map(str, filenames))
83 | for f in filenames:
84 | if not Path(str(f)).is_file():
85 | raise OSError("File does not exist")
86 | call([tvw] + filenames, stderr=PIPE)
87 |
88 | def page_text(self, text):
89 | """ Page a text using Less. """
90 | tmp = self.tempdir.tempfile()
91 | tmp.write_text(text)
92 | self.page(str(tmp))
93 |
94 | def save(self, key, dst):
95 | """ Save a resource. """
96 | with open(dst, 'wb') as f:
97 | f.write(self[key])
98 |
99 | def view(self, key):
100 | """ View a file using the configured text viewer. """
101 | from tinyscript.helpers import txt_terminal_render
102 | try:
103 | self.page_text(self[key])
104 | except KeyError:
105 | pass
106 | p = Path(self.console.config['WORKSPACE'], expand=True).joinpath(key)
107 | if p.suffix == ".md":
108 | self.page_text(txt_terminal_render(p.text, format="md").strip())
109 | else:
110 | # if the given key is not in the dictionary of files (APP_FOLDER/files/), it can still be in the workspace
111 | self.page(p)
112 |
113 | @property
114 | def list(self):
115 | """ Get the list of files from the workspace. """
116 | p = Path(self.console.config['WORKSPACE']).expanduser()
117 | for f in p.walk(filter_func=lambda p: p.is_file(), relative=True):
118 | if all(not re.match(x, f.filename) for x in ["(data|key|store)\.db.*", "history"]):
119 | yield f
120 |
121 | @property
122 | def tempdir(self):
123 | """ Get the temporary directory. """
124 | from tinyscript.helpers import TempPath
125 | if not hasattr(self, "_tempdir"):
126 | self._tempdir = TempPath(prefix=f"{self.console.appname}-", length=16)
127 | return self._tempdir
128 |
129 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/jobs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import shlex
3 | import subprocess
4 | from tinyscript.helpers.text import ansi_seq_strip
5 |
6 |
7 | __all__ = ["JobsPool"]
8 |
9 |
10 | communicate = lambda p, **i: tuple(map(lambda x: x.decode().strip(), p.communicate(**i)))
11 |
12 |
13 | class Job(subprocess.Popen):
14 | """ Subprocess-based job class, bound to its parent pool. """
15 | def __init__(self, cmd, **kwargs):
16 | self.parent = kwargs.pop('parent')
17 | debug = not kwargs.pop('no_debug', False)
18 | if debug:
19 | self.parent.logger.debug(" ".join(cmd) if isinstance(cmd, (tuple, list)) else cmd)
20 | cmd = shlex.split(cmd) if isinstance(cmd, str) and not kwargs.get('shell', False) else cmd
21 | super(Job, self).__init__(cmd, stdout=subprocess.PIPE, **kwargs)
22 | self._debug = debug
23 |
24 | def close(self, wait=True):
25 | for s in ["stdin", "stdout", "stderr"]:
26 | getattr(getattr(self, s, object()), "close", lambda: None)()
27 | if wait:
28 | return self.wait()
29 |
30 |
31 | class JobsPool(object):
32 | """ Subprocess-based pool for managing open jobs. """
33 | def __init__(self, max_jobs=None):
34 | self.__jobs = {None: []}
35 | self.max = max_jobs
36 |
37 | def __iter__(self):
38 | for j in self.__jobs.items():
39 | yield j
40 |
41 | def background(self, cmd, **kwargs):
42 | subpool = kwargs.pop('subpool')
43 | self.__jobs.setdefault(subpool, [])
44 | self.__jobs[subpool].append(Job(cmd, parent=self, **kwargs))
45 |
46 | def call(self, cmd, **kwargs):
47 | kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
48 | return subprocess.call(shlex.split(cmd), **kwargs)
49 |
50 | def free(self, subpool=None):
51 | for p in self.__jobs[subpool]:
52 | if p.poll():
53 | p.close(False)
54 | self.__jobs[subpool].remove(p)
55 |
56 | def run(self, cmd, stdin=None, show=False, timeout=None, ansi_strip=True, **kwargs):
57 | kwargs['stderr'] = subprocess.PIPE
58 | kwargs['stdin'] = (None if stdin is None else subprocess.PIPE)
59 | p = Job(cmd, parent=self, **kwargs)
60 | com_kw = {}
61 | if stdin is not None:
62 | com_kw['input'] = stdin.encode()
63 | if timeout is not None:
64 | com_kw['timeout'] = timeout
65 | out, err = "", ""
66 | try:
67 | out, err = tuple(map(lambda x: x.decode().strip(), p.communicate(**com_kw)))
68 | except (KeyboardInterrupt, subprocess.TimeoutExpired):
69 | out = []
70 | for line in iter(p.stdout.readline, ""):
71 | out.append(line)
72 | out = "\n".join(out)
73 | err = []
74 | for line in iter(p.stderr.readline, ""):
75 | err.append(line)
76 | err = "\n".join(err)
77 | if out != "" and p._debug:
78 | getattr(self.logger, ["debug", "info"][show])(out)
79 | if err != "" and p._debug:
80 | getattr(self.logger, ["debug", "error"][show])(err)
81 | if ansi_strip:
82 | out = ansi_seq_strip(out)
83 | return out, err
84 |
85 | def run_iter(self, cmd, timeout=None, ansi_strip=True, **kwargs):
86 | from time import time
87 | kwargs['stderr'] = subprocess.STDOUT
88 | kwargs['universal_newlines'] = True
89 | p = Job(cmd, parent=self, **kwargs)
90 | s = time()
91 | #FIXME: cleanup this part
92 | def readline():
93 | while True:
94 | try:
95 | l = p.stdout.readline()
96 | if l == "":
97 | break
98 | except UnicodeDecodeError:
99 | continue
100 | yield l
101 | try:
102 | for line in readline():
103 | if len(line) > 0:
104 | if p._debug:
105 | self.logger.debug(line)
106 | if ansi_strip:
107 | line = ansi_seq_strip(line)
108 | yield line
109 | if timeout is not None and time() - s > timeout:
110 | break
111 | finally:
112 | p.kill()
113 | p.close()
114 |
115 | def terminate(self, subpool=None):
116 | for p in self.__jobs.get(subpool, []):
117 | p.terminate()
118 | p.close()
119 | self.__jobs[subpool].remove(p)
120 |
121 | @property
122 | def logger(self):
123 | if hasattr(self, "console"):
124 | return self.console.logger
125 | from sploitkit.core.components.logger import null_logger
126 | return null_logger
127 |
128 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/layout.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from prompt_toolkit.widgets import FormattedTextToolbar, TextArea
3 | #TODO: do not forget to remove unuseful imports
4 |
5 |
6 | __all__ = ["CustomLayout"]
7 |
8 |
9 | #TODO: determine if this module is still useful ; remove it if necessary
10 |
11 | class AppToolbar(FormattedTextToolbar):
12 | pass
13 |
14 |
15 | class CustomLayout(object):
16 | def __init__(self, console):
17 | self.layout = console._session.app.layout
18 | #self.layout.container.children = self.layout.container.children[:-1]
19 | #print(self.layout.container.children)
20 |
21 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/logger.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | from tinyscript import colored, logging
3 |
4 |
5 | __all__ = ["get_logger", "null_logger", "set_logging_level"]
6 |
7 |
8 | DATETIME_FORMAT = "%m/%d/%y %H:%M:%S"
9 | LOGFILE_FORMAT = "%(asctime)s [%(process)5d] %(levelname)8s %(name)s - %(message)s"
10 | LOG_FORMAT = "%(levelsymbol)s %(message)s"
11 | LOG_FORMAT_DBG = "%(asctime)s %(name)32s %(levelname)8s %(message)s"
12 | LOG_LEVEL_SYMBOLS = {
13 | logging.DETAIL: colored("[#]", "white"), # this is aimed to provide even more info in dev mode
14 | logging.DEBUG: colored("[#]", "white"), # this is aimed to be used in normal mode
15 | logging.INFO: colored("[*]", "blue"),
16 | logging.WARNING: colored("[!]", "yellow"),
17 | logging.SUCCESS: colored("[+]", "green", attrs=['bold']),
18 | logging.ERROR: colored("[-]", "red", attrs=['bold']),
19 | logging.CRITICAL: colored("[X]", "red", attrs=['bold']),
20 | None: colored("[?]", "grey"),
21 | }
22 |
23 |
24 | # this avoids throwing e.g. FutureWarning or DeprecationWarning messages
25 | logging.captureWarnings(True)
26 | logger = logging.getLogger('py.warnings')
27 | logger.setLevel(logging.CRITICAL)
28 |
29 |
30 | # silent sh module's logging
31 | logger = logging.getLogger('sh.command')
32 | logger.setLevel(level=logging.WARNING)
33 | logger = logging.getLogger('sh.streamreader')
34 | logger.setLevel(level=logging.WARNING)
35 | logger = logging.getLogger('sh.stream_bufferer')
36 | logger.setLevel(level=logging.WARNING)
37 |
38 |
39 | # make aliases from logging functions
40 | null_logger = logging.nullLogger
41 | set_logging_level = logging.setLoggingLevel
42 |
43 |
44 | # add a custom message handler for tuning the format with 'levelsymbol'
45 | class ConsoleHandler(logging.StreamHandler):
46 | def emit(self, record):
47 | record.levelsymbol = LOG_LEVEL_SYMBOLS.get(record.levelno, "")
48 | super(ConsoleHandler, self).emit(record)
49 |
50 |
51 | # logging configuration
52 | def get_logger(name, logfile=None, level="INFO", dev=False, enabled=True):
53 | """ Logger initialization function. """
54 | def _setup_logfile(l):
55 | from logging.handlers import RotatingFileHandler
56 | if logfile is not None and not any(isinstance(h, RotatingFileHandler) for h in l.handlers):
57 | l.__logfile__ = logfile
58 | # setup a FileHandler for logging to a file (at level DEBUG)
59 | fh = RotatingFileHandler(logfile)
60 | fh.setFormatter(logging.Formatter(LOGFILE_FORMAT, datefmt=DATETIME_FORMAT))
61 | fh.setLevel(level)
62 | l.addHandler(fh)
63 | else:
64 | l.__logfile__ = None
65 |
66 | logger = logging.getLogger(name)
67 | logger.propagate = False
68 | level = getattr(logging, level) if not isinstance(level, int) else level
69 | # distinguish dev and framework-bound logger formats
70 | if dev:
71 | if enabled:
72 | # in dev mode, get a logger as of the native library
73 | logging.configLogger(logger, level, relative=True, fmt=LOG_FORMAT_DBG)
74 | _setup_logfile(logger)
75 | else:
76 | logger.setLevel(1000)
77 | else:
78 | # now use the dedicated class for the logger to be returned
79 | logger.setLevel(level)
80 | if len(logger.handlers) == 0:
81 | # setup a StreamHandler for the console (at level INFO)
82 | ch = ConsoleHandler()
83 | ch.setFormatter(logging.Formatter(fmt=LOG_FORMAT))
84 | ch.setLevel(level)
85 | logger.addHandler(ch)
86 | _setup_logfile(logger)
87 | else:
88 | for h in logger.handlers:
89 | h.setLevel(level)
90 | return logger
91 |
92 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/recorder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | __all__ = ["Recorder"]
3 |
4 |
5 | class Recorder(object):
6 | """ Simple text recording class. """
7 | _filter = ["record"]
8 | root_dir = "."
9 |
10 | def __init__(self):
11 | self.stop()
12 |
13 | @property
14 | def enabled(self):
15 | return self.__file is not None
16 |
17 | def save(self, text):
18 | """ Save the given text to the record file. """
19 | if self.enabled and text.split()[0] not in self._filter:
20 | self.__file.append_line(text)
21 |
22 | def start(self, filename, overwrite=False):
23 | """ Start the recorder, creating the record file. """
24 | from tinyscript.helpers import Path
25 | self.__file = f = Path(filename)
26 | if f.suffix != ".rc":
27 | self.__file = f = Path(self.root_dir).joinpath(filename + ".rc")
28 | if not overwrite and f.exists():
29 | raise OSError("File already exists")
30 | f.reset()
31 |
32 | def stop(self):
33 | """ Stop the recorder by removing the record file reference. """
34 | self.__file = None
35 |
36 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/sessions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import os
3 |
4 |
5 | __all__ = ["SessionsManager"]
6 |
7 |
8 | class Session(object):
9 | """ Class representing a session object based on a shell command """
10 | def __init__(self, n, cmd, **kwargs):
11 | from shlex import split
12 | from tinyscript.helpers import Path
13 | self.id = n
14 | self.parent = kwargs.pop('parent')
15 | if isinstance(cmd, str):
16 | cmd = split(cmd)
17 | self._path = Path(self.parent.console._files.tempdir, "session", str(n), create=True)
18 | for i, s in enumerate(["stdin", "stdout", "stderr"]):
19 | fifo = str(self._path.joinpath(str(i)))
20 | self._named_pipes.append(fifo)
21 | os.mkfifo(fifo, 0o777)
22 | setattr(self, "_" + s, os.open(fifo ,os.O_WRONLY))
23 |
24 | def close(self):
25 | from shutil import rmtree
26 | for s in ["stdin", "stdout", "stderr"]:
27 | getattr(self, "_" + s).close()
28 | rmtree(str(self._path))
29 | self._process.wait()
30 | del self.parent[self.id]
31 |
32 | def start(self, **kwargs):
33 | from subprocess import Popen
34 | kwargs['close_fds'] = True
35 | kwargs['preexec_fn'] = os.setsid # NB: see subprocess' doc ; preexec_fn is not thread-safe
36 | self._process = Popen(cmd, stdout=self._stdout, stderr=self._stderr, stdin=self._stdin, **kwargs)
37 |
38 |
39 | class SessionsManager(object):
40 | """ Class for managing session objects. """
41 | def __init__(self, max_sessions=None):
42 | self.__sessions = []
43 | self.max = max_sessions
44 |
45 | def __delitem__(self, session_id):
46 | self.__sessions[sessin_id] = None
47 | while self.__sessions[-1] is None:
48 | self.__sessions.pop()
49 |
50 | def __getitem__(self, session_id):
51 | return self.__sessions[int(session_id)]
52 |
53 | def __iter__(self):
54 | for i, s in enumerate(self.__sessions):
55 | if s is not None:
56 | yield i, s
57 |
58 | def __len__(self):
59 | n = 0
60 | for s in self:
61 | n += 1
62 | return n
63 |
64 | def new(self, session):
65 | for i, s in enumerate(self.__session):
66 | if s is None:
67 | self.__session[i] = session
68 | return session
69 | self.__session.append(session)
70 | return session
71 |
72 | def process(self, cmd, **kwargs):
73 | return self.new(Session(self, i+1, cmd, **kwargs))
74 |
75 | def shell(self, shell_cls, *args, **kwargs):
76 | return self.new(shell_cls(*args, **kwargs))
77 |
78 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/store.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from peewee import SqliteDatabase
3 |
4 |
5 | __all__ = ["StoragePool"]
6 |
7 |
8 | class StoragePool(object):
9 | """ Storage pool class. """
10 | __pool = []
11 |
12 | def __init__(self, ext_class=None):
13 | self._entity_class = getattr(ext_class(), "base_class", None)
14 | self._ext_class = ext_class
15 |
16 | def close(self, remove=False):
17 | """ Close every database in the pool. """
18 | for db in self.__pool[::-1]:
19 | self.remove(db) if remove else db.close()
20 |
21 | def free(self):
22 | """ Close and remove every database in the pool. """
23 | self.close(True)
24 |
25 | def get(self, path, *args, **kwargs):
26 | """ Get a database from the pool ; if the DB does not exist yet, create and register it. """
27 | path = str(path) # ensure the input is str, e.g. not a Path instance
28 | try:
29 | db = [_ for _ in self.__pool if _.path == path][0]
30 | except IndexError:
31 | classes = tuple([Store] + self.extensions)
32 | cls = type("ExtendedStore", classes, {})
33 | db = cls(path, *args, **kwargs)
34 | db._pool = self
35 | # as the store extension class should subclass Entity, in 'classes', store extension subclasses will be
36 | # present, therefore making ExtendedStore registered in its list of subclasses ; this line prevents from
37 | # having multiple combined classes having the same Store base class
38 | if self._ext_class is not None and hasattr(self._ext_class, "unregister_subclass"):
39 | self._ext_class.unregister_subclass(cls)
40 | self.__pool.append(db)
41 | for m in self.models:
42 | m.bind(db)
43 | db.create_tables(self.models, safe=True)
44 | db.close() # commit and save the created tables
45 | db.connect()
46 | return db
47 |
48 | def remove(self, db):
49 | """ Remove a database from the pool. """
50 | db.close()
51 | delattr(db, "_pool")
52 | self.__pool.remove(db)
53 | del db
54 |
55 | @property
56 | def extensions(self):
57 | """ Get the list of store extension subclasses. """
58 | try:
59 | return self._ext_class.subclasses
60 | except AttributeError:
61 | return []
62 |
63 |
64 | class Store(SqliteDatabase):
65 | """ Storage database class. """
66 | def __init__(self, path, *args, **kwargs):
67 | self.path = str(path) # ensure the input is str, e.g. not Path
68 | self._last_snapshot = 0
69 | kwargs.setdefault('pragmas', {})
70 | # enable automatic VACUUM (to regularly defragment the DB)
71 | kwargs['pragmas'].setdefault('auto_vacuum', 1)
72 | # set page cache size (in KiB)
73 | kwargs['pragmas'].setdefault('cache_size', -64000)
74 | # allow readers and writers to co-exist
75 | kwargs['pragmas'].setdefault('journal_mode', "wal")
76 | # enforce foreign-key constraints
77 | kwargs['pragmas'].setdefault('foreign_keys', 1)
78 | # enforce CHECK constraints
79 | kwargs['pragmas'].setdefault('ignore_check_constraints', 0)
80 | # let OS handle fsync
81 | kwargs['pragmas'].setdefault('synchronous', 0)
82 | # force every transaction in exclusive mode
83 | kwargs['pragmas'].setdefault('locking_mode', 1)
84 | super(Store, self).__init__(path, *args, **kwargs)
85 |
86 | def __getattr__(self, name):
87 | """ Override getattr to handle add_* store methods. """
88 | from re import match
89 | if name == "basemodels":
90 | BaseModel = self._pool._entity_class._subclasses["basemodel"]
91 | return self._pool._entity_class._subclasses[BaseModel]
92 | elif name == "models":
93 | Model = self._pool._entity_class._subclasses["model"]
94 | return self._pool._entity_class._subclasses[Model]
95 | elif name == "volatile":
96 | return self.path == ":memory:"
97 | elif match(r"^[gs]et_[a-z]+", name) and name != "model":
98 | model = "".join(w.capitalize() for w in name.split("_")[1:])
99 | cls = self.get_model(model)
100 | if cls is not None:
101 | if name.startswith("get"):
102 | return cls.get
103 | elif hasattr(cls, "set"):
104 | return cls.set
105 | raise AttributeError("Store object has no attribute %r" % name)
106 |
107 | def get_model(self, name, base=False):
108 | """ Get a model class from its name. """
109 | return self._pool._entity_class.get_subclass("model", name) or \
110 | self._pool._entity_class.get_subclass("basemodel", name)
111 |
112 | def snapshot(self, save=True):
113 | """ Snapshot the store in order to be able to get back to this state afterwards if the results are corrupted by
114 | a module OR provide the reference number of the snapshot to get back to, and remove every other snapshot
115 | after this number. """
116 | if not save and self._last_snapshot == 0:
117 | return
118 | self.close()
119 | if save:
120 | self._last_snapshot += 1
121 | s = f"{self.path}.snapshot{self._last_snapshot}"
122 | from shutil import copy
123 | copy(self.path, s) if save else copy(s, self.path)
124 | if not save:
125 | from os import remove
126 | remove("{}.snapshot{}".format(self.path, self._last_snapshot))
127 | self._last_snapshot -= 1
128 | self.connect()
129 |
130 |
--------------------------------------------------------------------------------
/src/sploitkit/core/components/validator.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from prompt_toolkit.validation import Validator, ValidationError
3 |
4 |
5 | __all__ = ["CommandValidator"]
6 |
7 |
8 | class CommandValidator(Validator):
9 | """ Completer for console's commands and arguments. """
10 | def __init__(self, fail=True):
11 | self._fail = fail
12 | super(CommandValidator, self).__init__()
13 |
14 | def validate(self, document):
15 | # first, tokenize document.text
16 | tokens = self.console._get_tokens(document.text.strip())
17 | l = len(tokens)
18 | # then handle tokens
19 | commands = self.console.commands
20 | # when no token provided, do nothing
21 | if l == 0:
22 | return
23 | # when a command is being typed, mention if it is existing
24 | cmd = tokens[0]
25 | if l == 1 and cmd not in commands.keys() and self._fail:
26 | raise ValidationError(message="Unknown command")
27 | # when a valid first token is provided, handle command's validation, if any available
28 | elif l >= 1 and cmd in commands.keys():
29 | c = commands[cmd]._instance
30 | try:
31 | c._validate(*tokens[1:])
32 | except Exception as e:
33 | m = f"Command syntax: {c.signature.format(cmd)}"
34 | e = str(e)
35 | if not e.startswith("validate() "):
36 | m = m.format([" (" + e + ")", ""][len(e) == 0])
37 | else:
38 | m = m.format("")
39 | raise ValidationError(message=m)
40 | # otherwise, the command is considered bad
41 | elif self._fail:
42 | raise ValidationError(message="Bad command")
43 |
44 |
--------------------------------------------------------------------------------
/src/sploitkit/core/console.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import gc
3 | import io
4 | import os
5 | import shlex
6 | import sys
7 | from asciistuff import get_banner, get_quote
8 | from bdb import BdbQuit
9 | from datetime import datetime
10 | from inspect import getfile, isfunction
11 | from itertools import chain
12 | from prompt_toolkit import print_formatted_text as print_ft, PromptSession
13 | from prompt_toolkit.application.current import get_app_session
14 | from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
15 | from prompt_toolkit.formatted_text import ANSI, FormattedText
16 | from prompt_toolkit.history import FileHistory
17 | from prompt_toolkit.output import DummyOutput
18 | from prompt_toolkit.styles import Style
19 | from random import choice
20 | from shutil import which
21 | from tinyscript.helpers import filter_bin, get_terminal_size, parse_docstring, Capture, Path
22 |
23 | from .command import *
24 | from .components import *
25 | from .entity import *
26 | from .model import *
27 | from .module import *
28 |
29 |
30 | __all__ = [
31 | "Entity",
32 | # subclassable main entities
33 | "BaseModel", "Command", "Console", "Model", "Module", "StoreExtension",
34 | # console-related classes
35 | "Config", "ConsoleExit", "ConsoleDuplicate", "FrameworkConsole", "Option",
36 | ]
37 |
38 | EDITORS = ["atom", "emacs", "gedit", "mousepad", "nano", "notepad", "notepad++", "vi", "vim"]
39 | VIEWERS = ["bat", "less"]
40 | try:
41 | DEFAULT_EDITOR = filter_bin(*EDITORS)[-1]
42 | except IndexError:
43 | DEFAULT_EDITOR = None
44 | try:
45 | DEFAULT_VIEWER = filter_bin(*VIEWERS)[0]
46 | except IndexError:
47 | DEFAULT_VIEWER = None
48 |
49 | _output = get_app_session().output
50 | dcount = lambda d, n=0: sum([dcount(v, n) if isinstance(v, dict) else n + 1 for v in d.values()])
51 | logger = get_logger("core.console")
52 |
53 |
54 | def print_formatted_text(*args, **kwargs):
55 | """ Proxy function that uses the global (capturable) _output. """
56 | kwargs['output'] = kwargs.get('output', _output)
57 | return print_ft(*args, **kwargs)
58 |
59 |
60 | class _CaptureOutput(DummyOutput):
61 | def __init__(self):
62 | self.__file = io.StringIO()
63 |
64 | def __str__(self):
65 | return self.__file.getvalue().strip()
66 |
67 | def write(self, data):
68 | self.__file.write(data)
69 |
70 |
71 | class MetaConsole(MetaEntity):
72 | """ Metaclass of a Console. """
73 | _has_config = True
74 |
75 |
76 | class Console(Entity, metaclass=MetaConsole):
77 | """ Base console class. """
78 | # convention: mangled attributes should not be customized when subclassing Console...
79 | _files = FilesManager()
80 | _jobs = JobsPool()
81 | _recorder = Recorder()
82 | _sessions = SessionsManager()
83 | _state = {} # state shared between all the consoles
84 | _storage = StoragePool(StoreExtension)
85 | # ... by opposition to public class attributes that can be tuned
86 | appname = ""
87 | config = Config()
88 | exclude = []
89 | level = ROOT_LEVEL
90 | message = PROMPT_FORMAT
91 | motd = """
92 |
93 | """
94 | parent = None
95 | sources = SOURCES
96 | style = PROMPT_STYLE
97 |
98 | def __init__(self, parent=None, **kwargs):
99 | fail = kwargs.pop("fail", True)
100 | super(Console, self).__init__()
101 | # determine the relevant parent
102 | self.parent = parent
103 | if self.parent is not None and self.parent.level == self.level:
104 | while parent is not None and parent.level == self.level:
105 | parent = parent.parent # go up of one console level
106 | # raise an exception in the context of command's .run() execution, to be propagated to console's .run()
107 | # execution, setting the directly higher level console in argument
108 | raise ConsoleDuplicate(self, parent)
109 | # back-reference the console
110 | self.config.console = self
111 | # configure the console regarding its parenthood
112 | if self.parent is None:
113 | if Console.parent is not None:
114 | raise Exception("Only one parent console can be used")
115 | Console.parent = self
116 | Console.parent._start_time = datetime.now()
117 | Console.appdispname = Console.appname
118 | Console.appname = Console.appname.lower()
119 | self._root = Path(getfile(self.__class__)).resolve()
120 | self.__init(**kwargs)
121 | else:
122 | self.parent.child = self
123 | # reset commands and other bound stuffs
124 | self.reset()
125 | # setup the session with the custom completer and validator
126 | completer, validator = CommandCompleter(), CommandValidator(fail)
127 | completer.console = validator.console = self
128 | message, style = self.prompt
129 | self._session = PromptSession(
130 | message,
131 | completer=completer,
132 | history=FileHistory(Path(self.config.option("WORKSPACE").value).joinpath("history")),
133 | validator=validator,
134 | style=Style.from_dict(style),
135 | )
136 | CustomLayout(self)
137 |
138 | def __init(self, **kwargs):
139 | """ Initialize the parent console with commands and modules. """
140 | # setup banners
141 | try:
142 | bsrc = str(choice(self._sources("banners")))
143 | print_formatted_text("")
144 | # display a random banner from the banners folder
145 | get_banner_func = kwargs.get('get_banner_func', get_banner)
146 | banner_colors = kwargs.get('banner_section_styles', {})
147 | text = get_banner_func(self.appdispname, bsrc, styles=banner_colors)
148 | if text:
149 | print_formatted_text(ANSI(text))
150 | # display a random quote from quotes.csv (in the banners folder)
151 | get_quote_func = kwargs.get('get_quote_func', get_quote)
152 | try:
153 | text = get_quote_func(os.path.join(bsrc, "quotes.csv"))
154 | if text:
155 | print_formatted_text(ANSI(text))
156 | except ValueError:
157 | pass
158 | except IndexError:
159 | pass
160 | # setup libraries
161 | for lib in self._sources("libraries"):
162 | sys.path.insert(0, str(lib))
163 | # setup entities
164 | self._load_kwargs = {'include_base': kwargs.get("include_base", True),
165 | 'select': kwargs.get("select", {'command': Command._functionalities}),
166 | 'exclude': kwargs.get("exclude", {}),
167 | 'backref': kwargs.get("backref", BACK_REFERENCES),
168 | 'docstr_parser': kwargs.get("docstr_parser", parse_docstring),
169 | 'remove_cache': True}
170 | load_entities([BaseModel, Command, Console, Model, Module, StoreExtension],
171 | *([self._root] + self._sources("entities")), **self._load_kwargs)
172 | Console._storage.models = Model.subclasses + BaseModel.subclasses
173 | # display module stats
174 | print_formatted_text(FormattedText([("#00ff00", Module.get_summary())]))
175 | # setup the prompt message
176 | self.message.insert(0, ('class:appname', self.appname))
177 | # display warnings
178 | self.reset()
179 | if Entity.has_issues():
180 | self.logger.warning("There are some issues ; use 'show issues' to see more details")
181 | # console's components back-referencing
182 | for attr in ["_files", "_jobs", "_sessions"]:
183 | setattr(getattr(Console, attr), "console", self)
184 |
185 | def _close(self):
186 | """ Gracefully close the console. """
187 | self.logger.debug(f"Exiting {self.__class__.__name__}[{id(self)}]")
188 | if hasattr(self, "close") and isfunction(self.close):
189 | self.close()
190 | # cleanup references for this console
191 | self.detach()
192 | # important note: do not confuse '_session' (refers to prompt session) with sessions (sessions manager)
193 | if hasattr(self, "_session"):
194 | delattr(self._session.completer, "console")
195 | delattr(self._session.validator, "console")
196 | # remove the singleton instance of the current console
197 | c = self.__class__
198 | if hasattr(c, "_instance"):
199 | del c._instance
200 | if self.parent is not None:
201 | del self.parent.child
202 | # rebind entities to the parent console
203 | self.parent.reset()
204 | # remove all finished jobs from the pool
205 | self._jobs.free()
206 | else:
207 | # gracefully close every DB in the pool
208 | self._storage.free()
209 | # terminate all running jobs
210 | self._jobs.terminate()
211 |
212 | def _get_tokens(self, text, suffix=("", "\"", "'")):
213 | """ Recursive token split function also handling ' and " (that is, when 'text' is a partial input with a string
214 | not closed by a quote). """
215 | text = text.lstrip()
216 | try:
217 | tokens = shlex.split(text + suffix[0])
218 | except ValueError:
219 | return self._get_tokens(text, suffix[1:])
220 | except IndexError:
221 | return []
222 | if len(tokens) > 0:
223 | cmd = tokens[0]
224 | if len(tokens) > 2 and getattr(self.commands.get(cmd), "single_arg", False):
225 | tokens = [cmd, " ".join(tokens[1:])]
226 | elif len(tokens) > 3:
227 | tokens = [cmd, tokens[1], " ".join(tokens[2:])]
228 | return tokens
229 |
230 | def _reset_logname(self):
231 | """ Reset logger's name according to console's attributes. """
232 | try:
233 | self.logger.name = f"{self.level}:{self.logname}"
234 | except AttributeError:
235 | self.logger.name = self.__class__.name
236 |
237 | def _run_if_defined(self, func):
238 | """ Run the given function if it is defined at the module level. """
239 | if hasattr(self, "module") and hasattr(self.module, func) and \
240 | not (getattr(self.module._instance, func)() is None):
241 | self.logger.debug(f"{func} failed")
242 | return False
243 | return True
244 |
245 | def _sources(self, items):
246 | """ Return the list of sources for the related items [banners|entities|libraries], first trying subclass' one
247 | then Console class' one. Also, resolve paths relative to the path where the parent Console is found. """
248 | src = self.sources.get(items, Console.sources[items])
249 | if isinstance(src, (str, Path)):
250 | src = [src]
251 | return [Path(self._root.dirname.joinpath(s).expanduser().resolve()) for s in (src or [])]
252 |
253 | def attach(self, eccls, directref=False, backref=True):
254 | """ Attach an entity child to the calling entity's instance. """
255 | # handle direct reference from self to eccls
256 | if directref:
257 | # attach new class
258 | setattr(self, eccls.entity, eccls)
259 | # handle back reference from eccls to self
260 | if backref:
261 | setattr(eccls, "console", self)
262 | # create a singleton instance of the entity
263 | eccls._instance = getattr(eccls, "_instance", None) or eccls()
264 |
265 | def detach(self, eccls=None):
266 | """ Detach an entity child class from the console and remove its back-reference. """
267 | # if no argument, detach every class registered in self._attached
268 | if eccls is None:
269 | for subcls in Entity._subclasses:
270 | self.detach(subcls)
271 | elif eccls in ["command", "module"]:
272 | for ec in [Command, Module][eccls == "module"].subclasses:
273 | if ec.entity == eccls:
274 | self.detach(ec)
275 | else:
276 | if hasattr(eccls, "entity") and hasattr(self, eccls.entity):
277 | delattr(self, eccls.entity)
278 | # remove the singleton instance of the entity previously opened
279 | if hasattr(eccls, "_instance"):
280 | del eccls._instance
281 |
282 | def execute(self, cmd, abort=False):
283 | """ Alias for run. """
284 | return self.run(cmd, abort)
285 |
286 | def play(self, *commands, capture=False):
287 | """ Execute a list of commands. """
288 | global _output
289 | if capture:
290 | r = []
291 | error = False
292 | try:
293 | w, _ = get_terminal_size()
294 | except TypeError:
295 | w = 80
296 | for c in commands:
297 | if capture:
298 | if error:
299 | r.append((c, None, None))
300 | continue
301 | __tmp = _output
302 | _output = _CaptureOutput()
303 | error = not self.run(c, True)
304 | r.append((c, str(_output)))
305 | _output = __tmp
306 | else:
307 | print_formatted_text("\n" + (" " + c + " ").center(w, "+") + "\n")
308 | if not self.run(c, True):
309 | break
310 | if capture:
311 | if r[-1][0] == "exit":
312 | r.pop(-1)
313 | return r
314 |
315 | def rcfile(self, rcfile, capture=False):
316 | """ Execute commands from a .rc file. """
317 | with open(rcfile) as f:
318 | commands = [c.strip() for c in f]
319 | return self.play(*commands, capture)
320 |
321 | def reset(self):
322 | """ Setup commands for the current level, reset bindings between commands and the current console then update
323 | store's object. """
324 | self.detach("command")
325 | # setup level's commands, starting from general-purpose commands
326 | self.commands = {}
327 | # add commands
328 | for n, c in chain(Command.commands.get("general", {}).items(), Command.commands.get(self.level, {}).items()):
329 | self.attach(c)
330 | if self.level not in getattr(c, "except_levels", []) and c.check():
331 | self.commands[n] = c
332 | else:
333 | self.detach(c)
334 | root = self.config.option('WORKSPACE').value
335 | # get the relevant store and bind it to loaded models
336 | Console.store = Console._storage.get(Path(root).joinpath("store.db"))
337 | # update command recorder's root directory
338 | self._recorder.root_dir = root
339 |
340 | def run(self, cmd, abort=False):
341 | """ Run a framework console command. """
342 | # assign tokens (or abort if tokens' split gives [])
343 | tokens = self._get_tokens(cmd)
344 | try:
345 | name, args = tokens[0], tokens[1:]
346 | except IndexError:
347 | if abort:
348 | raise
349 | return True
350 | # get the command singleton instance (or abort if name not in self.commands) ; if command arguments should not
351 | # be split, adapt args
352 | try:
353 | obj = self.commands[name]._instance
354 | except KeyError:
355 | if abort:
356 | raise
357 | return True
358 | # now handle the command (and its validation if existing)
359 | try:
360 | if hasattr(obj, "validate"):
361 | obj.validate(*args)
362 | if name != "run" or self._run_if_defined("prerun"):
363 | obj.run(*args)
364 | if name == "run":
365 | self._run_if_defined("postrun")
366 | return True
367 | except BdbQuit: # when using pdb.set_trace()
368 | return True
369 | except ConsoleDuplicate as e:
370 | # pass the higher console instance attached to the exception raised from within a command's .run() execution
371 | # to console's .start(), keeping the current command to be reexecuted
372 | raise ConsoleDuplicate(e.current, e.higher, cmd if e.cmd is None else e.cmd)
373 | except ConsoleExit:
374 | return False
375 | except ValueError as e:
376 | if str(e).startswith("invalid width ") and str(e).endswith(" (must be > 0)"):
377 | self.logger.warning("Cannot display ; terminal width too low")
378 | else:
379 | (self.logger.exception if self.config.option('DEBUG').value else self.logger.failure)(e)
380 | return abort is False
381 | except Exception as e:
382 | self.logger.exception(e)
383 | return abort is False
384 | finally:
385 | gc.collect()
386 |
387 | def start(self):
388 | """ Start looping with console's session prompt. """
389 | reexec = None
390 | self._reset_logname()
391 | self.logger.debug(f"Starting {self.__class__.__name__}[{id(self)}]")
392 | # execute attached module's pre-load function if relevant
393 | self._run_if_defined("preload")
394 | # now start the console loop
395 | while True:
396 | self._reset_logname()
397 | try:
398 | c = reexec if reexec is not None else self._session.prompt(
399 | auto_suggest=AutoSuggestFromHistory(),
400 | #bottom_toolbar="This is\na multiline toolbar", # note: this disables terminal scrolling
401 | #mouse_support=True,
402 | )
403 | reexec = None
404 | Console._recorder.save(c)
405 | if not self.run(c):
406 | break # console run aborted
407 | except ConsoleDuplicate as e:
408 | # stop raising duplicate when reaching a console with a different level, then reset associated commands
409 | # not to rerun the erroneous one from the context of the just-exited console
410 | if self == e.higher:
411 | reexec = e.cmd
412 | self.reset()
413 | continue
414 | self._close()
415 | # reraise up to the higher (level) console
416 | raise e
417 | except EOFError:
418 | Console._recorder.save("exit")
419 | break
420 | except (KeyboardInterrupt, ValueError):
421 | continue
422 | # execute attached module's post-load function if relevant
423 | self._run_if_defined("postload")
424 | # gracefully close and chain this console instance
425 | self._close()
426 | return self
427 |
428 | @property
429 | def logger(self):
430 | try:
431 | return Console._logger
432 | except:
433 | return null_logger
434 |
435 | @property
436 | def modules(self):
437 | return Module.modules
438 |
439 | @property
440 | def prompt(self):
441 | if self.parent is None:
442 | return self.message, self.style
443 | # setup the prompt message by adding child's message tokens at the end of parent's one (parent's last token is
444 | # then re-appended) if it shall not be reset, otherwise reset it then set child's tokens
445 | if getattr(self, "message_reset", False):
446 | return self.message, self.style
447 | pmessage, pstyle = self.parent.prompt
448 | message = pmessage.copy() # copy parent message tokens
449 | t = message.pop()
450 | message.extend(self.message)
451 | message.append(t)
452 | # setup the style, using this of the parent
453 | style = pstyle.copy() # copy parent style dict
454 | style.update(self.style)
455 | return message, style
456 |
457 | @property
458 | def root(self):
459 | return Console.parent
460 |
461 | @property
462 | def sessions(self):
463 | return list(self._sessions)
464 |
465 | @property
466 | def state(self):
467 | """ Getter for the shared state. """
468 | return Console._state
469 |
470 | @property
471 | def uptime(self):
472 | """ Get application's uptime. """
473 | t = datetime.now() - Console.parent._start_time
474 | s = t.total_seconds()
475 | h, _ = divmod(s, 3600)
476 | m, s = divmod(_, 60)
477 | return f"{h:02}:{m:02}:{s:02}"
478 |
479 |
480 | class ConsoleDuplicate(Exception):
481 | """ Dedicated exception class for exiting a duplicate (sub)console. """
482 | def __init__(self, current, higher, cmd=None):
483 | self.cmd, self.current, self.higher = cmd, current, higher
484 | super(ConsoleDuplicate, self).__init__("Another console of the same level is already running")
485 |
486 |
487 | class ConsoleExit(SystemExit):
488 | """ Dedicated exception class for exiting a (sub)console. """
489 | pass
490 |
491 |
492 | class FrameworkConsole(Console):
493 | """ Framework console subclass for defining specific config options. """
494 | _entity_class = Console
495 | aliases = []
496 | config = Config({
497 | Option(
498 | 'APP_FOLDER',
499 | "folder where application assets (i.e. logs) are saved",
500 | True,
501 | #set_callback=lambda o: o.root._set_app_folder(debug=o.config.option('DEBUG').value),
502 | glob=False,
503 | ): "~/.{appname}",
504 | ROption(
505 | 'DEBUG',
506 | "debug mode",
507 | True,
508 | bool,
509 | set_callback=lambda o: o.root._set_logging(o.value),
510 | glob=False,
511 | ): "false",
512 | ROption(
513 | 'TEXT_EDITOR',
514 | "text file editor to be used",
515 | False,
516 | choices=lambda: filter_bin(*EDITORS),
517 | validate=lambda s, v: which(v) is not None,
518 | glob=False,
519 | ): DEFAULT_EDITOR,
520 | ROption(
521 | 'TEXT_VIEWER',
522 | "text file viewer (pager) to be used",
523 | False,
524 | choices=lambda: filter_bin(*VIEWERS),
525 | validate=lambda s, v: which(v) is not None,
526 | glob=False,
527 | ): DEFAULT_VIEWER,
528 | Option(
529 | 'ENCRYPT_PROJECT',
530 | "ask for a password to encrypt a project when archiving",
531 | True,
532 | bool,
533 | glob=False,
534 | ): "true",
535 | Option(
536 | 'WORKSPACE',
537 | "folder where results are saved",
538 | True,
539 | set_callback=lambda o: o.root._set_workspace(),
540 | glob=False,
541 | ): "~/Notes",
542 | })
543 |
544 | def __init__(self, appname=None, *args, **kwargs):
545 | Console._dev_mode = kwargs.pop("dev", False)
546 | Console.appname = appname or getattr(self, "appname", Console.appname)
547 | self.opt_prefix = "Console"
548 | o, v = self.config.option('APP_FOLDER'), str(self.config['APP_FOLDER'])
549 | self.config[o] = Path(v.format(appname=self.appname.lower()))
550 | o.old_value = None
551 | self.config['DEBUG'] = kwargs.get('debug', False)
552 | self._set_app_folder(silent=True, **kwargs)
553 | self._set_workspace()
554 | super(FrameworkConsole, self).__init__(*args, **kwargs)
555 |
556 | def __set_folder(self, option, subpath=""):
557 | """ Set a new folder, moving an old to the new one if necessary. """
558 | o = self.config.option(option)
559 | old, new = o.old_value, o.value
560 | if old == new:
561 | return
562 | try:
563 | if old is not None:
564 | os.rename(old, new)
565 | except Exception as e:
566 | pass
567 | Path(new).joinpath(subpath).mkdir(parents=True, exist_ok=True)
568 | return new
569 |
570 | def _set_app_folder(self, **kwargs):
571 | """ Set a new APP_FOLDER, moving an old to the new one if necessary. """
572 | self._files.root_dir = self.__set_folder("APP_FOLDER", "files")
573 | self._set_logging(**kwargs) # this is necessary as the log file is located in APP_FOLDER
574 |
575 | def _set_logging(self, debug=False, to_file=True, **kwargs):
576 | """ Set a new logger with the input logging level. """
577 | l, p1, p2, dev = "INFO", None, None, Console._dev_mode
578 | if debug:
579 | l = "DETAIL" if Console._dev_mode else "DEBUG"
580 | if to_file:
581 | # attach a logger to the console
582 | lpath = self.app_folder.joinpath("logs")
583 | lpath.mkdir(parents=True, exist_ok=True)
584 | p1 = str(lpath.joinpath("main.log"))
585 | if dev:
586 | p2 = str(lpath.joinpath("debug.log"))
587 | if l == "INFO" and not kwargs.get('silent', False):
588 | self.logger.debug("Set logging to INFO")
589 | Console._logger = get_logger(self.appname.lower(), p1, l)
590 | # setup framework's logger with its own get_logger function (configuring other handlers than the default one)
591 | set_logging_level(l, self.appname.lower(), config_func=lambda lgr, lvl: get_logger(lgr.name, p1, lvl))
592 | # setup internal (dev) loggers with the default logging.configLogger (enhancement to logging from Tinyscript)
593 | set_logging_level(l, "core", config_func=lambda lgr, lvl: get_logger(lgr.name, p2, lvl, True, dev))
594 | if l != "INFO" and not kwargs.get('silent', False):
595 | self.logger.debug(f"Set logging to {l}")
596 |
597 | def _set_workspace(self):
598 | """ Set a new APP_FOLDER, moving an old to the new one if necessary. """
599 | self.__set_folder("WORKSPACE")
600 |
601 | @property
602 | def app_folder(self):
603 | """ Shortcut to the current application folder. """
604 | return Path(self.config.option('APP_FOLDER').value)
605 |
606 | @property
607 | def workspace(self):
608 | """ Shortcut to the current workspace. """
609 | return Path(self.config.option("WORKSPACE").value)
610 |
611 |
--------------------------------------------------------------------------------
/src/sploitkit/core/model.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import datetime
3 | from peewee import *
4 | from peewee import Model as PeeweeModel, ModelBase
5 |
6 | from .components.logger import get_logger
7 | from .entity import Entity, MetaEntityBase
8 |
9 |
10 | __all__ = ["BaseModel", "Model", "StoreExtension"]
11 |
12 |
13 | logger = get_logger("core.model")
14 |
15 |
16 | class MetaModel(ModelBase, MetaEntityBase):
17 | """ Metaclass of a Model. """
18 | triggers = []
19 |
20 | def __new__(meta, name, bases, clsdict):
21 | subcls = ModelBase.__new__(meta, name, bases, clsdict)
22 | if subcls.__name__ != "Model":
23 | pass
24 | # add triggers here
25 | #try:
26 | # trigger = f"{subcls._meta.table_name}_updated"
27 | # subcls.add_trigger(trigger, "AFTER", "UPDATE", "UPDATE", "SET updated=CURRENT_TIMESTAMP")
28 | #except AttributeError:
29 | # pass
30 | return subcls
31 |
32 | def __repr__(self):
33 | return "<%s: %s>" % (self.entity.capitalize(), self.__name__)
34 |
35 |
36 | class BaseModel(PeeweeModel, Entity, metaclass=MetaModel):
37 | """ Main class handling console store's base models (that is, without pre-attached fields). """
38 | pass
39 |
40 |
41 | class Model(BaseModel):
42 | """ Main class handling console store's models. """
43 | source = CharField(null=True)
44 | created = DateTimeField(default=datetime.datetime.now, null=False)
45 | updated = DateTimeField(default=datetime.datetime.now, null=False)
46 |
47 | @classmethod
48 | def add_trigger(cls, trig, when, top, op, sql, safe=True):
49 | """ Add a trigger to model's list of triggers. """
50 | cls.triggers.append(Trigger(cls, trig, when, top, op, sql, safe))
51 |
52 | @classmethod
53 | def create_table(cls, **options):
54 | """ Create this table in the bound database."""
55 | super(Model, cls).create_table(**options)
56 | for trigger in cls.triggers:
57 | try:
58 | cls._meta.database.execute_sql(str(trigger))
59 | except:
60 | pass
61 |
62 | @classmethod
63 | def set(cls, **items):
64 | """ Insert or update a record. """
65 | items["updated"] = datetime.datetime.now()
66 | return super(Model, cls).get_or_create(**items)
67 |
68 |
69 | class StoreExtension(Entity, metaclass=MetaEntityBase):
70 | """ Dummy class handling store extensions for the Store class. """
71 | pass
72 |
73 |
74 | # source:
75 | # https://stackoverflow.com/questions/34142550/sqlite-triggers-datetime-defaults-in-sql-ddl-using-peewee-in-python
76 | class Trigger(object):
77 | """Trigger template wrapper for use with peewee ORM."""
78 | _template = """
79 | {create} {name} {when} {trigger_op}
80 | ON {tablename}
81 | BEGIN
82 | {op} {tablename} {sql} WHERE {pk}={old_new}.{pk};
83 | END;
84 | """
85 |
86 | def __init__(self, table, name, when, trigger_op, op, sql, safe=True):
87 | self.create = "CREATE TRIGGER" + (" IF NOT EXISTS" if safe else "")
88 | self.tablename = table._meta.name
89 | self.pk = table._meta.primary_key.name
90 | self.name = name
91 | self.when = when
92 | self.trigger_op = trigger_op
93 | self.op = op
94 | self.sql = sql
95 | self.old_new = "new" if trigger_op.lower() == "insert" else "old"
96 |
97 | def __str__(self):
98 | return self._template.format(**self.__dict__)
99 |
100 |
--------------------------------------------------------------------------------
/src/sploitkit/core/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | from inspect import getfile
3 | from tinyscript.helpers import flatten_dict, BorderlessTable, Path, PathBasedDict
4 |
5 | from .components.logger import get_logger
6 | from .entity import Entity, MetaEntity
7 |
8 |
9 | __all__ = ["Module"]
10 |
11 |
12 | logger = get_logger("core.module")
13 |
14 |
15 | class MetaModule(MetaEntity):
16 | """ Metaclass of a Module. """
17 | _has_config = True
18 | _inherit_metadata = True
19 |
20 | def __new__(meta, name, bases, clsdict):
21 | subcls = type.__new__(meta, name, bases, clsdict)
22 | # compute module's path from its root folder if no path attribute defined on its class
23 | if getattr(subcls, "path", None) is None:
24 | p = Path(getfile(subcls)).parent
25 | # collect the source temporary attribute
26 | s = getattr(subcls, "_source", ".")
27 | try:
28 | scp = p.relative_to(Path(s))
29 | if len(scp.parts) > 0 and scp.parts[-1] == "__pycache__":
30 | scp = scp.parent
31 | subcls.path = str(scp)
32 | except ValueError:
33 | subcls.path = None
34 | # then pass the subclass with its freshly computed path attribute to the original __new__ method, for
35 | # registration in subclasses and in the list of modules
36 | super(MetaModule, meta).__new__(meta, name, bases, clsdict, subcls)
37 | return subcls
38 |
39 | @property
40 | def base(self):
41 | """ Module's category. """
42 | return str(Path(self.fullpath).child) if self.category != "" else self.name
43 |
44 | @property
45 | def category(self):
46 | """ Module's category. """
47 | try:
48 | return str(Path(self.path).parts[0])
49 | except IndexError:
50 | return ""
51 |
52 | @property
53 | def fullpath(self):
54 | """ Full path of the module, that is, its path joined with its name. """
55 | return str(Path(self.path).joinpath(self.name))
56 |
57 | @property
58 | def help(self):
59 | """ Help message for the module. """
60 | return self.get_info(("name", "description"), "comments")
61 |
62 | @property
63 | def subpath(self):
64 | """ First child path of the module. """
65 | return str(Path(self.path).child)
66 |
67 | def search(self, text):
68 | """ Search for text in module's attributes. """
69 | t = text.lower()
70 | return any(t in "".join(v).lower() for v in self._metadata.values()) or t in self.fullpath
71 |
72 |
73 | class Module(Entity, metaclass=MetaModule):
74 | """ Main class handling console modules. """
75 | modules = PathBasedDict()
76 |
77 | @property
78 | def files(self):
79 | """ Shortcut to bound console's file manager instance. """
80 | return self.console.__class__._files
81 |
82 | @property
83 | def logger(self):
84 | """ Shortcut to bound console's logger instance. """
85 | return self.console.logger
86 |
87 | @property
88 | def store(self):
89 | """ Shortcut to bound console's store instance. """
90 | return self.console.store
91 |
92 | @property
93 | def workspace(self):
94 | """ Shortcut to the current workspace. """
95 | return self.console.workspace
96 |
97 | @classmethod
98 | def get_count(cls, path=None, **attrs):
99 | """ Count the number of modules under the given path and matching attributes. """
100 | return cls.modules.count(path, **attrs)
101 |
102 | @classmethod
103 | def get_help(cls, category=None):
104 | """ Display command's help, using its metaclass' properties. """
105 | uncat = {}
106 | for c, v in cls.modules.items():
107 | if not isinstance(v, dict):
108 | uncat[c] = v
109 | if category is None:
110 | categories = list(set(cls.modules.keys()) - set(uncat.keys()))
111 | if len(uncat) > 0:
112 | categories += ["uncategorized"]
113 | else:
114 | categories = [category]
115 | s, i = "", 0
116 | for c in categories:
117 | d = [["Name", "Path", "Enabled", "Description"]]
118 | for n, m in sorted((flatten_dict(cls.modules.get(c, {})) if c != "uncategorized" else uncat).items(),
119 | key=lambda x: x[1].name):
120 | e = ["N", "Y"][m.enabled]
121 | d.append([m.name, m.subpath, e, m.description])
122 | t = BorderlessTable(d, f"{c.capitalize()} modules")
123 | s += t.table + "\n\n"
124 | i += 1
125 | return "\n" + s.strip() + "\n" if i > 0 else ""
126 |
127 | @classmethod
128 | def get_list(cls):
129 | """ Get the list of modules' fullpath. """
130 | return sorted([m.fullpath for m in Module.subclasses if m.check()])
131 |
132 | @classmethod
133 | def get_modules(cls, path=None):
134 | """ Get the subdictionary of modules matching the given path. """
135 | return cls.modules[path or ""]
136 |
137 | @classmethod
138 | def get_summary(cls):
139 | """ Get the summary of module counts per category. """
140 | # display module stats
141 | m = []
142 | uncat = []
143 | for category in cls.modules.keys():
144 | if isinstance(cls.modules[category], MetaModule):
145 | uncat.append(cls.modules[category])
146 | continue
147 | l = "%d %s" % (Module.get_count(category), category)
148 | disabled = Module.get_count(category, enabled=False)
149 | if disabled > 0:
150 | l += " (%d disabled)" % disabled
151 | m.append(l)
152 | if len(uncat) > 0:
153 | l = "%d uncategorized" % len(uncat)
154 | disabled = len([u for u in uncat if not u.enabled])
155 | if disabled > 0:
156 | l += " (%d disabled)" % disabled
157 | m.append(l)
158 | if len(m) > 0:
159 | mlen = max(map(len, m))
160 | s = "\n"
161 | for line in m:
162 | s += f"\t-=[ {line: <{mlen}} ]=-\n"
163 | return s
164 | return ""
165 |
166 | @classmethod
167 | def register_module(cls, subcls):
168 | """ Register a Module subclass to the dictionary of modules. """
169 | if subcls.path is None:
170 | return # do not consider orphan modules
171 | cls.modules[subcls.path, subcls.name] = subcls
172 |
173 | @classmethod
174 | def unregister_module(cls, subcls):
175 | """ Unregister a Module subclass from the dictionary of modules. """
176 | p, n = subcls.path, subcls.name
177 | try:
178 | del cls.modules[n if p == "." else (p, n)]
179 | except KeyError:
180 | pass
181 | for M in Module.subclasses:
182 | if p == M.path and n == M.name:
183 | Module.subclasses.remove(M)
184 | break
185 | logger.detail(f"Unregistered module '{p}/{n}'")
186 |
187 | @classmethod
188 | def unregister_modules(cls, *subcls):
189 | """ Unregister Module subclasses from the dictionary of modules. """
190 | for sc in subcls:
191 | cls.unregister_module(sc)
192 |
193 | def _feedback(self, success, failmsg):
194 | """ Dummy feedback method using a fail-message formatted with the "not" keyword (to be replaced by a null string
195 | in case of success). """
196 | if success is None:
197 | return
198 | elif success:
199 | self.logger.success(failmsg.replace("not ", ""))
200 | else:
201 | self.logger.failure(failmsg)
202 |
203 |
--------------------------------------------------------------------------------
/tests/__utils__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Test utility functions.
4 |
5 | """
6 | import os
7 | import pytest
8 | import re
9 | import sys
10 | from peewee import DoesNotExist
11 | from subprocess import Popen, PIPE
12 | from tinyscript.helpers import ClassRegistry, Path
13 | from unittest import TestCase
14 | from unittest.mock import patch
15 |
16 | from sploitkit import *
17 | from sploitkit.__info__ import *
18 | from sploitkit.core.entity import load_entities, Entity
19 |
20 | from testsploit.main import MySploitConsole
21 |
22 |
23 | __all__ = ["CONSOLE", "execute", "patch", "rcfile", "reset_entities", "BaseModel", "Command", "Console", "DoesNotExist",
24 | "Entity", "Model", "Module", "StoreExtension", "TestCase"]
25 |
26 |
27 | try:
28 | CONSOLE = MySploitConsole()
29 | CONSOLE.config['APP_FOLDER'] = "testsploit/workspace"
30 | CONSOLE.config['WORKSPACE'] = "testsploit/workspace"
31 | except:
32 | CONSOLE = MySploitConsole.parent
33 | FILE = ".commands.rc"
34 |
35 |
36 | def execute(*commands):
37 | """ Execute commands. """
38 | c = list(commands) + ["exit"]
39 | p = os.path.join("testsploit", FILE)
40 | with open(p, 'w') as f:
41 | f.write("\n".join(c))
42 | r = rcfile(FILE)
43 | os.remove(p)
44 | return r
45 |
46 |
47 | def rcfile(rcfile, debug=False):
48 | """ Execute commands using a rcfile. """
49 | p = os.path.join("testsploit", rcfile)
50 | if not os.path.isfile(p):
51 | raise ValueError("Bad rc file")
52 | cmd = "cd testsploit && python3 main.py --rcfile %s" % rcfile
53 | if debug:
54 | cmd += " -v"
55 | out, err = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
56 | out = re.split(r"\+{10,}\s.*?\s\+{10,}", out.decode())[1:]
57 | err = "\n".join(l for l in err.decode().splitlines() if not l.startswith("Warning: ") and \
58 | all(x not in l for x in ["DeprecationWarning: ", "import pkg_resources", "There are some issues"])).strip()
59 | c = []
60 | with open(p) as f:
61 | for l in f:
62 | l = l.strip()
63 | try:
64 | c.append((l, re.sub(r"\x1b\[\??\d{1,3}[hm]", "", out.pop(0)).strip()))
65 | except IndexError:
66 | c.append((l, None))
67 | if c[-1][0] == "exit":
68 | c.pop(-1)
69 | return c, err
70 |
71 |
72 | def reset_entities(*entities):
73 | entities = list(entities) or [BaseModel, Command, Console, Model, Module, StoreExtension]
74 | Entity._subclasses = ClassRegistry()
75 | load_entities(entities)
76 |
77 |
--------------------------------------------------------------------------------
/tests/test_base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Core entity assets' tests.
4 |
5 | """
6 | from __utils__ import *
7 |
8 |
9 | class TestBase(TestCase):
10 | def test_general_commands(self):
11 | self.assertRaises(SystemExit, Entity.get_subclass(Command, "Back")().run)
12 | self.assertRaises(SystemExit, Entity.get_subclass(Command, "Exit")().run)
13 | for Help in Entity.get_subclass(Command, "Help"):
14 | if Help.level == "general":
15 | self.assertIsNone(Help().run())
16 | search = Entity.get_subclass(Command, "Search")()
17 | self.assertIsNone(search.run("does_not_exist"))
18 | self.assertIsNone(search.run("first"))
19 | for Show in Entity.get_subclass(Command, "Show"):
20 | if Show.level == "root":
21 | Show.keys += ["issues"]
22 | Show().set_keys()
23 | for k in Show.keys + ["issues"]:
24 | self.assertIsNotNone(Show().complete_values(k))
25 | self.assertIsNone(Show().run(k))
26 | if k == "options":
27 | self.assertIsNone(Show().run(k, "DEBUG"))
28 | break
29 | Set = Entity.get_subclass(Command, "Set")
30 | keys = list(Set().complete_keys())
31 | self.assertTrue(len(keys) > 0)
32 | self.assertIsNotNone(Set().complete_values(keys[0]))
33 | self.assertIsNotNone(Set().complete_values("WORKSPACE"))
34 | self.assertRaises(ValueError, Set().validate, "BAD", "whatever")
35 | self.assertRaises(ValueError, Set().validate, "WORKSPACE", None)
36 | self.assertRaises(ValueError, Set().validate, "DEBUG", "whatever")
37 | self.assertIsNone(Set().run("DEBUG", "false"))
38 | Unset = Entity.get_subclass(Command, "Unset")
39 | self.assertTrue(len(list(Unset().complete_values())) > 0)
40 | self.assertRaises(ValueError, Unset().validate, "BAD")
41 | self.assertRaises(ValueError, Unset().validate, "WORKSPACE")
42 | self.assertRaises(ValueError, Unset().run, "DEBUG")
43 | self.assertIsNone(Set().run("DEBUG", "false"))
44 |
45 | def test_root_commands(self):
46 | for Help in Entity.get_subclass(Command, "Help"):
47 | if Help.level == "root":
48 | self.assertIsNone(Help().validate())
49 | self.assertRaises(ValueError, Help().validate, "BAD")
50 | self.assertIsNone(Help().run())
51 | for k in Help().keys:
52 | for v in Help().complete_values(k):
53 | self.assertIsNone(Help().run(k, v))
54 | break
55 |
56 |
--------------------------------------------------------------------------------
/tests/test_components.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Core entity assets' tests.
4 |
5 | """
6 | from sploitkit.core.components import *
7 |
8 | from __utils__ import *
9 |
10 |
11 | class TestComponents(TestCase):
12 | def test_store(self):
13 | s = CONSOLE._storage
14 | self.assertTrue(isinstance(s.extensions, list))
15 | s = CONSOLE.store
16 | self.assertTrue(isinstance(s.basemodels, list))
17 | self.assertTrue(isinstance(s.models, list))
18 | self.assertFalse(s.volatile)
19 | try:
20 | s.get_user(username="test")
21 | except DoesNotExist:
22 | s.set_user(username="test")
23 | self.assertEqual(s._last_snapshot, 0)
24 | self.assertIsNone(s.snapshot())
25 | self.assertEqual(s._last_snapshot, 1)
26 | self.assertIsNone(s.snapshot(False))
27 | self.assertEqual(s._last_snapshot, 0)
28 | self.assertIsNone(s.snapshot(False))
29 |
30 |
--------------------------------------------------------------------------------
/tests/test_console.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Core entity assets' tests.
4 |
5 | """
6 | from sploitkit.core.components import *
7 |
8 | from __utils__ import *
9 |
10 |
11 | class TestConsole(TestCase):
12 | def test_console(self):
13 | self.assertIsNotNone(CONSOLE._get_tokens("help"))
14 | self.assertIsNone(CONSOLE.play("help"))
15 | r = CONSOLE.play("help", "show modules", capture=True)
16 | # check the presence of some commands from the base
17 | for cmd in ["?", "exit", "quit", "unset", "use", "record", "replay", "help", "show", "select"]:
18 | self.assertIn(" " + cmd + " ", r[0][1])
19 | # check that some particular commands are missing
20 | for cmd in ["pydbg", "memory", "dict"]:
21 | self.assertNotIn(" " + cmd + " ", r[0][1])
22 |
23 |
--------------------------------------------------------------------------------
/tests/test_entity.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Core entity assets' tests.
4 |
5 | """
6 | from sploitkit.core.entity import load_entities, set_metadata
7 | from tinyscript.helpers import parse_docstring
8 |
9 | from __utils__ import *
10 |
11 |
12 | class TestEntity(TestCase):
13 | def test_load_entities(self):
14 | self.assertIn(Command, list(Entity._subclasses.keys()))
15 | self.assertIn(Model, list(Entity._subclasses.keys()))
16 | self.assertIn(Module, list(Entity._subclasses.keys()))
17 | self.assertTrue(len(Command.subclasses) > 0)
18 | self.assertTrue(len(Model.subclasses) > 0)
19 | l = len(Module.subclasses)
20 | self.assertTrue(l > 0)
21 | M = Module.subclasses[0]
22 | del Entity._subclasses[Module]
23 | load_entities([Module], CONSOLE._root.dirname.joinpath("modules"), exclude={'module': [M]})
24 | self.assertTrue(len(Module.subclasses) > 0)
25 | self.assertRaises(ValueError, Entity._subclasses.__getitem__, (Module, M.__name__))
26 | del Entity._subclasses[Module]
27 | load_entities([Module], CONSOLE._root.dirname.joinpath("modules"))
28 | self.assertTrue(len(Module.subclasses) > 0)
29 | self.assertIn(Entity._subclasses[Module, M.__name__], Module.subclasses)
30 | load_entities([Console], CONSOLE._root, backref={'command': ["console"]})
31 |
32 | def test_set_metadata(self):
33 | # check that every subclass has its own description, and not the one of its entity class
34 | for cls in Entity._subclasses.keys():
35 | for subcls in cls.subclasses:
36 | self.assertNotEqual(subcls.__doc__, cls.__doc__)
37 | # now, alter a Command subclass to test for set_metadata
38 | C = Command.subclasses[0]
39 | C.meta = {'options': ["BAD_OPTION"]}
40 | self.assertRaises(ValueError, set_metadata, C, parse_docstring)
41 | C.meta = {'options': [("test", "default", False, "description")]}
42 | set_metadata(C, parse_docstring)
43 | M = Module.subclasses[0]
44 | B = M.__base__
45 | B.meta = {'test': "test"}
46 | M._inherit_metadata = True
47 | set_metadata(M, parse_docstring)
48 |
49 |
--------------------------------------------------------------------------------
/tests/test_model.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Model entity tests.
4 |
5 | """
6 | from __utils__ import *
7 |
8 |
9 | class TestModule(TestCase):
10 | def test_model(self):
11 | self.assertTrue(len(Model.subclasses) > 0)
12 | self.assertTrue(len(StoreExtension.subclasses) == 0) # sploitkit's base has no StoreExtension at this moment
13 | self.assertIsNotNone(repr(Model.subclasses[0]))
14 |
15 |
--------------------------------------------------------------------------------
/tests/test_module.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Module entity tests.
4 |
5 | """
6 | from __utils__ import *
7 |
8 |
9 | class TestModule(TestCase):
10 | def test_module_attributes(self):
11 | for m in Module.subclasses:
12 | m.console = CONSOLE # use the main console ; should normally be a ModuleConsole
13 | self.assertIsNotNone(m.base)
14 | for a in ["files", "logger", "store", "workspace"]:
15 | self.assertIsNotNone(getattr(m(), a))
16 |
17 | def test_module_help(self):
18 | for c in [None, "uncategorized", "does_not_exist"]:
19 | self.assertIsNotNone(Module.get_help(c))
20 | M = Module.subclasses[0]
21 | self.assertIsNone(M()._feedback(None, ""))
22 | self.assertIsNone(M()._feedback(True, "test"))
23 | self.assertIsNone(M()._feedback(False, "test"))
24 |
25 | def test_module_registry(self):
26 | self.assertIsNotNone(Module.get_list())
27 | self.assertIsNotNone(Module.get_modules())
28 | class FakeModule(Module):
29 | path = "fake_module"
30 | name = "fake_module"
31 | self.assertIsNone(Module.unregister_module(FakeModule))
32 | class OrphanModule(Module):
33 | path = None
34 | name = "orphan_module"
35 | self.assertIsNone(Module.register_module(OrphanModule))
36 | self.assertNotIn(OrphanModule, Module.subclasses)
37 |
38 |
--------------------------------------------------------------------------------
/tests/test_scenarios.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | """Scenario-based tests of a Sploitkit application.
4 |
5 | """
6 | from __utils__ import *
7 |
8 |
9 | class TestScenarios(TestCase):
10 | def test_bad_command(self):
11 | # single bad command
12 | out, err = execute("bad")
13 | self.assertNotEqual(err, "")
14 | self.assertEqual(out[0][1], "")
15 | # successful command before the failing one
16 | out, err = execute("help", "bad")
17 | self.assertNotEqual(err, "")
18 | self.assertNotEqual(out[0][1], "")
19 | self.assertEqual(out[1][1], "")
20 | # failing command before the successful one
21 | out, err = execute("bad", "help")
22 | self.assertNotEqual(err, "")
23 | self.assertEqual(out[0][1], "")
24 | self.assertIsNone(out[1][1])
25 |
26 | def test_help(self):
27 | out, err = execute("help")
28 | self.assertEqual(err, "")
29 | self.assertNotEqual(out, "")
30 | out, err = execute("?")
31 | self.assertEqual(err, "")
32 | self.assertNotEqual(out, "")
33 |
34 | def test_set_debug(self):
35 | out, err = execute("set DEBUG true", "help")
36 |
37 | def test_show_modules(self):
38 | out, err = execute("show modules")
39 | self.assertEqual(err, "")
40 | self.assertIn("modules", out[0][1])
41 | self.assertIn("my_first_module", out[0][1])
42 |
43 | def test_show_options(self):
44 | out, err = execute("show options")
45 | self.assertEqual(err, "")
46 | self.assertIn("Console options", out[0][1])
47 | self.assertIn("APP_FOLDER", out[0][1])
48 | self.assertIn("DEBUG", out[0][1])
49 | self.assertIn("WORKSPACE", out[0][1])
50 |
51 | def test_show_projects(self):
52 | out, err = execute("show projects")
53 | self.assertEqual(err, "")
54 | self.assertIn("Existing projects", out[0][1])
55 |
56 |
--------------------------------------------------------------------------------
/testsploit/README:
--------------------------------------------------------------------------------
1 | # {}
2 |
3 | #TODO: Fill in the README
--------------------------------------------------------------------------------
/testsploit/commands/commands.py:
--------------------------------------------------------------------------------
1 | from sploitkit import *
2 |
3 |
4 | class CommandWithOneArg(Command):
5 | """ Description here """
6 | level = "module"
7 | single_arg = True
8 |
9 | def complete_values(self):
10 | #TODO: compute the list of possible values
11 | return []
12 |
13 | def run(self):
14 | #TODO: compute results here
15 | pass
16 |
17 | def validate(self, value):
18 | #TODO: validate the input value
19 | if value not in self.complete_values():
20 | raise ValueError("invalid value")
21 |
22 |
23 | class CommandWithTwoArgs(Command):
24 | """ Description here """
25 | level = "module"
26 |
27 | def complete_keys(self):
28 | #TODO: compute the list of possible keys
29 | return []
30 |
31 | def complete_values(self, key=None):
32 | #TODO: compute the list of possible values taking the key into account
33 | return []
34 |
35 | def run(self):
36 | #TODO: compute results here
37 | pass
38 |
--------------------------------------------------------------------------------
/testsploit/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import site
3 | site.addsitedir("../src")
4 |
5 | from sploitkit import FrameworkConsole
6 | from tinyscript import *
7 |
8 |
9 | class MySploitConsole(FrameworkConsole):
10 | #TODO: set your console attributes
11 | pass
12 |
13 |
14 | if __name__ == '__main__':
15 | parser.add_argument("-d", "--dev", action="store_true", help="enable development mode")
16 | parser.add_argument("-r", "--rcfile", type=ts.file_exists, help="execute commands from a rcfile")
17 | initialize()
18 | c = MySploitConsole(
19 | "MySploit",
20 | #TODO: configure your console settings
21 | dev=args.dev,
22 | )
23 | c.rcfile(args.rcfile) if args.rcfile else c.start()
24 |
--------------------------------------------------------------------------------
/testsploit/modules/modules.py:
--------------------------------------------------------------------------------
1 | from sploitkit import *
2 |
3 |
4 | class MyFirstModule(Module):
5 | """ Description here
6 |
7 | Author: your name (your email)
8 | Version: 1.0
9 | """
10 | def run(self):
11 | pass
12 |
13 |
14 | class MySecondModule(Module):
15 | """ Description here
16 |
17 | Author: your name (your email)
18 | Version: 1.0
19 | """
20 | def run(self):
21 | pass
22 |
--------------------------------------------------------------------------------
/testsploit/requirements.txt:
--------------------------------------------------------------------------------
1 | sploitkit>=0.5.8
2 |
--------------------------------------------------------------------------------
/testsploit/workspace/store.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhondta/python-sploitkit/689597358ca12fe4076a8d9faa86b345304955d1/testsploit/workspace/store.db
--------------------------------------------------------------------------------