iotsploit-cli 0.0.7__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.
@@ -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.7"
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"
@@ -35,7 +35,7 @@ iotsploit-drivers = "*"
35
35
  iotsploit-exploits = "*"
36
36
  iotsploit-mcp = "*"
37
37
  cmd2 = "^2.4"
38
- pwntools = "^4.12"
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
- discovered_devices = device_registry.scan_devices()
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.
@@ -112,47 +144,57 @@ 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
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=sys.stdout, # 直接输出到控制台
125
- stderr=sys.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=sys.stdout, # 直接输出到控制台
132
- stderr=sys.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
175
  # Start the MCP HTTP server in its own process group so that we can
137
176
  # later terminate the entire group
138
177
  # Set up environment variables for MCP bridge (Django API URL)
139
- mcp_env = os.environ.copy()
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=sys.stdout, # 直接输出到控制台
145
- stderr=sys.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=sys.stdout, # 直接输出到控制台
154
- stderr=sys.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.")
@@ -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),
@@ -3,7 +3,6 @@
3
3
  import cmd2
4
4
  from cmd2 import ansi
5
5
  from .base_commands import BaseCommands
6
- from iotsploit_django.tools.input_mgr import Input_Mgr
7
6
  from iotsploit_core.core.tool_service import get_firmware_service
8
7
  from iotsploit_core.utils import iots_logger
9
8
 
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
 
3
3
  import cmd2
4
- from cmd2 import ansi
5
4
  import subprocess
6
5
  from .base_commands import BaseCommands
7
6
  from iotsploit_core.utils import iots_logger
@@ -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 iotsploit_django.tools.wifi_mgr import WiFi_Mgr
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
- WiFi_Mgr.Instance().sta_connect_wifi(ssid, password)
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
- WiFi_Mgr.Instance().status()
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
- # Prompt for required parameters that are not in the target
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
- if param_name not in target_dict and param_info.get('required', False):
80
- param_type = param_info.get('type', 'str')
81
- description = param_info.get('description', f"Enter {param_name}")
82
- default = param_info.get('default')
83
- validation = param_info.get('validation', {})
84
-
85
- if param_type == 'str':
86
- if 'choices' in validation:
87
- # Use single_choice for string with choices
88
- value = Input_Mgr.Instance().single_choice(
89
- f"{description} (Choose one)",
90
- validation['choices']
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
- # Regular string input
94
- value = Input_Mgr.Instance().string_input(description)
95
- elif param_type == 'int':
96
- # Integer input with optional min/max validation
97
- min_val = validation.get('min')
98
- max_val = validation.get('max')
99
- value = Input_Mgr.Instance().int_input(
100
- description,
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
@@ -159,7 +159,6 @@ class TargetCommands(BaseCommands):
159
159
 
160
160
  else:
161
161
  # Handle regular field editing
162
- current_value = target.get(field, '')
163
162
  new_value = Input_Mgr.Instance().string_input(
164
163
  f"Enter new value for {field}"
165
164
  )
@@ -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 pwnlib import term
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(f" Device: No device connected")
422
- logger.info(f" State: unknown")
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
-
@@ -1,68 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: iotsploit-cli
3
- Version: 0.0.7
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: pwntools (>=4.12,<5.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
- ## License
65
-
66
- GPL-3.0-or-later. See [LICENSE](../LICENSE) for details.
67
- For commercial use, contact wang3919379@gmail.com.
68
-
@@ -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.