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.
Files changed (49) hide show
  1. xpl_core-0.3.0/LICENSE +123 -0
  2. xpl_core-0.3.0/PKG-INFO +24 -0
  3. xpl_core-0.3.0/README.md +68 -0
  4. xpl_core-0.3.0/pyproject.toml +70 -0
  5. xpl_core-0.3.0/setup.cfg +4 -0
  6. xpl_core-0.3.0/tests/test_config.py +76 -0
  7. xpl_core-0.3.0/tests/test_credential.py +129 -0
  8. xpl_core-0.3.0/tests/test_dispatcher.py +152 -0
  9. xpl_core-0.3.0/tests/test_finding.py +64 -0
  10. xpl_core-0.3.0/tests/test_handler.py +132 -0
  11. xpl_core-0.3.0/tests/test_listener.py +34 -0
  12. xpl_core-0.3.0/tests/test_module.py +182 -0
  13. xpl_core-0.3.0/tests/test_pool.py +74 -0
  14. xpl_core-0.3.0/tests/test_registry.py +165 -0
  15. xpl_core-0.3.0/tests/test_session.py +158 -0
  16. xpl_core-0.3.0/tests/test_session_manager.py +182 -0
  17. xpl_core-0.3.0/tests/test_shell_engine.py +162 -0
  18. xpl_core-0.3.0/tests/test_shells.py +136 -0
  19. xpl_core-0.3.0/tests/test_stager.py +131 -0
  20. xpl_core-0.3.0/xpl_core/__init__.py +3 -0
  21. xpl_core-0.3.0/xpl_core/config.py +74 -0
  22. xpl_core-0.3.0/xpl_core/credential.py +112 -0
  23. xpl_core-0.3.0/xpl_core/dispatcher.py +98 -0
  24. xpl_core-0.3.0/xpl_core/module.py +168 -0
  25. xpl_core-0.3.0/xpl_core/pool.py +87 -0
  26. xpl_core-0.3.0/xpl_core/py.typed +0 -0
  27. xpl_core-0.3.0/xpl_core/registry.py +127 -0
  28. xpl_core-0.3.0/xpl_core/schemas/__init__.py +81 -0
  29. xpl_core-0.3.0/xpl_core/schemas/xpl_finding_v1.json +105 -0
  30. xpl_core-0.3.0/xpl_core/session.py +173 -0
  31. xpl_core-0.3.0/xpl_core/shells/__init__.py +85 -0
  32. xpl_core-0.3.0/xpl_core/shells/dns_tunnel_shell.py +275 -0
  33. xpl_core-0.3.0/xpl_core/shells/handler.py +214 -0
  34. xpl_core-0.3.0/xpl_core/shells/http_poll_shell.py +252 -0
  35. xpl_core-0.3.0/xpl_core/shells/icmp_covert_shell.py +226 -0
  36. xpl_core-0.3.0/xpl_core/shells/internal_shell.py +296 -0
  37. xpl_core-0.3.0/xpl_core/shells/listener.py +204 -0
  38. xpl_core-0.3.0/xpl_core/shells/meterpreter_bridge.py +245 -0
  39. xpl_core-0.3.0/xpl_core/shells/mqtt_shell.py +219 -0
  40. xpl_core-0.3.0/xpl_core/shells/raw_tcp_shell.py +180 -0
  41. xpl_core-0.3.0/xpl_core/shells/raw_udp_shell.py +252 -0
  42. xpl_core-0.3.0/xpl_core/shells/session_manager.py +299 -0
  43. xpl_core-0.3.0/xpl_core/shells/shell_engine.py +230 -0
  44. xpl_core-0.3.0/xpl_core/shells/stager.py +357 -0
  45. xpl_core-0.3.0/xpl_core.egg-info/PKG-INFO +24 -0
  46. xpl_core-0.3.0/xpl_core.egg-info/SOURCES.txt +47 -0
  47. xpl_core-0.3.0/xpl_core.egg-info/dependency_links.txt +1 -0
  48. xpl_core-0.3.0/xpl_core.egg-info/requires.txt +14 -0
  49. 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
@@ -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
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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