xpl-core 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xpl_core-0.3.0/LICENSE +123 -0
- xpl_core-0.3.0/PKG-INFO +24 -0
- xpl_core-0.3.0/README.md +68 -0
- xpl_core-0.3.0/pyproject.toml +70 -0
- xpl_core-0.3.0/setup.cfg +4 -0
- xpl_core-0.3.0/tests/test_config.py +76 -0
- xpl_core-0.3.0/tests/test_credential.py +129 -0
- xpl_core-0.3.0/tests/test_dispatcher.py +152 -0
- xpl_core-0.3.0/tests/test_finding.py +64 -0
- xpl_core-0.3.0/tests/test_handler.py +132 -0
- xpl_core-0.3.0/tests/test_listener.py +34 -0
- xpl_core-0.3.0/tests/test_module.py +182 -0
- xpl_core-0.3.0/tests/test_pool.py +74 -0
- xpl_core-0.3.0/tests/test_registry.py +165 -0
- xpl_core-0.3.0/tests/test_session.py +158 -0
- xpl_core-0.3.0/tests/test_session_manager.py +182 -0
- xpl_core-0.3.0/tests/test_shell_engine.py +162 -0
- xpl_core-0.3.0/tests/test_shells.py +136 -0
- xpl_core-0.3.0/tests/test_stager.py +131 -0
- xpl_core-0.3.0/xpl_core/__init__.py +3 -0
- xpl_core-0.3.0/xpl_core/config.py +74 -0
- xpl_core-0.3.0/xpl_core/credential.py +112 -0
- xpl_core-0.3.0/xpl_core/dispatcher.py +98 -0
- xpl_core-0.3.0/xpl_core/module.py +168 -0
- xpl_core-0.3.0/xpl_core/pool.py +87 -0
- xpl_core-0.3.0/xpl_core/py.typed +0 -0
- xpl_core-0.3.0/xpl_core/registry.py +127 -0
- xpl_core-0.3.0/xpl_core/schemas/__init__.py +81 -0
- xpl_core-0.3.0/xpl_core/schemas/xpl_finding_v1.json +105 -0
- xpl_core-0.3.0/xpl_core/session.py +173 -0
- xpl_core-0.3.0/xpl_core/shells/__init__.py +85 -0
- xpl_core-0.3.0/xpl_core/shells/dns_tunnel_shell.py +275 -0
- xpl_core-0.3.0/xpl_core/shells/handler.py +214 -0
- xpl_core-0.3.0/xpl_core/shells/http_poll_shell.py +252 -0
- xpl_core-0.3.0/xpl_core/shells/icmp_covert_shell.py +226 -0
- xpl_core-0.3.0/xpl_core/shells/internal_shell.py +296 -0
- xpl_core-0.3.0/xpl_core/shells/listener.py +204 -0
- xpl_core-0.3.0/xpl_core/shells/meterpreter_bridge.py +245 -0
- xpl_core-0.3.0/xpl_core/shells/mqtt_shell.py +219 -0
- xpl_core-0.3.0/xpl_core/shells/raw_tcp_shell.py +180 -0
- xpl_core-0.3.0/xpl_core/shells/raw_udp_shell.py +252 -0
- xpl_core-0.3.0/xpl_core/shells/session_manager.py +299 -0
- xpl_core-0.3.0/xpl_core/shells/shell_engine.py +230 -0
- xpl_core-0.3.0/xpl_core/shells/stager.py +357 -0
- xpl_core-0.3.0/xpl_core.egg-info/PKG-INFO +24 -0
- xpl_core-0.3.0/xpl_core.egg-info/SOURCES.txt +47 -0
- xpl_core-0.3.0/xpl_core.egg-info/dependency_links.txt +1 -0
- xpl_core-0.3.0/xpl_core.egg-info/requires.txt +14 -0
- xpl_core-0.3.0/xpl_core.egg-info/top_level.txt +1 -0
xpl_core-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# XPL-Suite Security Research License (XSRL) v1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Andre Henrike de Oliveira (@mrhenrike) / Uniao Geek
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
## 1. Definitions
|
|
7
|
+
|
|
8
|
+
- "Software" means the source code, documentation, binaries, scripts, configuration
|
|
9
|
+
files, and any other materials included in this repository.
|
|
10
|
+
- "Author" means the original copyright holder: Andre Henrike de Oliveira (@mrhenrike).
|
|
11
|
+
- "Original Repository" means the source repository hosted at
|
|
12
|
+
https://github.com/mrhenrike/xpl-core from which the Software originates.
|
|
13
|
+
- "Upstream Credits" means the authors and licenses of third-party components,
|
|
14
|
+
libraries, or code referenced in the Software, as documented in source file headers,
|
|
15
|
+
CREDITS files, or README attribution sections.
|
|
16
|
+
- "Authorized Security Testing" means penetration testing, vulnerability assessment,
|
|
17
|
+
red team exercises, purple team exercises, security auditing, or similar activities
|
|
18
|
+
conducted with explicit written permission from the owner of the target system(s).
|
|
19
|
+
|
|
20
|
+
## 2. Grant of Rights
|
|
21
|
+
|
|
22
|
+
Subject to the terms and conditions of this License, the Author grants you a
|
|
23
|
+
non-exclusive, worldwide, royalty-free license to:
|
|
24
|
+
|
|
25
|
+
a) **Use** the Software for educational purposes, academic research, security studies,
|
|
26
|
+
and Authorized Security Testing;
|
|
27
|
+
b) **Copy and reproduce** the Software;
|
|
28
|
+
c) **Modify** the Software and create derivative works;
|
|
29
|
+
d) **Fork** the Software and maintain independent copies;
|
|
30
|
+
e) **Distribute** the Software and derivative works, provided all conditions in
|
|
31
|
+
Section 3 are met.
|
|
32
|
+
|
|
33
|
+
## 3. Conditions
|
|
34
|
+
|
|
35
|
+
Any use, copy, modification, fork, or distribution of the Software must comply with
|
|
36
|
+
ALL of the following conditions:
|
|
37
|
+
|
|
38
|
+
### 3.1 Attribution
|
|
39
|
+
|
|
40
|
+
a) You must retain the Author's original copyright notice and attribution in all
|
|
41
|
+
copies and derivative works.
|
|
42
|
+
b) You must retain all Upstream Credits, including but not limited to: original author
|
|
43
|
+
names, license notices, and attribution statements of third-party code used as a
|
|
44
|
+
base or reference for the Software.
|
|
45
|
+
c) Modified files must carry a prominent notice stating that you changed the files and
|
|
46
|
+
the date of any change.
|
|
47
|
+
d) Derivative works must include a clear statement identifying the original Software
|
|
48
|
+
and its Author.
|
|
49
|
+
e) Any copy, fork, or derivative work must include a visible reference to the Original
|
|
50
|
+
Repository URL (https://github.com/mrhenrike/xpl-core) in the project's
|
|
51
|
+
README or equivalent documentation, so that users can trace the Software back to
|
|
52
|
+
its source.
|
|
53
|
+
f) The Author's name and credits must not be removed, obscured, or replaced in any
|
|
54
|
+
copy, fork, or derivative work. The Author must remain credited as the original
|
|
55
|
+
creator in all circumstances.
|
|
56
|
+
|
|
57
|
+
### 3.2 Same License
|
|
58
|
+
|
|
59
|
+
a) Any distribution of the Software or derivative works must be under this same
|
|
60
|
+
License (XSRL v1.0).
|
|
61
|
+
b) You may not apply any additional restrictions or alternative license terms to the
|
|
62
|
+
Software or derivative works.
|
|
63
|
+
c) You may not sublicense the Software under different terms.
|
|
64
|
+
|
|
65
|
+
### 3.3 Non-Commercial Use Only
|
|
66
|
+
|
|
67
|
+
a) The Software may not be used, in whole or in part, for commercial purposes.
|
|
68
|
+
b) "Commercial purposes" includes but is not limited to: selling the Software or
|
|
69
|
+
derivative works, incorporating the Software into commercial products or services,
|
|
70
|
+
using the Software to generate revenue directly or indirectly, and offering the
|
|
71
|
+
Software as part of a paid service.
|
|
72
|
+
c) Use in non-profit organizations, academic institutions, and personal projects is
|
|
73
|
+
permitted, provided no commercial gain is derived.
|
|
74
|
+
|
|
75
|
+
### 3.4 Ethical Use Only
|
|
76
|
+
|
|
77
|
+
a) The Software may only be used for:
|
|
78
|
+
- Educational purposes and academic research;
|
|
79
|
+
- Security studies, training, and skill development;
|
|
80
|
+
- Authorized Security Testing (as defined in Section 1);
|
|
81
|
+
- Capture The Flag (CTF) competitions and security labs;
|
|
82
|
+
- Defensive security, incident response, and threat analysis.
|
|
83
|
+
|
|
84
|
+
b) The Software may NOT be used for:
|
|
85
|
+
- Unauthorized access to systems, networks, or data;
|
|
86
|
+
- Any activity that violates applicable laws or regulations;
|
|
87
|
+
- Malicious purposes, including but not limited to: deploying malware against
|
|
88
|
+
unauthorized targets, conducting denial-of-service attacks, unauthorized
|
|
89
|
+
surveillance, data theft, or any form of cybercrime;
|
|
90
|
+
- Targeting individuals, organizations, or infrastructure without explicit
|
|
91
|
+
authorization;
|
|
92
|
+
- Any purpose that causes harm to individuals or organizations.
|
|
93
|
+
|
|
94
|
+
## 4. No Warranty
|
|
95
|
+
|
|
96
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
97
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
98
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
99
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
100
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
101
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
102
|
+
|
|
103
|
+
## 5. Limitation of Liability
|
|
104
|
+
|
|
105
|
+
The Author shall not be held responsible for any misuse of the Software. Users are
|
|
106
|
+
solely responsible for ensuring their use of the Software complies with all applicable
|
|
107
|
+
laws, regulations, and ethical standards.
|
|
108
|
+
|
|
109
|
+
## 6. Termination
|
|
110
|
+
|
|
111
|
+
This License and the rights granted hereunder will terminate automatically if you fail
|
|
112
|
+
to comply with any of the terms and conditions of this License. Upon termination, you
|
|
113
|
+
must cease all use and destroy all copies of the Software in your possession.
|
|
114
|
+
|
|
115
|
+
## 7. Severability
|
|
116
|
+
|
|
117
|
+
If any provision of this License is held to be unenforceable, such provision shall be
|
|
118
|
+
reformed only to the extent necessary to make it enforceable, and the remaining
|
|
119
|
+
provisions shall continue in full force and effect.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
SPDX-License-Identifier: LicenseRef-XSRL-1.0
|
xpl_core-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xpl-core
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: XPL-Suite shared schemas, policies and registry
|
|
5
|
+
Author-email: Andre Henrike <safelabs.support@visioncybersecurity.com>
|
|
6
|
+
License: XSRL-1.0
|
|
7
|
+
Keywords: security,pentest,schema,findings
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: crypto
|
|
14
|
+
Requires-Dist: cryptography>=42.0; extra == "crypto"
|
|
15
|
+
Provides-Extra: mqtt
|
|
16
|
+
Requires-Dist: paho-mqtt>=2.0; extra == "mqtt"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
22
|
+
Requires-Dist: cryptography>=42.0; extra == "dev"
|
|
23
|
+
Requires-Dist: paho-mqtt>=2.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
xpl_core-0.3.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# xpl-core
|
|
2
|
+
|
|
3
|
+
Shared schemas, policies, registry and canonical modules for [XPL-Suite](https://github.com/mrhenrike) forges.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`xpl-core` provides the common infrastructure that all XPL-Suite offensive security forges depend on:
|
|
8
|
+
|
|
9
|
+
- **Finding schema** (`xpl.finding.v1`) — standardized vulnerability finding format
|
|
10
|
+
- **Shell infrastructure** — 8 transport engines (TCP, UDP, ICMP, DNS, HTTP, MQTT, Meterpreter, IWP), handler, listener, stager, session manager
|
|
11
|
+
- **Credential models** — `BaseCredential`, `ADCredential`, `CloudCredential`, `NetworkCredential`
|
|
12
|
+
- **SmartPool** — adaptive thread/process pool with `map_unordered` and context manager
|
|
13
|
+
- **HostSession** — persistent per-host assessment tracking with `ModuleResult`
|
|
14
|
+
- **ForgeConfig** — JSON-based per-forge configuration with defaults and persistence
|
|
15
|
+
- **Module** and **Registry** — plugin-style module loading and dispatch
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install xpl-core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Optional extras:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install xpl-core[crypto] # cryptography
|
|
27
|
+
pip install xpl-core[mqtt] # paho-mqtt
|
|
28
|
+
pip install xpl-core[dev] # all dev tools
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from xpl_core.credential import ADCredential, CloudCredential
|
|
35
|
+
from xpl_core.pool import SmartPool, PoolStrategy
|
|
36
|
+
from xpl_core.session import SessionManager, HostSession, ModuleResult
|
|
37
|
+
from xpl_core.config import ForgeConfig
|
|
38
|
+
|
|
39
|
+
# Credential handling
|
|
40
|
+
cred = ADCredential(username="admin", nt_hash="31d6cfe0d16ae931b73c59d7e0c089c0", domain="CORP")
|
|
41
|
+
print(cred.impacket_hash)
|
|
42
|
+
|
|
43
|
+
# Adaptive pool
|
|
44
|
+
with SmartPool(max_workers=16) as pool:
|
|
45
|
+
results = pool.map_unordered(scan_target, targets)
|
|
46
|
+
|
|
47
|
+
# Session tracking
|
|
48
|
+
mgr = SessionManager("/tmp/sessions")
|
|
49
|
+
session = mgr.get_or_create("fw01", "10.0.0.1", vendor="Fortinet")
|
|
50
|
+
session.add_result(ModuleResult(module_path="exploits/firewalls/fortigate_rce", vulnerable=True))
|
|
51
|
+
mgr.save(session)
|
|
52
|
+
|
|
53
|
+
# Configuration
|
|
54
|
+
cfg = ForgeConfig("embedxpl", defaults={"threads": 10, "timeout": 30})
|
|
55
|
+
print(cfg.get("threads"))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python >= 3.9
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
XSRL-1.0. See [LICENSE](LICENSE).
|
|
65
|
+
|
|
66
|
+
## Author
|
|
67
|
+
|
|
68
|
+
André Henrique ([@mrhenrike](https://github.com/mrhenrike)) | União Geek
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=72", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xpl-core"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "XPL-Suite shared schemas, policies and registry"
|
|
9
|
+
license = { text = "XSRL-1.0" }
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Andre Henrike", email = "safelabs.support@visioncybersecurity.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = ["security", "pentest", "schema", "findings"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Information Technology",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["xpl_core*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
"xpl_core" = ["schemas/*.json", "py.typed"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
crypto = ["cryptography>=42.0"]
|
|
30
|
+
mqtt = ["paho-mqtt>=2.0"]
|
|
31
|
+
dev = [
|
|
32
|
+
"mypy>=1.11",
|
|
33
|
+
"ruff>=0.6",
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-cov>=5.0",
|
|
36
|
+
"cryptography>=42.0",
|
|
37
|
+
"paho-mqtt>=2.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
strict = true
|
|
42
|
+
warn_return_any = true
|
|
43
|
+
warn_unused_configs = true
|
|
44
|
+
disallow_untyped_defs = true
|
|
45
|
+
disallow_incomplete_defs = true
|
|
46
|
+
check_untyped_defs = true
|
|
47
|
+
no_implicit_optional = true
|
|
48
|
+
warn_redundant_casts = true
|
|
49
|
+
warn_unused_ignores = true
|
|
50
|
+
show_error_codes = true
|
|
51
|
+
python_version = "3.9"
|
|
52
|
+
|
|
53
|
+
[[tool.mypy.overrides]]
|
|
54
|
+
module = ["paho.*"]
|
|
55
|
+
ignore_missing_imports = true
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py39"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "W", "F", "I", "UP", "B", "SIM", "TCH", "RUF"]
|
|
63
|
+
ignore = ["E501"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.isort]
|
|
66
|
+
known-first-party = ["xpl_core"]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
addopts = "-v --tb=short"
|
xpl_core-0.3.0/setup.cfg
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for xpl_core.config ForgeConfig."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from xpl_core.config import ForgeConfig
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestForgeConfig:
|
|
14
|
+
def test_defaults(self, tmp_path: Path) -> None:
|
|
15
|
+
cfg = ForgeConfig("test", config_dir=tmp_path, defaults={"threads": 10})
|
|
16
|
+
assert cfg.get("threads") == 10
|
|
17
|
+
assert cfg.get("nonexistent") is None
|
|
18
|
+
assert cfg.get("nonexistent", 42) == 42
|
|
19
|
+
|
|
20
|
+
def test_set_and_get(self, tmp_path: Path) -> None:
|
|
21
|
+
cfg = ForgeConfig("test", config_dir=tmp_path)
|
|
22
|
+
cfg.set_val("timeout", 30)
|
|
23
|
+
assert cfg.get("timeout") == 30
|
|
24
|
+
|
|
25
|
+
def test_persistence(self, tmp_path: Path) -> None:
|
|
26
|
+
cfg = ForgeConfig("test", config_dir=tmp_path)
|
|
27
|
+
cfg.set_val("key", "value")
|
|
28
|
+
|
|
29
|
+
cfg2 = ForgeConfig("test", config_dir=tmp_path)
|
|
30
|
+
assert cfg2.get("key") == "value"
|
|
31
|
+
|
|
32
|
+
def test_delete(self, tmp_path: Path) -> None:
|
|
33
|
+
cfg = ForgeConfig("test", config_dir=tmp_path)
|
|
34
|
+
cfg.set_val("key", "value")
|
|
35
|
+
assert cfg.delete("key") is True
|
|
36
|
+
assert cfg.get("key") is None
|
|
37
|
+
assert cfg.delete("nonexistent") is False
|
|
38
|
+
|
|
39
|
+
def test_reset(self, tmp_path: Path) -> None:
|
|
40
|
+
cfg = ForgeConfig("test", config_dir=tmp_path, defaults={"a": 1})
|
|
41
|
+
cfg.set_val("b", 2)
|
|
42
|
+
cfg.reset()
|
|
43
|
+
assert cfg.get("b") is None
|
|
44
|
+
assert cfg.get("a") == 1
|
|
45
|
+
|
|
46
|
+
def test_all_keys(self, tmp_path: Path) -> None:
|
|
47
|
+
cfg = ForgeConfig("test", config_dir=tmp_path, defaults={"a": 1, "c": 3})
|
|
48
|
+
cfg.set_val("b", 2)
|
|
49
|
+
assert cfg.all_keys() == ["a", "b", "c"]
|
|
50
|
+
|
|
51
|
+
def test_to_dict(self, tmp_path: Path) -> None:
|
|
52
|
+
cfg = ForgeConfig("test", config_dir=tmp_path, defaults={"a": 1})
|
|
53
|
+
cfg.set_val("b", 2)
|
|
54
|
+
cfg.set_val("a", 99)
|
|
55
|
+
d = cfg.to_dict()
|
|
56
|
+
assert d["a"] == 99
|
|
57
|
+
assert d["b"] == 2
|
|
58
|
+
|
|
59
|
+
def test_path_property(self, tmp_path: Path) -> None:
|
|
60
|
+
cfg = ForgeConfig("myforge", config_dir=tmp_path)
|
|
61
|
+
assert cfg.path == tmp_path / "myforge.json"
|
|
62
|
+
|
|
63
|
+
def test_forge_name_property(self, tmp_path: Path) -> None:
|
|
64
|
+
cfg = ForgeConfig("myforge", config_dir=tmp_path)
|
|
65
|
+
assert cfg.forge_name == "myforge"
|
|
66
|
+
|
|
67
|
+
def test_corrupted_file(self, tmp_path: Path) -> None:
|
|
68
|
+
(tmp_path / "broken.json").write_text("{bad", encoding="utf-8")
|
|
69
|
+
cfg = ForgeConfig("broken", config_dir=tmp_path)
|
|
70
|
+
assert cfg.to_dict() == {}
|
|
71
|
+
|
|
72
|
+
def test_override_default(self, tmp_path: Path) -> None:
|
|
73
|
+
cfg = ForgeConfig("test", config_dir=tmp_path, defaults={"x": "default"})
|
|
74
|
+
assert cfg.get("x") == "default"
|
|
75
|
+
cfg.set_val("x", "custom")
|
|
76
|
+
assert cfg.get("x") == "custom"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Tests for xpl_core.credential canonical models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from xpl_core.credential import (
|
|
10
|
+
ADCredential,
|
|
11
|
+
BaseCredential,
|
|
12
|
+
CloudCredential,
|
|
13
|
+
NetworkCredential,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestBaseCredential:
|
|
18
|
+
def test_defaults(self) -> None:
|
|
19
|
+
c = BaseCredential()
|
|
20
|
+
assert c.username is None
|
|
21
|
+
assert c.password is None
|
|
22
|
+
assert c.has_password is False
|
|
23
|
+
|
|
24
|
+
def test_has_password(self) -> None:
|
|
25
|
+
c = BaseCredential(username="admin", password="secret")
|
|
26
|
+
assert c.has_password is True
|
|
27
|
+
|
|
28
|
+
def test_empty_password(self) -> None:
|
|
29
|
+
c = BaseCredential(username="admin", password="")
|
|
30
|
+
assert c.has_password is False
|
|
31
|
+
|
|
32
|
+
def test_masked(self) -> None:
|
|
33
|
+
c = BaseCredential(username="admin", password="secret")
|
|
34
|
+
assert c.masked() == "admin:****"
|
|
35
|
+
|
|
36
|
+
def test_masked_no_user(self) -> None:
|
|
37
|
+
c = BaseCredential()
|
|
38
|
+
assert c.masked() == "(none):(empty)"
|
|
39
|
+
|
|
40
|
+
def test_frozen(self) -> None:
|
|
41
|
+
c = BaseCredential(username="admin")
|
|
42
|
+
with pytest.raises(AttributeError):
|
|
43
|
+
c.username = "other" # type: ignore[misc]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestADCredential:
|
|
47
|
+
def test_from_args(self) -> None:
|
|
48
|
+
args = types.SimpleNamespace(
|
|
49
|
+
username="admin", password="Pass123", domain="CORP.LOCAL",
|
|
50
|
+
lm_hash="", nt_hash="abc123", aes_key="", ticket="",
|
|
51
|
+
)
|
|
52
|
+
c = ADCredential.from_args(args)
|
|
53
|
+
assert c.username == "admin"
|
|
54
|
+
assert c.domain == "CORP.LOCAL"
|
|
55
|
+
assert c.nt_hash == "abc123"
|
|
56
|
+
|
|
57
|
+
def test_from_args_missing(self) -> None:
|
|
58
|
+
args = types.SimpleNamespace()
|
|
59
|
+
c = ADCredential.from_args(args)
|
|
60
|
+
assert c.username is None
|
|
61
|
+
assert c.domain == ""
|
|
62
|
+
|
|
63
|
+
def test_has_hash(self) -> None:
|
|
64
|
+
c = ADCredential(nt_hash="abc")
|
|
65
|
+
assert c.has_hash is True
|
|
66
|
+
|
|
67
|
+
def test_no_hash(self) -> None:
|
|
68
|
+
c = ADCredential()
|
|
69
|
+
assert c.has_hash is False
|
|
70
|
+
|
|
71
|
+
def test_impacket_hash_defaults(self) -> None:
|
|
72
|
+
c = ADCredential()
|
|
73
|
+
assert c.impacket_hash == "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0"
|
|
74
|
+
|
|
75
|
+
def test_impacket_hash_custom(self) -> None:
|
|
76
|
+
c = ADCredential(lm_hash="aaa", nt_hash="bbb")
|
|
77
|
+
assert c.impacket_hash == "aaa:bbb"
|
|
78
|
+
|
|
79
|
+
def test_inherits_base(self) -> None:
|
|
80
|
+
c = ADCredential(username="admin", password="pass")
|
|
81
|
+
assert c.has_password is True
|
|
82
|
+
assert c.masked() == "admin:****"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestCloudCredential:
|
|
86
|
+
def test_masked_secret_none(self) -> None:
|
|
87
|
+
c = CloudCredential(provider="aws")
|
|
88
|
+
assert c.masked_secret == "(none)"
|
|
89
|
+
|
|
90
|
+
def test_masked_secret_short(self) -> None:
|
|
91
|
+
c = CloudCredential(provider="aws", secret_key="abc")
|
|
92
|
+
assert c.masked_secret == "****"
|
|
93
|
+
|
|
94
|
+
def test_masked_secret_long(self) -> None:
|
|
95
|
+
c = CloudCredential(provider="aws", secret_key="AKIAIOSFODNN7EXAMPLE")
|
|
96
|
+
assert c.masked_secret.startswith("AKIA")
|
|
97
|
+
assert c.masked_secret.endswith("MPLE")
|
|
98
|
+
assert "****" in c.masked_secret
|
|
99
|
+
|
|
100
|
+
def test_to_dict(self) -> None:
|
|
101
|
+
c = CloudCredential(
|
|
102
|
+
provider="aws", identity="arn:aws:iam::123:user/admin",
|
|
103
|
+
secret_key="supersecret123456", token="tok",
|
|
104
|
+
)
|
|
105
|
+
d = c.to_dict()
|
|
106
|
+
assert d["provider"] == "aws"
|
|
107
|
+
assert "supersecret" not in d["secret_key"]
|
|
108
|
+
assert d["token"] == "****"
|
|
109
|
+
|
|
110
|
+
def test_to_dict_no_token(self) -> None:
|
|
111
|
+
c = CloudCredential(provider="gcp")
|
|
112
|
+
d = c.to_dict()
|
|
113
|
+
assert d["token"] is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestNetworkCredential:
|
|
117
|
+
def test_defaults(self) -> None:
|
|
118
|
+
c = NetworkCredential()
|
|
119
|
+
assert c.has_password is False
|
|
120
|
+
assert c.tags == []
|
|
121
|
+
|
|
122
|
+
def test_masked(self) -> None:
|
|
123
|
+
c = NetworkCredential(username="admin", password="pass", protocol="ssh", port=22)
|
|
124
|
+
assert c.masked() == "admin:****@ssh:22"
|
|
125
|
+
|
|
126
|
+
def test_mutable(self) -> None:
|
|
127
|
+
c = NetworkCredential(username="admin")
|
|
128
|
+
c.password = "new"
|
|
129
|
+
assert c.has_password is True
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Tests for xpl_core.dispatcher — Dispatcher and dispatch patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from xpl_core.dispatcher import Dispatcher, DispatchError
|
|
11
|
+
from xpl_core.module import BaseModule, ModuleManifest, ModuleResult, ModuleStatus, SafetyClass
|
|
12
|
+
from xpl_core.registry import PluginRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _TestModule(BaseModule):
|
|
16
|
+
@property
|
|
17
|
+
def manifest(self) -> ModuleManifest:
|
|
18
|
+
return ModuleManifest(
|
|
19
|
+
capability_id="test.dispatch.v1",
|
|
20
|
+
name="test-dispatch",
|
|
21
|
+
safety_class=SafetyClass.PASSIVE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def run(self) -> ModuleResult:
|
|
25
|
+
return ModuleResult(status=ModuleStatus.SUCCESS, output="dispatched")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestDispatcher:
|
|
29
|
+
def _make_dispatcher(self) -> Dispatcher:
|
|
30
|
+
reg = PluginRegistry()
|
|
31
|
+
reg.register("json", "stdlib", "json")
|
|
32
|
+
return Dispatcher(registry=reg)
|
|
33
|
+
|
|
34
|
+
def test_registry_property(self) -> None:
|
|
35
|
+
d = Dispatcher()
|
|
36
|
+
assert d.registry is not None
|
|
37
|
+
assert d.registry.count == 0
|
|
38
|
+
|
|
39
|
+
def test_dispatch_unknown_raises(self) -> None:
|
|
40
|
+
d = Dispatcher()
|
|
41
|
+
with pytest.raises(DispatchError, match="Unknown module"):
|
|
42
|
+
d.dispatch("fake", "nonexistent")
|
|
43
|
+
|
|
44
|
+
def test_dispatch_function_module(self) -> None:
|
|
45
|
+
fake_mod = types.ModuleType("fake_module")
|
|
46
|
+
fake_mod.MANIFEST = { # type: ignore[attr-defined]
|
|
47
|
+
"capability_id": "test.fake.v1",
|
|
48
|
+
"name": "fake",
|
|
49
|
+
"safety_class": "PASSIVE",
|
|
50
|
+
}
|
|
51
|
+
fake_mod.run = lambda args: 0 # type: ignore[attr-defined]
|
|
52
|
+
|
|
53
|
+
reg = PluginRegistry()
|
|
54
|
+
reg.register("fake", "test", "fake_module")
|
|
55
|
+
entry = reg.get("test", "fake")
|
|
56
|
+
assert entry is not None
|
|
57
|
+
entry._loaded = fake_mod
|
|
58
|
+
|
|
59
|
+
d = Dispatcher(registry=reg)
|
|
60
|
+
result = d.dispatch("test", "fake", args=None)
|
|
61
|
+
assert result.status == ModuleStatus.SUCCESS
|
|
62
|
+
|
|
63
|
+
def test_dispatch_class_module(self) -> None:
|
|
64
|
+
class_mod = types.ModuleType("class_module")
|
|
65
|
+
class_mod.Module = _TestModule # type: ignore[attr-defined]
|
|
66
|
+
|
|
67
|
+
reg = PluginRegistry()
|
|
68
|
+
reg.register("test-cls", "test", "class_module")
|
|
69
|
+
entry = reg.get("test", "test-cls")
|
|
70
|
+
assert entry is not None
|
|
71
|
+
entry._loaded = class_mod
|
|
72
|
+
|
|
73
|
+
d = Dispatcher(registry=reg)
|
|
74
|
+
result = d.dispatch("test", "test-cls")
|
|
75
|
+
assert result.status == ModuleStatus.SUCCESS
|
|
76
|
+
assert result.output == "dispatched"
|
|
77
|
+
|
|
78
|
+
def test_dispatch_no_run_raises(self) -> None:
|
|
79
|
+
empty_mod = types.ModuleType("empty_module")
|
|
80
|
+
|
|
81
|
+
reg = PluginRegistry()
|
|
82
|
+
reg.register("empty", "test", "empty_module")
|
|
83
|
+
entry = reg.get("test", "empty")
|
|
84
|
+
assert entry is not None
|
|
85
|
+
entry._loaded = empty_mod
|
|
86
|
+
|
|
87
|
+
d = Dispatcher(registry=reg)
|
|
88
|
+
with pytest.raises(DispatchError, match="has no BaseModule"):
|
|
89
|
+
d.dispatch("test", "empty")
|
|
90
|
+
|
|
91
|
+
def test_dispatch_args_compat(self) -> None:
|
|
92
|
+
fake_mod = types.ModuleType("compat_module")
|
|
93
|
+
fake_mod.run = lambda args: 0 # type: ignore[attr-defined]
|
|
94
|
+
|
|
95
|
+
reg = PluginRegistry()
|
|
96
|
+
reg.register("psexec", "lateral", "compat_module")
|
|
97
|
+
entry = reg.get("lateral", "psexec")
|
|
98
|
+
assert entry is not None
|
|
99
|
+
entry._loaded = fake_mod
|
|
100
|
+
|
|
101
|
+
d = Dispatcher(registry=reg)
|
|
102
|
+
args = MagicMock()
|
|
103
|
+
args.command = "lateral"
|
|
104
|
+
args.module = "psexec"
|
|
105
|
+
exit_code = d.dispatch_args(args)
|
|
106
|
+
assert exit_code == 0
|
|
107
|
+
|
|
108
|
+
def test_dispatch_args_unknown(self) -> None:
|
|
109
|
+
d = Dispatcher()
|
|
110
|
+
args = MagicMock()
|
|
111
|
+
args.command = "fake"
|
|
112
|
+
args.module = "nonexistent"
|
|
113
|
+
assert d.dispatch_args(args) == 1
|
|
114
|
+
|
|
115
|
+
def test_dispatch_args_import_error(self) -> None:
|
|
116
|
+
reg = PluginRegistry()
|
|
117
|
+
reg.register("broken", "test", "nonexistent.module.path.xyz")
|
|
118
|
+
|
|
119
|
+
d = Dispatcher(registry=reg)
|
|
120
|
+
args = MagicMock()
|
|
121
|
+
args.command = "test"
|
|
122
|
+
args.module = "broken"
|
|
123
|
+
assert d.dispatch_args(args) == 1
|
|
124
|
+
|
|
125
|
+
def test_list_commands(self) -> None:
|
|
126
|
+
reg = PluginRegistry()
|
|
127
|
+
reg.register("ldap-enum", "enum", "mod.ldap")
|
|
128
|
+
reg.register("spn-scan", "enum", "mod.spn")
|
|
129
|
+
reg.register("kerberoast", "attack", "mod.kerb")
|
|
130
|
+
|
|
131
|
+
d = Dispatcher(registry=reg)
|
|
132
|
+
cmds = d.list_commands()
|
|
133
|
+
assert "enum" in cmds
|
|
134
|
+
assert "attack" in cmds
|
|
135
|
+
assert set(cmds["enum"]) == {"ldap-enum", "spn-scan"}
|
|
136
|
+
assert cmds["attack"] == ["kerberoast"]
|
|
137
|
+
|
|
138
|
+
def test_dispatch_function_exception(self) -> None:
|
|
139
|
+
crash_mod = types.ModuleType("crash_module")
|
|
140
|
+
crash_mod.MANIFEST = {"capability_id": "crash", "name": "crash"} # type: ignore[attr-defined]
|
|
141
|
+
crash_mod.run = lambda args: (_ for _ in ()).throw(ValueError("boom")) # type: ignore[attr-defined]
|
|
142
|
+
|
|
143
|
+
reg = PluginRegistry()
|
|
144
|
+
reg.register("crash", "test", "crash_module")
|
|
145
|
+
entry = reg.get("test", "crash")
|
|
146
|
+
assert entry is not None
|
|
147
|
+
entry._loaded = crash_mod
|
|
148
|
+
|
|
149
|
+
d = Dispatcher(registry=reg)
|
|
150
|
+
result = d.dispatch("test", "crash", args=None)
|
|
151
|
+
assert result.status == ModuleStatus.FAILURE
|
|
152
|
+
assert "boom" in result.error
|