iotsploit-cli 0.0.6__tar.gz → 0.0.8__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.
- iotsploit_cli-0.0.8/PKG-INFO +118 -0
- iotsploit_cli-0.0.8/README.md +83 -0
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/pyproject.toml +7 -7
- iotsploit_cli-0.0.8/src/iotsploit_cli/command_palette.py +349 -0
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/device_commands.py +2 -2
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/django_commands.py +66 -22
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/firmware_commands.py +0 -1
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/linux_commands.py +0 -1
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/network_commands.py +9 -5
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/plugin_commands.py +69 -49
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/system_commands.py +45 -1
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/target_commands.py +0 -1
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/console.py +132 -5
- iotsploit_cli-0.0.6/PKG-INFO +0 -67
- iotsploit_cli-0.0.6/README.md +0 -33
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/__init__.py +0 -0
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/__init__.py +0 -0
- {iotsploit_cli-0.0.6 → iotsploit_cli-0.0.8}/src/iotsploit_cli/commands/base_commands.py +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iotsploit-cli
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: IoTSploit CLI shell (console + command modules) - IoT security testing interactive interface
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Keywords: iot,security,testing,pentest,cli,shell
|
|
7
|
+
Author: IoTSploit Team
|
|
8
|
+
Author-email: support@iotsploit.org
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Hardware
|
|
23
|
+
Requires-Dist: cmd2 (>=2.4,<3.0)
|
|
24
|
+
Requires-Dist: iotsploit-core
|
|
25
|
+
Requires-Dist: iotsploit-django
|
|
26
|
+
Requires-Dist: iotsploit-drivers
|
|
27
|
+
Requires-Dist: iotsploit-exploits
|
|
28
|
+
Requires-Dist: iotsploit-mcp
|
|
29
|
+
Requires-Dist: prompt-toolkit (>=3.0.48,<4.0.0)
|
|
30
|
+
Project-URL: Documentation, https://www.iotsploit.org/
|
|
31
|
+
Project-URL: Homepage, https://www.iotsploit.org/
|
|
32
|
+
Project-URL: Repository, https://github.com/TKXB/iotsploit
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# iotsploit-cli
|
|
36
|
+
|
|
37
|
+
IoTSploit interactive CLI shell for IoT security testing.
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
This package provides the `iotsploit` command-line shell built on top of `cmd2`.
|
|
42
|
+
It bundles the core console loop (`console.py`) and all command modules
|
|
43
|
+
(`commands/`) that implement device management, plugin execution, target
|
|
44
|
+
management, network operations, and more.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install iotsploit-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
iotsploit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or with the Django server started immediately:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
iotsploit --runserver
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Custom plugins
|
|
65
|
+
|
|
66
|
+
`IOTSPLOIT_EXPLOIT_PLUGINS_DIR` can be used for user custom exploit plugins.
|
|
67
|
+
`IOTSPLOIT_DEVICE_PLUGINS_DIR` can be used for user custom device plugins.
|
|
68
|
+
|
|
69
|
+
## Command Palette
|
|
70
|
+
|
|
71
|
+
The IoTSploit shell includes a live command palette for all commands. When
|
|
72
|
+
you type any character at the top-level interactive prompt, a menu appears
|
|
73
|
+
immediately showing every eligible command matching that prefix with a short
|
|
74
|
+
description beside each entry.
|
|
75
|
+
|
|
76
|
+
### How it works
|
|
77
|
+
|
|
78
|
+
1. Start typing any character at the empty prompt.
|
|
79
|
+
2. The menu lists all visible commands matching the typed prefix (e.g. `e`
|
|
80
|
+
shows `edit`, `exploit`, `exit`, `execute_plugin`; `h` shows `help`,
|
|
81
|
+
`history`).
|
|
82
|
+
3. Additional characters filter the list in real time (e.g. `ex` narrows to
|
|
83
|
+
`exit`, `execute_plugin`, `exploit`).
|
|
84
|
+
4. Navigate the list, insert a selection, or dismiss the menu.
|
|
85
|
+
|
|
86
|
+
### Keyboard controls
|
|
87
|
+
|
|
88
|
+
| Key | Behavior |
|
|
89
|
+
|-----|----------|
|
|
90
|
+
| Any first-token character | Open the palette menu |
|
|
91
|
+
| Additional characters | Filter the list case-insensitively |
|
|
92
|
+
| Up / Down | Move selection without changing the buffer |
|
|
93
|
+
| Tab | Insert the selected command name (does not submit) |
|
|
94
|
+
| Enter | Accept the selected command and submit through cmd2 dispatch |
|
|
95
|
+
| Escape | Close the menu and retain the current input text |
|
|
96
|
+
| Backspace to empty | Close the menu |
|
|
97
|
+
| Space after a command | Close the menu and allow argument entry |
|
|
98
|
+
| Ctrl+C | Cancel the current input (normal shell behavior) |
|
|
99
|
+
| Ctrl+D on empty line | Exit the shell (normal EOF behavior) |
|
|
100
|
+
|
|
101
|
+
### Behaviour notes
|
|
102
|
+
|
|
103
|
+
- The palette is **TTY-only**. Non-interactive use (piped input, startup
|
|
104
|
+
scripts, non-TTY stdin) bypasses the palette entirely and uses the normal
|
|
105
|
+
cmd2 input path.
|
|
106
|
+
- The command list is derived dynamically from the runtime command registry,
|
|
107
|
+
so newly loaded command modules appear without editing the palette.
|
|
108
|
+
- Tab completion for arguments (after a space) still uses cmd2's existing
|
|
109
|
+
completion engine, including argument-specific completers and argparse
|
|
110
|
+
completers.
|
|
111
|
+
- Selecting a command from the palette does **not** execute it; it inserts the
|
|
112
|
+
command name so you can type arguments before pressing Enter.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
GPL-3.0-or-later. See [LICENSE](../LICENSE) for details.
|
|
117
|
+
For commercial use, contact wang3919379@gmail.com.
|
|
118
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# iotsploit-cli
|
|
2
|
+
|
|
3
|
+
IoTSploit interactive CLI shell for IoT security testing.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides the `iotsploit` command-line shell built on top of `cmd2`.
|
|
8
|
+
It bundles the core console loop (`console.py`) and all command modules
|
|
9
|
+
(`commands/`) that implement device management, plugin execution, target
|
|
10
|
+
management, network operations, and more.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install iotsploit-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
iotsploit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with the Django server started immediately:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
iotsploit --runserver
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Custom plugins
|
|
31
|
+
|
|
32
|
+
`IOTSPLOIT_EXPLOIT_PLUGINS_DIR` can be used for user custom exploit plugins.
|
|
33
|
+
`IOTSPLOIT_DEVICE_PLUGINS_DIR` can be used for user custom device plugins.
|
|
34
|
+
|
|
35
|
+
## Command Palette
|
|
36
|
+
|
|
37
|
+
The IoTSploit shell includes a live command palette for all commands. When
|
|
38
|
+
you type any character at the top-level interactive prompt, a menu appears
|
|
39
|
+
immediately showing every eligible command matching that prefix with a short
|
|
40
|
+
description beside each entry.
|
|
41
|
+
|
|
42
|
+
### How it works
|
|
43
|
+
|
|
44
|
+
1. Start typing any character at the empty prompt.
|
|
45
|
+
2. The menu lists all visible commands matching the typed prefix (e.g. `e`
|
|
46
|
+
shows `edit`, `exploit`, `exit`, `execute_plugin`; `h` shows `help`,
|
|
47
|
+
`history`).
|
|
48
|
+
3. Additional characters filter the list in real time (e.g. `ex` narrows to
|
|
49
|
+
`exit`, `execute_plugin`, `exploit`).
|
|
50
|
+
4. Navigate the list, insert a selection, or dismiss the menu.
|
|
51
|
+
|
|
52
|
+
### Keyboard controls
|
|
53
|
+
|
|
54
|
+
| Key | Behavior |
|
|
55
|
+
|-----|----------|
|
|
56
|
+
| Any first-token character | Open the palette menu |
|
|
57
|
+
| Additional characters | Filter the list case-insensitively |
|
|
58
|
+
| Up / Down | Move selection without changing the buffer |
|
|
59
|
+
| Tab | Insert the selected command name (does not submit) |
|
|
60
|
+
| Enter | Accept the selected command and submit through cmd2 dispatch |
|
|
61
|
+
| Escape | Close the menu and retain the current input text |
|
|
62
|
+
| Backspace to empty | Close the menu |
|
|
63
|
+
| Space after a command | Close the menu and allow argument entry |
|
|
64
|
+
| Ctrl+C | Cancel the current input (normal shell behavior) |
|
|
65
|
+
| Ctrl+D on empty line | Exit the shell (normal EOF behavior) |
|
|
66
|
+
|
|
67
|
+
### Behaviour notes
|
|
68
|
+
|
|
69
|
+
- The palette is **TTY-only**. Non-interactive use (piped input, startup
|
|
70
|
+
scripts, non-TTY stdin) bypasses the palette entirely and uses the normal
|
|
71
|
+
cmd2 input path.
|
|
72
|
+
- The command list is derived dynamically from the runtime command registry,
|
|
73
|
+
so newly loaded command modules appear without editing the palette.
|
|
74
|
+
- Tab completion for arguments (after a space) still uses cmd2's existing
|
|
75
|
+
completion engine, including argument-specific completers and argparse
|
|
76
|
+
completers.
|
|
77
|
+
- Selecting a command from the palette does **not** execute it; it inserts the
|
|
78
|
+
command name so you can type arguments before pressing Enter.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
GPL-3.0-or-later. See [LICENSE](../LICENSE) for details.
|
|
83
|
+
For commercial use, contact wang3919379@gmail.com.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "iotsploit-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.8"
|
|
4
4
|
description = "IoTSploit CLI shell (console + command modules) - IoT security testing interactive interface"
|
|
5
5
|
authors = ["IoTSploit Team <support@iotsploit.org>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -29,13 +29,13 @@ iotsploit = "iotsploit_cli.console:main"
|
|
|
29
29
|
|
|
30
30
|
[tool.poetry.dependencies]
|
|
31
31
|
python = ">=3.10,<4.0"
|
|
32
|
-
iotsploit-core = "
|
|
33
|
-
iotsploit-django = "
|
|
34
|
-
iotsploit-drivers = "
|
|
35
|
-
iotsploit-exploits = "
|
|
36
|
-
iotsploit-mcp = "
|
|
32
|
+
iotsploit-core = "*"
|
|
33
|
+
iotsploit-django = "*"
|
|
34
|
+
iotsploit-drivers = "*"
|
|
35
|
+
iotsploit-exploits = "*"
|
|
36
|
+
iotsploit-mcp = "*"
|
|
37
37
|
cmd2 = "^2.4"
|
|
38
|
-
|
|
38
|
+
prompt-toolkit = "^3.0.48"
|
|
39
39
|
|
|
40
40
|
[tool.poetry.group.dev.dependencies]
|
|
41
41
|
pytest = "^7.4.0"
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Command palette for IoTSploit CLI -- live command discovery.
|
|
2
|
+
|
|
3
|
+
This module implements the live prompt-toolkit command palette that shows
|
|
4
|
+
eligible commands as the user types at the top-level interactive prompt.
|
|
5
|
+
Any typed prefix triggers the palette; additional characters filter the list
|
|
6
|
+
in real time. It preserves all existing cmd2 behaviour for non-TTY input,
|
|
7
|
+
nested prompts, scripts, and argument completion.
|
|
8
|
+
|
|
9
|
+
The public classes are:
|
|
10
|
+
|
|
11
|
+
* :class:`CommandPaletteEntry` -- immutable metadata value
|
|
12
|
+
* :class:`CommandCatalog` -- UI-independent metadata provider
|
|
13
|
+
* :class:`Cmd2CompletionAdapter` -- bridges cmd2 readline Tab completion
|
|
14
|
+
* :class:`CommandPaletteCompleter` -- prompt-toolkit ``Completer``
|
|
15
|
+
* :class:`PaletteInputSession` -- prompt-toolkit ``PromptSession`` wrapper
|
|
16
|
+
|
|
17
|
+
This module must **not** import anything from ``iotsploit_cli.console`` or
|
|
18
|
+
Django so it can be unit-tested in isolation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Iterable, List, Optional
|
|
24
|
+
|
|
25
|
+
from prompt_toolkit import PromptSession
|
|
26
|
+
from prompt_toolkit.completion import (
|
|
27
|
+
Completion,
|
|
28
|
+
CompleteEvent,
|
|
29
|
+
Completer,
|
|
30
|
+
)
|
|
31
|
+
from prompt_toolkit.shortcuts import CompleteStyle
|
|
32
|
+
from prompt_toolkit.document import Document
|
|
33
|
+
from prompt_toolkit.formatted_text import ANSI
|
|
34
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------- #
|
|
37
|
+
# Constants
|
|
38
|
+
# --------------------------------------------------------------------------- #
|
|
39
|
+
|
|
40
|
+
FALLBACK_DESCRIPTION = "No description available"
|
|
41
|
+
EOF_SENTINEL = "eof"
|
|
42
|
+
|
|
43
|
+
# Matches ANSI/CSI escape sequences so descriptions are control-char free.
|
|
44
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# --------------------------------------------------------------------------- #
|
|
48
|
+
# Helpers
|
|
49
|
+
# --------------------------------------------------------------------------- #
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _sanitize_description(raw: Optional[str]) -> str:
|
|
53
|
+
"""Strip ANSI control characters and normalise whitespace."""
|
|
54
|
+
if not raw:
|
|
55
|
+
return FALLBACK_DESCRIPTION
|
|
56
|
+
cleaned = _ANSI_ESCAPE_RE.sub("", raw)
|
|
57
|
+
cleaned = " ".join(cleaned.split())
|
|
58
|
+
if not cleaned:
|
|
59
|
+
return FALLBACK_DESCRIPTION
|
|
60
|
+
return cleaned
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --------------------------------------------------------------------------- #
|
|
64
|
+
# CommandPaletteEntry
|
|
65
|
+
# --------------------------------------------------------------------------- #
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class CommandPaletteEntry:
|
|
70
|
+
"""Immutable value describing a single palette entry."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
description: str
|
|
74
|
+
category: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------------------------------- #
|
|
78
|
+
# CommandCatalog (UI-independent metadata provider)
|
|
79
|
+
# --------------------------------------------------------------------------- #
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CommandCatalog:
|
|
83
|
+
"""UI-independent command metadata provider for the command palette.
|
|
84
|
+
|
|
85
|
+
The catalog reads metadata **only** -- it never calls a ``do_*`` handler.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, shell: Any) -> None:
|
|
89
|
+
"""Store a reference to the shell; do not invoke handlers."""
|
|
90
|
+
self._shell = shell
|
|
91
|
+
|
|
92
|
+
# -- public API -------------------------------------------------------- #
|
|
93
|
+
|
|
94
|
+
def get_eligible_entries(self, prefix: str) -> List[CommandPaletteEntry]:
|
|
95
|
+
"""Return eligible palette entries whose name starts with *prefix*.
|
|
96
|
+
|
|
97
|
+
Matching is case-insensitive. Results are sorted with exact matches
|
|
98
|
+
first, then alphabetical.
|
|
99
|
+
"""
|
|
100
|
+
if not prefix:
|
|
101
|
+
return []
|
|
102
|
+
prefix_lower = prefix.lower()
|
|
103
|
+
entries: List[CommandPaletteEntry] = []
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
visible = self._shell.get_visible_commands()
|
|
107
|
+
except Exception:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
# Build exclusion sets for aliases, macros, and the eof sentinel.
|
|
111
|
+
aliases: set = set()
|
|
112
|
+
macros: set = set()
|
|
113
|
+
if hasattr(self._shell, "aliases") and self._shell.aliases:
|
|
114
|
+
aliases = set(self._shell.aliases.keys())
|
|
115
|
+
if hasattr(self._shell, "macros") and self._shell.macros:
|
|
116
|
+
macros = set(self._shell.macros.keys())
|
|
117
|
+
|
|
118
|
+
for cmd_name in visible:
|
|
119
|
+
# Prefix filter (case-insensitive)
|
|
120
|
+
if not cmd_name.lower().startswith(prefix_lower):
|
|
121
|
+
continue
|
|
122
|
+
# Exclude eof sentinel
|
|
123
|
+
if cmd_name == EOF_SENTINEL:
|
|
124
|
+
continue
|
|
125
|
+
# Exclude aliases and macros (MVP: no stable description metadata)
|
|
126
|
+
if cmd_name in aliases or cmd_name in macros:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# Description via the shell's existing doc extractor
|
|
130
|
+
try:
|
|
131
|
+
raw_doc = self._shell.get_command_doc(cmd_name)
|
|
132
|
+
except Exception:
|
|
133
|
+
raw_doc = None
|
|
134
|
+
description = _sanitize_description(raw_doc)
|
|
135
|
+
|
|
136
|
+
# Optional category from cmd2's @with_category decorator
|
|
137
|
+
category: Optional[str] = None
|
|
138
|
+
cmd_func = getattr(self._shell, "do_" + cmd_name, None)
|
|
139
|
+
if cmd_func is not None and hasattr(cmd_func, "category"):
|
|
140
|
+
category = cmd_func.category
|
|
141
|
+
|
|
142
|
+
entries.append(
|
|
143
|
+
CommandPaletteEntry(
|
|
144
|
+
name=cmd_name,
|
|
145
|
+
description=description,
|
|
146
|
+
category=category,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Sort: exact match first, then alphabetical by name
|
|
151
|
+
entries.sort(
|
|
152
|
+
key=lambda e: (e.name.lower() != prefix_lower, e.name.lower())
|
|
153
|
+
)
|
|
154
|
+
return entries
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def is_first_token_context(text: str, cursor_pos: int) -> bool:
|
|
158
|
+
"""Return ``True`` if the cursor is in the first token and it is non-empty.
|
|
159
|
+
|
|
160
|
+
Leading whitespace is skipped so that `` he`` is recognised the same
|
|
161
|
+
as ``he``. This activates the palette for any command prefix, not just
|
|
162
|
+
a specific letter.
|
|
163
|
+
"""
|
|
164
|
+
stripped = text.lstrip()
|
|
165
|
+
if not stripped:
|
|
166
|
+
return False
|
|
167
|
+
offset = len(text) - len(stripped)
|
|
168
|
+
first_space = stripped.find(" ")
|
|
169
|
+
if first_space == -1:
|
|
170
|
+
in_first_token = True
|
|
171
|
+
first_word = stripped
|
|
172
|
+
else:
|
|
173
|
+
rel_cursor = cursor_pos - offset
|
|
174
|
+
in_first_token = rel_cursor <= first_space
|
|
175
|
+
first_word = stripped[:first_space]
|
|
176
|
+
|
|
177
|
+
if not in_first_token:
|
|
178
|
+
return False
|
|
179
|
+
if not first_word:
|
|
180
|
+
return False
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --------------------------------------------------------------------------- #
|
|
185
|
+
# Cmd2CompletionAdapter (bridges cmd2 readline Tab completion)
|
|
186
|
+
# --------------------------------------------------------------------------- #
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Cmd2CompletionAdapter:
|
|
190
|
+
"""Bridge cmd2's readline-based ``complete()`` to prompt-toolkit.
|
|
191
|
+
|
|
192
|
+
When the user presses **Tab** outside the first-token context (i.e.
|
|
193
|
+
for argument completion), this adapter temporarily mocks the ``readline``
|
|
194
|
+
module functions that cmd2 reads and calls ``shell.complete(text, 0)`` to
|
|
195
|
+
populate ``completion_matches`` / ``display_matches``.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, shell: Any) -> None:
|
|
199
|
+
self._shell = shell
|
|
200
|
+
|
|
201
|
+
def get_completions(self, document: Document) -> Iterable[Completion]:
|
|
202
|
+
"""Delegate to cmd2's ``complete()`` and yield prompt-toolkit Completions."""
|
|
203
|
+
try:
|
|
204
|
+
from cmd2 import rl_utils
|
|
205
|
+
|
|
206
|
+
readline_mod = rl_utils.readline
|
|
207
|
+
except (ImportError, AttributeError):
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
line = document.text
|
|
211
|
+
cursor = document.cursor_position
|
|
212
|
+
|
|
213
|
+
# Find the current word being completed (scan backwards for whitespace)
|
|
214
|
+
word_start = cursor
|
|
215
|
+
while word_start > 0 and not line[word_start - 1].isspace():
|
|
216
|
+
word_start -= 1
|
|
217
|
+
text = line[word_start:cursor]
|
|
218
|
+
|
|
219
|
+
if not text:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Save originals
|
|
223
|
+
orig_get_line_buffer = readline_mod.get_line_buffer
|
|
224
|
+
orig_get_begidx = readline_mod.get_begidx
|
|
225
|
+
orig_get_endidx = readline_mod.get_endidx
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Mock readline functions so cmd2's complete() sees the PT Document
|
|
229
|
+
readline_mod.get_line_buffer = lambda: line
|
|
230
|
+
readline_mod.get_begidx = lambda: word_start
|
|
231
|
+
readline_mod.get_endidx = lambda: cursor
|
|
232
|
+
|
|
233
|
+
# state=0 triggers _reset_completion_defaults + _perform_completion
|
|
234
|
+
self._shell.complete(text, 0)
|
|
235
|
+
|
|
236
|
+
matches = getattr(self._shell, "completion_matches", [])
|
|
237
|
+
displays = getattr(self._shell, "display_matches", matches)
|
|
238
|
+
|
|
239
|
+
for i, match in enumerate(matches):
|
|
240
|
+
display = displays[i] if i < len(displays) else match
|
|
241
|
+
yield Completion(
|
|
242
|
+
match,
|
|
243
|
+
start_position=-len(text),
|
|
244
|
+
display=display,
|
|
245
|
+
)
|
|
246
|
+
except Exception:
|
|
247
|
+
return
|
|
248
|
+
finally:
|
|
249
|
+
# Always restore originals
|
|
250
|
+
readline_mod.get_line_buffer = orig_get_line_buffer
|
|
251
|
+
readline_mod.get_begidx = orig_get_begidx
|
|
252
|
+
readline_mod.get_endidx = orig_get_endidx
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# --------------------------------------------------------------------------- #
|
|
256
|
+
# CommandPaletteCompleter (prompt-toolkit Completer)
|
|
257
|
+
# --------------------------------------------------------------------------- #
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CommandPaletteCompleter(Completer):
|
|
261
|
+
"""prompt-toolkit ``Completer`` for live command palette and cmd2 Tab delegation.
|
|
262
|
+
|
|
263
|
+
* While the cursor is in the first token and it is non-empty, yield live
|
|
264
|
+
palette entries for any command prefix (works with
|
|
265
|
+
``complete_while_typing=True``).
|
|
266
|
+
* When **Tab** is explicitly requested outside the first token (i.e.
|
|
267
|
+
for argument completion), delegate to :class:`Cmd2CompletionAdapter`
|
|
268
|
+
so existing cmd2 argument completion still works.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
shell: Any,
|
|
274
|
+
catalog: CommandCatalog,
|
|
275
|
+
cmd2_adapter: Cmd2CompletionAdapter,
|
|
276
|
+
) -> None:
|
|
277
|
+
self._shell = shell
|
|
278
|
+
self._catalog = catalog
|
|
279
|
+
self._adapter = cmd2_adapter
|
|
280
|
+
|
|
281
|
+
def get_completions(
|
|
282
|
+
self, document: Document, complete_event: CompleteEvent
|
|
283
|
+
) -> Iterable[Completion]:
|
|
284
|
+
text = document.text_before_cursor
|
|
285
|
+
cursor = document.cursor_position
|
|
286
|
+
|
|
287
|
+
# Strip leading whitespace for token analysis
|
|
288
|
+
stripped = text.lstrip()
|
|
289
|
+
offset = len(text) - len(stripped)
|
|
290
|
+
first_space = stripped.find(" ")
|
|
291
|
+
if first_space == -1:
|
|
292
|
+
in_first_token = True
|
|
293
|
+
first_word = stripped
|
|
294
|
+
else:
|
|
295
|
+
rel_cursor = cursor - offset
|
|
296
|
+
in_first_token = rel_cursor <= first_space
|
|
297
|
+
first_word = stripped[:first_space]
|
|
298
|
+
|
|
299
|
+
# -- live palette for any first-token prefix ---------------------- #
|
|
300
|
+
if in_first_token and first_word:
|
|
301
|
+
for entry in self._catalog.get_eligible_entries(first_word):
|
|
302
|
+
yield Completion(
|
|
303
|
+
entry.name,
|
|
304
|
+
start_position=-len(first_word),
|
|
305
|
+
display=entry.name,
|
|
306
|
+
display_meta=entry.description,
|
|
307
|
+
)
|
|
308
|
+
# -- explicit Tab delegation (argument completion) ---------------- #
|
|
309
|
+
elif complete_event.completion_requested:
|
|
310
|
+
yield from self._adapter.get_completions(document)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------- #
|
|
314
|
+
# PaletteInputSession (prompt-toolkit PromptSession wrapper)
|
|
315
|
+
# --------------------------------------------------------------------------- #
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class PaletteInputSession:
|
|
319
|
+
"""Wrap a prompt-toolkit ``PromptSession`` for the command palette.
|
|
320
|
+
|
|
321
|
+
Most key behaviour (Up/Down navigation, Tab apply, Enter accept, Escape
|
|
322
|
+
dismiss) is provided by prompt-toolkit's defaults when
|
|
323
|
+
``complete_while_typing=True`` with ``CompleteStyle.COLUMN``.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self, shell: Any, completer: CommandPaletteCompleter) -> None:
|
|
327
|
+
self._shell = shell
|
|
328
|
+
self._session: PromptSession = PromptSession(
|
|
329
|
+
completer=completer,
|
|
330
|
+
complete_while_typing=True,
|
|
331
|
+
complete_style=CompleteStyle.COLUMN,
|
|
332
|
+
history=InMemoryHistory(),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def prompt(self, prompt_text: str) -> str:
|
|
336
|
+
"""Display the prompt and return the accepted line.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
KeyboardInterrupt: when the user presses Ctrl+C.
|
|
340
|
+
Returns ``'eof'`` on EOF (Ctrl+D on empty line), matching cmd2.
|
|
341
|
+
"""
|
|
342
|
+
message = ANSI(prompt_text) if prompt_text else ""
|
|
343
|
+
try:
|
|
344
|
+
line = self._session.prompt(message=message)
|
|
345
|
+
return line
|
|
346
|
+
except KeyboardInterrupt:
|
|
347
|
+
raise
|
|
348
|
+
except EOFError:
|
|
349
|
+
return EOF_SENTINEL
|
|
@@ -233,7 +233,7 @@ class DeviceCommands(BaseCommands):
|
|
|
233
233
|
|
|
234
234
|
# 执行设备扫描
|
|
235
235
|
logger.info(ansi.style("Scanning for devices...", fg=ansi.Fg.CYAN))
|
|
236
|
-
|
|
236
|
+
device_registry.scan_devices()
|
|
237
237
|
|
|
238
238
|
# 获取所有设备(包括已存储的和新发现的)
|
|
239
239
|
all_devices = device_registry.device_store.devices
|
|
@@ -533,4 +533,4 @@ class DeviceCommands(BaseCommands):
|
|
|
533
533
|
logger.debug("Detailed error:", exc_info=True)
|
|
534
534
|
|
|
535
535
|
# Add alias for device_import
|
|
536
|
-
do_dimport = do_device_import
|
|
536
|
+
do_dimport = do_device_import
|
|
@@ -24,6 +24,38 @@ logger = iots_logger.get_logger(__name__)
|
|
|
24
24
|
class DjangoCommands(BaseCommands):
|
|
25
25
|
"""Django-related commands for the SAT Shell"""
|
|
26
26
|
|
|
27
|
+
def _services_log_to_console(self) -> bool:
|
|
28
|
+
override = os.getenv("IOTSPLOIT_SERVICE_LOG_TO_CONSOLE", "").strip().lower()
|
|
29
|
+
if override:
|
|
30
|
+
return override in ("1", "true", "yes", "y", "on")
|
|
31
|
+
return os.getenv("IOTSPLOIT_LOG_FORMAT", "standard").strip().lower() == "standard"
|
|
32
|
+
|
|
33
|
+
def _open_service_log(self, service_name: str):
|
|
34
|
+
if not hasattr(self, "_service_log_files"):
|
|
35
|
+
self._service_log_files = []
|
|
36
|
+
|
|
37
|
+
log_dir = os.getenv("IOTSPLOIT_SERVICE_LOG_DIR", "/tmp/sat_logs")
|
|
38
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
39
|
+
log_path = os.path.join(log_dir, f"{service_name}.log")
|
|
40
|
+
log_file = open(log_path, "a", buffering=1, encoding="utf-8")
|
|
41
|
+
self._service_log_files.append(log_file)
|
|
42
|
+
return log_file, log_path
|
|
43
|
+
|
|
44
|
+
def _service_stdio(self, service_name: str):
|
|
45
|
+
if self._services_log_to_console():
|
|
46
|
+
return sys.stdout, sys.stderr, None
|
|
47
|
+
|
|
48
|
+
log_file, log_path = self._open_service_log(service_name)
|
|
49
|
+
return log_file, subprocess.STDOUT, log_path
|
|
50
|
+
|
|
51
|
+
def _close_service_log_files(self):
|
|
52
|
+
for log_file in getattr(self, "_service_log_files", []):
|
|
53
|
+
try:
|
|
54
|
+
log_file.close()
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
self._service_log_files = []
|
|
58
|
+
|
|
27
59
|
def _check_redis_available(self) -> Tuple[bool, str]:
|
|
28
60
|
"""
|
|
29
61
|
Preflight check for Redis reachability using Django settings.
|
|
@@ -66,13 +98,13 @@ class DjangoCommands(BaseCommands):
|
|
|
66
98
|
|
|
67
99
|
@cmd2.with_category('Django Commands')
|
|
68
100
|
def do_runserver(self, arg):
|
|
69
|
-
'Start Django development server, Daphne WebSocket server, MCP
|
|
101
|
+
'Start Django development server, Daphne WebSocket server, MCP HTTP server, and Celery worker in the background'
|
|
70
102
|
if self.django_server_process or self.daphne_server_process:
|
|
71
103
|
self.poutput("Servers are already running.")
|
|
72
104
|
return
|
|
73
105
|
|
|
74
106
|
try:
|
|
75
|
-
logger.info("Attempting to start Django, Daphne, MCP
|
|
107
|
+
logger.info("Attempting to start Django, Daphne, MCP HTTP server, and Celery servers in background...")
|
|
76
108
|
|
|
77
109
|
# Preflight: check Redis and fail fast if unavailable
|
|
78
110
|
redis_ok, redis_msg = self._check_redis_available()
|
|
@@ -97,11 +129,11 @@ class DjangoCommands(BaseCommands):
|
|
|
97
129
|
sys.executable,
|
|
98
130
|
'-m',
|
|
99
131
|
'iotsploit_mcp.cli',
|
|
100
|
-
'
|
|
132
|
+
'http',
|
|
101
133
|
'--host',
|
|
102
|
-
'
|
|
134
|
+
'127.0.0.1',
|
|
103
135
|
'--port',
|
|
104
|
-
'
|
|
136
|
+
'9900',
|
|
105
137
|
]
|
|
106
138
|
celery_cmd = [
|
|
107
139
|
sys.executable,
|
|
@@ -112,54 +144,64 @@ class DjangoCommands(BaseCommands):
|
|
|
112
144
|
'worker',
|
|
113
145
|
'--loglevel=info'
|
|
114
146
|
]
|
|
147
|
+
service_env = os.environ.copy()
|
|
115
148
|
|
|
116
149
|
logger.info(f"Running Django command: {' '.join(django_cmd)}")
|
|
117
150
|
logger.info(f"Running Daphne command: {' '.join(daphne_cmd)}")
|
|
118
|
-
logger.info(f"Running MCP
|
|
151
|
+
logger.info(f"Running MCP HTTP server command: {' '.join(mcp_bridge_cmd)}")
|
|
119
152
|
logger.info(f"Running Celery command: {' '.join(celery_cmd)}")
|
|
153
|
+
if not self._services_log_to_console():
|
|
154
|
+
logger.info(f"Service logs are redirected to {os.getenv('IOTSPLOIT_SERVICE_LOG_DIR', '/tmp/sat_logs')}")
|
|
120
155
|
|
|
121
156
|
# Start the processes with direct output to stdout/stderr
|
|
157
|
+
django_stdout, django_stderr, _ = self._service_stdio("django")
|
|
122
158
|
self.django_server_process = subprocess.Popen(
|
|
123
159
|
django_cmd,
|
|
124
|
-
stdout=
|
|
125
|
-
stderr=
|
|
126
|
-
universal_newlines=True
|
|
160
|
+
stdout=django_stdout,
|
|
161
|
+
stderr=django_stderr,
|
|
162
|
+
universal_newlines=True,
|
|
163
|
+
env=service_env,
|
|
127
164
|
)
|
|
128
165
|
|
|
166
|
+
daphne_stdout, daphne_stderr, _ = self._service_stdio("daphne")
|
|
129
167
|
self.daphne_server_process = subprocess.Popen(
|
|
130
168
|
daphne_cmd,
|
|
131
|
-
stdout=
|
|
132
|
-
stderr=
|
|
133
|
-
universal_newlines=True
|
|
169
|
+
stdout=daphne_stdout,
|
|
170
|
+
stderr=daphne_stderr,
|
|
171
|
+
universal_newlines=True,
|
|
172
|
+
env=service_env,
|
|
134
173
|
)
|
|
135
174
|
|
|
136
|
-
# Start the
|
|
137
|
-
# later terminate the entire group
|
|
175
|
+
# Start the MCP HTTP server in its own process group so that we can
|
|
176
|
+
# later terminate the entire group
|
|
138
177
|
# Set up environment variables for MCP bridge (Django API URL)
|
|
139
|
-
mcp_env =
|
|
178
|
+
mcp_env = service_env.copy()
|
|
140
179
|
mcp_env.setdefault('IOTSPLOIT_DJANGO_API_BASE_URL', 'http://127.0.0.1:8888')
|
|
141
180
|
|
|
181
|
+
mcp_stdout, mcp_stderr, _ = self._service_stdio("mcp")
|
|
142
182
|
self.mcp_bridge_process = subprocess.Popen(
|
|
143
183
|
mcp_bridge_cmd,
|
|
144
|
-
stdout=
|
|
145
|
-
stderr=
|
|
184
|
+
stdout=mcp_stdout,
|
|
185
|
+
stderr=mcp_stderr,
|
|
146
186
|
universal_newlines=True,
|
|
147
187
|
start_new_session=True, # create new session = new PGID on POSIX
|
|
148
188
|
env=mcp_env
|
|
149
189
|
)
|
|
150
190
|
|
|
191
|
+
celery_stdout, celery_stderr, _ = self._service_stdio("celery")
|
|
151
192
|
self.celery_worker_process = subprocess.Popen(
|
|
152
193
|
celery_cmd,
|
|
153
|
-
stdout=
|
|
154
|
-
stderr=
|
|
155
|
-
universal_newlines=True
|
|
194
|
+
stdout=celery_stdout,
|
|
195
|
+
stderr=celery_stderr,
|
|
196
|
+
universal_newlines=True,
|
|
197
|
+
env=service_env,
|
|
156
198
|
)
|
|
157
199
|
|
|
158
200
|
logger.info("All servers started successfully in the background.")
|
|
159
201
|
logger.info("Services running on:")
|
|
160
202
|
logger.info(" - Django HTTP API: http://localhost:8888")
|
|
161
203
|
logger.info(" - Daphne WebSocket: ws://localhost:9999")
|
|
162
|
-
logger.info(" - MCP
|
|
204
|
+
logger.info(" - MCP HTTP (Streamable HTTP): http://127.0.0.1:9900/mcp")
|
|
163
205
|
logger.info(" - Celery Worker: background task processing")
|
|
164
206
|
|
|
165
207
|
# Wait for HTTP server to be available and initialize devices
|
|
@@ -197,7 +239,7 @@ class DjangoCommands(BaseCommands):
|
|
|
197
239
|
|
|
198
240
|
@cmd2.with_category('Django Commands')
|
|
199
241
|
def do_stop_server(self, arg):
|
|
200
|
-
'Stop Django development server, Daphne WebSocket server, MCP
|
|
242
|
+
'Stop Django development server, Daphne WebSocket server, MCP HTTP server, and Celery worker'
|
|
201
243
|
try:
|
|
202
244
|
# Cleanup devices using HTTP endpoint (GET method)
|
|
203
245
|
# Only attempt HTTP cleanup if the Django server process is still alive
|
|
@@ -241,6 +283,8 @@ class DjangoCommands(BaseCommands):
|
|
|
241
283
|
if self.celery_worker_process:
|
|
242
284
|
self.celery_worker_process.terminate()
|
|
243
285
|
self.celery_worker_process = None
|
|
286
|
+
|
|
287
|
+
self._close_service_log_files()
|
|
244
288
|
|
|
245
289
|
if not any([self.django_server_process, self.daphne_server_process,
|
|
246
290
|
getattr(self, 'mcp_bridge_process', None),
|
|
@@ -4,7 +4,7 @@ import cmd2
|
|
|
4
4
|
from cmd2 import ansi
|
|
5
5
|
import time
|
|
6
6
|
from .base_commands import BaseCommands
|
|
7
|
-
from
|
|
7
|
+
from iotsploit_platforms import get_shared_wifi_backend
|
|
8
8
|
from iotsploit_django.tools.input_mgr import Input_Mgr
|
|
9
9
|
from iotsploit_core.utils import iots_logger
|
|
10
10
|
|
|
@@ -27,10 +27,14 @@ class NetworkCommands(BaseCommands):
|
|
|
27
27
|
|
|
28
28
|
# Attempt to connect
|
|
29
29
|
logger.info(ansi.style(f"Attempting to connect to {ssid}...", fg=ansi.Fg.CYAN))
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
wifi_backend = get_shared_wifi_backend()
|
|
31
|
+
try:
|
|
32
|
+
wifi_backend.sta_connect(str(ssid), str(password))
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logger.error(ansi.style(f"WiFi connection failed: {e}", fg=ansi.Fg.RED))
|
|
35
|
+
|
|
32
36
|
# Wait for connection to establish
|
|
33
37
|
time.sleep(2)
|
|
34
|
-
|
|
38
|
+
|
|
35
39
|
# Show connection status
|
|
36
|
-
|
|
40
|
+
wifi_backend.status()
|
|
@@ -5,8 +5,6 @@ from cmd2 import ansi
|
|
|
5
5
|
from .base_commands import BaseCommands
|
|
6
6
|
from iotsploit_core.core.exploit_spec import ExploitResult
|
|
7
7
|
from iotsploit_django.tools.input_mgr import Input_Mgr
|
|
8
|
-
from iotsploit_django.adapters.django.plugins.models import Plugin
|
|
9
|
-
from iotsploit_django.adapters.django.plugins.models import PluginGroup, PluginGroupTree
|
|
10
8
|
from iotsploit_core.utils import iots_logger
|
|
11
9
|
|
|
12
10
|
logger = iots_logger.get_logger(__name__)
|
|
@@ -66,58 +64,80 @@ class PluginCommands(BaseCommands):
|
|
|
66
64
|
target_manager = self.target_manager
|
|
67
65
|
current_target = target_manager.get_current_target()
|
|
68
66
|
|
|
69
|
-
# Prepare target dictionary
|
|
67
|
+
# Prepare target dictionary (device/target info only, e.g. ip_address)
|
|
70
68
|
target_dict = {}
|
|
71
|
-
|
|
72
|
-
# If we have a target, include its properties
|
|
73
69
|
if current_target:
|
|
74
|
-
# Add target properties to target dictionary
|
|
75
70
|
target_dict = current_target.get_info() if hasattr(current_target, 'get_info') else {}
|
|
76
|
-
|
|
77
|
-
#
|
|
71
|
+
|
|
72
|
+
# Collect the plugin's declared parameters separately from the target.
|
|
73
|
+
# Plugins read their params from `parameters` (not `target`), so these
|
|
74
|
+
# must be passed as parameters=, matching the HTTP/GUI execute path.
|
|
75
|
+
# We prompt for every declared parameter (not only required ones) so the
|
|
76
|
+
# TUI mirrors the GUI. Each prompt is labeled REQUIRED or optional:
|
|
77
|
+
# - required -> the user must enter a non-empty value.
|
|
78
|
+
# - optional -> the declared default is shown; pressing Enter skips it.
|
|
79
|
+
parameters = {}
|
|
78
80
|
for param_name, param_info in plugin_params.items():
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
# Skip params already satisfied by the current target
|
|
82
|
+
if param_name in target_dict:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
param_type = param_info.get('type', 'str')
|
|
86
|
+
required = param_info.get('required', False)
|
|
87
|
+
description = param_info.get('description', f"Enter {param_name}")
|
|
88
|
+
default = param_info.get('default')
|
|
89
|
+
validation = param_info.get('validation', {})
|
|
90
|
+
|
|
91
|
+
# Label so the user knows whether the field is mandatory or skippable
|
|
92
|
+
if required:
|
|
93
|
+
prompt = f"{description} (REQUIRED)"
|
|
94
|
+
elif default is not None:
|
|
95
|
+
prompt = f"{description} (optional, default: {default}, press Enter to skip)"
|
|
96
|
+
else:
|
|
97
|
+
prompt = f"{description} (optional, press Enter to skip)"
|
|
98
|
+
|
|
99
|
+
if param_type == 'int':
|
|
100
|
+
min_val = validation.get('min')
|
|
101
|
+
max_val = validation.get('max')
|
|
102
|
+
value = Input_Mgr.Instance().int_input(
|
|
103
|
+
prompt,
|
|
104
|
+
default=default if isinstance(default, int) else None,
|
|
105
|
+
min_val=min_val,
|
|
106
|
+
max_val=max_val
|
|
107
|
+
)
|
|
108
|
+
elif param_type == 'bool':
|
|
109
|
+
value = Input_Mgr.Instance().yes_no_input(
|
|
110
|
+
prompt,
|
|
111
|
+
default=bool(default) if default is not None else True
|
|
112
|
+
)
|
|
113
|
+
elif param_type == 'str' and 'choices' in validation:
|
|
114
|
+
value = Input_Mgr.Instance().single_choice(
|
|
115
|
+
f"{prompt} (Choose one)",
|
|
116
|
+
validation['choices']
|
|
117
|
+
)
|
|
118
|
+
elif required:
|
|
119
|
+
# Mandatory free-text: re-prompt until the user enters something
|
|
120
|
+
value = Input_Mgr.Instance().string_input(
|
|
121
|
+
prompt,
|
|
122
|
+
verify_func=lambda s: bool(s and s.strip())
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
# Optional free-text: empty input skips the field. Fall back to the
|
|
126
|
+
# declared default if there is one, otherwise omit it entirely so
|
|
127
|
+
# the plugin applies its own default.
|
|
128
|
+
value = Input_Mgr.Instance().string_input(prompt)
|
|
129
|
+
if value is None or value == "":
|
|
130
|
+
if default is not None:
|
|
131
|
+
value = default
|
|
92
132
|
else:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
min_val=min_val,
|
|
102
|
-
max_val=max_val
|
|
103
|
-
)
|
|
104
|
-
elif param_type == 'bool':
|
|
105
|
-
# Boolean input
|
|
106
|
-
value = Input_Mgr.Instance().yes_no_input(
|
|
107
|
-
description,
|
|
108
|
-
default=default if default is not None else True
|
|
109
|
-
)
|
|
110
|
-
else:
|
|
111
|
-
# Default to string for unknown types
|
|
112
|
-
value = Input_Mgr.Instance().string_input(description)
|
|
113
|
-
|
|
114
|
-
# Add to target dict
|
|
115
|
-
target_dict[param_name] = value
|
|
116
|
-
|
|
117
|
-
logger.debug(f"Executing plugin with target configuration: {target_dict}")
|
|
118
|
-
|
|
119
|
-
# Now execute the plugin with our target dictionary
|
|
120
|
-
result = self.plugin_manager.execute_plugin(choice, target=target_dict)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
parameters[param_name] = value
|
|
136
|
+
|
|
137
|
+
logger.debug(f"Executing plugin '{choice}' with target={target_dict}, parameters={parameters}")
|
|
138
|
+
|
|
139
|
+
# Execute with target (device info) and parameters (plugin inputs) kept separate
|
|
140
|
+
result = self.plugin_manager.execute_plugin(choice, target=target_dict, parameters=parameters)
|
|
121
141
|
|
|
122
142
|
# Check if this is an async execution
|
|
123
143
|
if isinstance(result, dict) and result.get('execution_type') == 'async':
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import cmd2
|
|
4
4
|
from cmd2 import ansi
|
|
5
|
+
import os
|
|
5
6
|
from .base_commands import BaseCommands
|
|
6
7
|
from iotsploit_core.core.exploit_spec import ExploitResult
|
|
7
8
|
from iotsploit_django.tools.input_mgr import Input_Mgr
|
|
9
|
+
from iotsploit_django.tools.xlogger import xlog
|
|
8
10
|
from iotsploit_core.utils import iots_logger
|
|
9
11
|
|
|
10
12
|
logger = iots_logger.get_logger(__name__)
|
|
@@ -74,4 +76,46 @@ class SystemCommands(BaseCommands):
|
|
|
74
76
|
logger.debug("Detailed error:", exc_info=True)
|
|
75
77
|
|
|
76
78
|
# Add an alias for set_log_level
|
|
77
|
-
do_sll = do_set_log_level
|
|
79
|
+
do_sll = do_set_log_level
|
|
80
|
+
|
|
81
|
+
@cmd2.with_category('System Commands')
|
|
82
|
+
def do_set_log_format(self, arg):
|
|
83
|
+
'Set the terminal logging format (standard, compact, plain)'
|
|
84
|
+
valid_formats = ['standard', 'compact', 'plain']
|
|
85
|
+
|
|
86
|
+
if not arg:
|
|
87
|
+
selected_format = Input_Mgr.Instance().single_choice(
|
|
88
|
+
"Select terminal log format",
|
|
89
|
+
valid_formats
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
selected_format = arg.strip().lower()
|
|
93
|
+
if selected_format not in valid_formats:
|
|
94
|
+
logger.error(ansi.style(f"Invalid log format. Choose from: {', '.join(valid_formats)}", fg=ansi.Fg.RED))
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
os.environ["IOTSPLOIT_LOG_FORMAT"] = selected_format
|
|
99
|
+
iots_logger.set_format(selected_format)
|
|
100
|
+
xlog.set_format(selected_format)
|
|
101
|
+
logger.info(ansi.style(f"Log format set to {selected_format}", fg=ansi.Fg.GREEN))
|
|
102
|
+
running_services = any(
|
|
103
|
+
getattr(self, attr, None)
|
|
104
|
+
for attr in (
|
|
105
|
+
"django_server_process",
|
|
106
|
+
"daphne_server_process",
|
|
107
|
+
"mcp_bridge_process",
|
|
108
|
+
"celery_worker_process",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
if running_services and selected_format != "standard":
|
|
112
|
+
logger.warning(
|
|
113
|
+
"Already-running service processes keep their current terminal output. "
|
|
114
|
+
"Run stop_server then runserver to redirect service logs to /tmp/sat_logs."
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(ansi.style(f"Error setting log format: {str(e)}", fg=ansi.Fg.RED))
|
|
118
|
+
logger.debug("Detailed error:", exc_info=True)
|
|
119
|
+
|
|
120
|
+
# Add an alias for set_log_format
|
|
121
|
+
do_slf = do_set_log_format
|
|
@@ -7,6 +7,29 @@ import inspect
|
|
|
7
7
|
from typing import Dict
|
|
8
8
|
import argparse
|
|
9
9
|
|
|
10
|
+
LOG_FORMAT_CHOICES = ("standard", "compact", "plain")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _prime_log_format_env_from_argv(argv):
|
|
14
|
+
selected_format = None
|
|
15
|
+
idx = 0
|
|
16
|
+
while idx < len(argv):
|
|
17
|
+
arg = argv[idx]
|
|
18
|
+
if arg == '--plain-log' and selected_format is None:
|
|
19
|
+
selected_format = "plain"
|
|
20
|
+
elif arg == '--log-format' and idx + 1 < len(argv):
|
|
21
|
+
selected_format = argv[idx + 1].strip().lower()
|
|
22
|
+
idx += 1
|
|
23
|
+
elif arg.startswith('--log-format='):
|
|
24
|
+
selected_format = arg.split('=', 1)[1].strip().lower()
|
|
25
|
+
idx += 1
|
|
26
|
+
|
|
27
|
+
if selected_format in LOG_FORMAT_CHOICES:
|
|
28
|
+
os.environ["IOTSPLOIT_LOG_FORMAT"] = selected_format
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_prime_log_format_env_from_argv(sys.argv[1:])
|
|
32
|
+
|
|
10
33
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "iotsploit_django.settings.dev")
|
|
11
34
|
|
|
12
35
|
import django
|
|
@@ -66,6 +89,20 @@ ensure_database_initialized()
|
|
|
66
89
|
# Now it's safe to import Django and other modules
|
|
67
90
|
import cmd2
|
|
68
91
|
from cmd2 import ansi
|
|
92
|
+
|
|
93
|
+
# Command palette imports (prompt-toolkit based live command discovery).
|
|
94
|
+
# Wrapped in try/except so the shell still works if prompt-toolkit is missing.
|
|
95
|
+
try:
|
|
96
|
+
from iotsploit_cli.command_palette import (
|
|
97
|
+
CommandCatalog,
|
|
98
|
+
Cmd2CompletionAdapter,
|
|
99
|
+
CommandPaletteCompleter,
|
|
100
|
+
PaletteInputSession,
|
|
101
|
+
)
|
|
102
|
+
_PALETTE_AVAILABLE = True
|
|
103
|
+
except ImportError: # pragma: no cover
|
|
104
|
+
_PALETTE_AVAILABLE = False
|
|
105
|
+
|
|
69
106
|
from iotsploit_django.adapters.django.target_models import TargetManager
|
|
70
107
|
from iotsploit_core.domain.target import Vehicle
|
|
71
108
|
from iotsploit_django.composition_root.wiring import get_device_driver_manager, get_exploit_plugin_manager
|
|
@@ -75,8 +112,7 @@ from iotsploit_django.tools.env_mgr import Env_Mgr
|
|
|
75
112
|
from iotsploit_django.tools.report_mgr import Report_Mgr
|
|
76
113
|
from iotsploit_django.tools.input_mgr import Input_Mgr
|
|
77
114
|
from iotsploit_django.tools.xlogger import xlog as logger
|
|
78
|
-
from
|
|
79
|
-
term.term_mode = True
|
|
115
|
+
from iotsploit_core.utils import iots_logger
|
|
80
116
|
|
|
81
117
|
def global_exception_handler(exctype, value, traceback):
|
|
82
118
|
logger.error("Unhandled exception", exc_info=(exctype, value, traceback))
|
|
@@ -227,12 +263,83 @@ class SAT_Shell(SAT_Shell_Base):
|
|
|
227
263
|
'run_script': 'Shell Commands',
|
|
228
264
|
'runserver': 'Shell Commands',
|
|
229
265
|
'set': 'Shell Commands',
|
|
266
|
+
'set_log_format': 'Shell Commands',
|
|
230
267
|
'set_log_level': 'Shell Commands',
|
|
231
268
|
'shell': 'Shell Commands',
|
|
232
269
|
'shortcuts': 'Shell Commands',
|
|
270
|
+
'slf': 'Shell Commands',
|
|
271
|
+
'sll': 'Shell Commands',
|
|
233
272
|
'stop_server': 'Shell Commands',
|
|
234
273
|
})
|
|
235
274
|
|
|
275
|
+
# -- Command palette initialization (TTY-only) --------------------- #
|
|
276
|
+
# The palette uses prompt-toolkit for live command discovery.
|
|
277
|
+
# It is only initialized when stdin/stdout are TTYs so that non-interactive
|
|
278
|
+
# use (pipes, scripts, tests) bypasses it entirely.
|
|
279
|
+
self._palette_session = None
|
|
280
|
+
if (
|
|
281
|
+
_PALETTE_AVAILABLE
|
|
282
|
+
and sys.stdin.isatty()
|
|
283
|
+
and sys.stdout.isatty()
|
|
284
|
+
):
|
|
285
|
+
try:
|
|
286
|
+
catalog = CommandCatalog(self)
|
|
287
|
+
adapter = Cmd2CompletionAdapter(self)
|
|
288
|
+
completer = CommandPaletteCompleter(self, catalog, adapter)
|
|
289
|
+
self._palette_session = PaletteInputSession(self, completer)
|
|
290
|
+
except Exception:
|
|
291
|
+
self._palette_session = None
|
|
292
|
+
|
|
293
|
+
def read_input(
|
|
294
|
+
self,
|
|
295
|
+
prompt: str,
|
|
296
|
+
*,
|
|
297
|
+
history=None,
|
|
298
|
+
completion_mode=cmd2.utils.CompletionMode.NONE,
|
|
299
|
+
preserve_quotes=False,
|
|
300
|
+
choices=None,
|
|
301
|
+
choices_provider=None,
|
|
302
|
+
completer=None,
|
|
303
|
+
parser=None,
|
|
304
|
+
) -> str:
|
|
305
|
+
"""Override cmd2's ``read_input`` to use the palette for top-level prompts.
|
|
306
|
+
|
|
307
|
+
Only intercepts when:
|
|
308
|
+
* ``completion_mode`` is ``COMMANDS`` (the top-level interactive prompt)
|
|
309
|
+
* stdin and stdout are TTYs
|
|
310
|
+
* ``use_rawinput`` is True
|
|
311
|
+
* the palette session was successfully initialized
|
|
312
|
+
* we are not at a continuation prompt
|
|
313
|
+
|
|
314
|
+
Everything else delegates to ``super().read_input()`` unchanged.
|
|
315
|
+
"""
|
|
316
|
+
if (
|
|
317
|
+
completion_mode == cmd2.utils.CompletionMode.COMMANDS
|
|
318
|
+
and self._palette_session is not None
|
|
319
|
+
and not getattr(self, "_at_continuation_prompt", False)
|
|
320
|
+
and sys.stdin.isatty()
|
|
321
|
+
and sys.stdout.isatty()
|
|
322
|
+
and self.use_rawinput
|
|
323
|
+
):
|
|
324
|
+
try:
|
|
325
|
+
return self._palette_session.prompt(prompt)
|
|
326
|
+
except (KeyboardInterrupt, EOFError):
|
|
327
|
+
raise
|
|
328
|
+
except Exception:
|
|
329
|
+
# Safe fallback: use cmd2's default input on any unexpected error
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
return super().read_input(
|
|
333
|
+
prompt,
|
|
334
|
+
history=history,
|
|
335
|
+
completion_mode=completion_mode,
|
|
336
|
+
preserve_quotes=preserve_quotes,
|
|
337
|
+
choices=choices,
|
|
338
|
+
choices_provider=choices_provider,
|
|
339
|
+
completer=completer,
|
|
340
|
+
parser=parser,
|
|
341
|
+
)
|
|
342
|
+
|
|
236
343
|
def emptyline(self):
|
|
237
344
|
self.onecmd("help")
|
|
238
345
|
|
|
@@ -418,8 +525,8 @@ class SAT_Shell(SAT_Shell_Base):
|
|
|
418
525
|
logger.info(f" Device: {current_device.name}")
|
|
419
526
|
logger.info(f" State: {state.value}")
|
|
420
527
|
else:
|
|
421
|
-
logger.info(
|
|
422
|
-
logger.info(
|
|
528
|
+
logger.info(" Device: No device connected")
|
|
529
|
+
logger.info(" State: unknown")
|
|
423
530
|
|
|
424
531
|
commands = self.device_driver_manager.get_supported_commands(driver_name)
|
|
425
532
|
if commands:
|
|
@@ -448,8 +555,29 @@ def main():
|
|
|
448
555
|
|
|
449
556
|
parser = argparse.ArgumentParser(description='SAT Shell entrypoint')
|
|
450
557
|
parser.add_argument('--runserver', action='store_true', help='Start servers directly and keep running until Ctrl+C; on Ctrl+C, stop servers')
|
|
558
|
+
parser.add_argument(
|
|
559
|
+
'--log-format',
|
|
560
|
+
choices=LOG_FORMAT_CHOICES,
|
|
561
|
+
default=None,
|
|
562
|
+
help='Set terminal log format. Overrides --plain-log and IOTSPLOIT_LOG_FORMAT.',
|
|
563
|
+
)
|
|
564
|
+
parser.add_argument(
|
|
565
|
+
'--plain-log',
|
|
566
|
+
action='store_true',
|
|
567
|
+
help='Use message-only terminal logs unless --log-format is also provided.',
|
|
568
|
+
)
|
|
451
569
|
args = parser.parse_args()
|
|
452
570
|
|
|
571
|
+
selected_log_format = (
|
|
572
|
+
args.log_format
|
|
573
|
+
or ("plain" if args.plain_log else None)
|
|
574
|
+
or os.getenv("IOTSPLOIT_LOG_FORMAT", "standard").strip().lower()
|
|
575
|
+
)
|
|
576
|
+
if selected_log_format not in LOG_FORMAT_CHOICES:
|
|
577
|
+
selected_log_format = "standard"
|
|
578
|
+
iots_logger.set_format(selected_log_format)
|
|
579
|
+
logger.set_format(selected_log_format)
|
|
580
|
+
|
|
453
581
|
shell = SAT_Shell()
|
|
454
582
|
Report_Mgr.Instance().log_init()
|
|
455
583
|
Env_Mgr.Instance().set("SAT_RUN_IN_SHELL", True)
|
|
@@ -490,4 +618,3 @@ def main():
|
|
|
490
618
|
|
|
491
619
|
if __name__ == '__main__':
|
|
492
620
|
main()
|
|
493
|
-
|
iotsploit_cli-0.0.6/PKG-INFO
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: iotsploit-cli
|
|
3
|
-
Version: 0.0.6
|
|
4
|
-
Summary: IoTSploit CLI shell (console + command modules) - IoT security testing interactive interface
|
|
5
|
-
License: GPL-3.0-or-later
|
|
6
|
-
Keywords: iot,security,testing,pentest,cli,shell
|
|
7
|
-
Author: IoTSploit Team
|
|
8
|
-
Author-email: support@iotsploit.org
|
|
9
|
-
Requires-Python: >=3.10,<4.0
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Intended Audience :: Information Technology
|
|
14
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Topic :: Security
|
|
21
|
-
Classifier: Topic :: System :: Hardware
|
|
22
|
-
Requires-Dist: cmd2 (>=2.4,<3.0)
|
|
23
|
-
Requires-Dist: iotsploit-core (>=0.0.6,<0.0.7)
|
|
24
|
-
Requires-Dist: iotsploit-django (>=0.0.6,<0.0.7)
|
|
25
|
-
Requires-Dist: iotsploit-drivers (>=0.0.6,<0.0.7)
|
|
26
|
-
Requires-Dist: iotsploit-exploits (>=0.0.6,<0.0.7)
|
|
27
|
-
Requires-Dist: iotsploit-mcp (>=0.0.6,<0.0.7)
|
|
28
|
-
Requires-Dist: pwntools (>=4.12,<5.0)
|
|
29
|
-
Project-URL: Documentation, https://www.iotsploit.org/
|
|
30
|
-
Project-URL: Homepage, https://www.iotsploit.org/
|
|
31
|
-
Project-URL: Repository, https://github.com/TKXB/iotsploit
|
|
32
|
-
Description-Content-Type: text/markdown
|
|
33
|
-
|
|
34
|
-
# iotsploit-cli
|
|
35
|
-
|
|
36
|
-
IoTSploit interactive CLI shell for IoT security testing.
|
|
37
|
-
|
|
38
|
-
## Overview
|
|
39
|
-
|
|
40
|
-
This package provides the `iotsploit` command-line shell built on top of `cmd2`.
|
|
41
|
-
It bundles the core console loop (`console.py`) and all command modules
|
|
42
|
-
(`commands/`) that implement device management, plugin execution, target
|
|
43
|
-
management, network operations, and more.
|
|
44
|
-
|
|
45
|
-
## Installation
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install iotsploit-cli
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
iotsploit
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Or with the Django server started immediately:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
iotsploit --runserver
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## License
|
|
64
|
-
|
|
65
|
-
GPL-3.0-or-later. See [LICENSE](../LICENSE) for details.
|
|
66
|
-
For commercial use, contact wang3919379@gmail.com.
|
|
67
|
-
|
iotsploit_cli-0.0.6/README.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# iotsploit-cli
|
|
2
|
-
|
|
3
|
-
IoTSploit interactive CLI shell for IoT security testing.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This package provides the `iotsploit` command-line shell built on top of `cmd2`.
|
|
8
|
-
It bundles the core console loop (`console.py`) and all command modules
|
|
9
|
-
(`commands/`) that implement device management, plugin execution, target
|
|
10
|
-
management, network operations, and more.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
pip install iotsploit-cli
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Usage
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
iotsploit
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Or with the Django server started immediately:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
iotsploit --runserver
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## License
|
|
31
|
-
|
|
32
|
-
GPL-3.0-or-later. See [LICENSE](../LICENSE) for details.
|
|
33
|
-
For commercial use, contact wang3919379@gmail.com.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|