netexec-mcp 1.0.0__py3-none-any.whl
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.
- netexec_mcp/__init__.py +3 -0
- netexec_mcp/auth.py +156 -0
- netexec_mcp/config.py +219 -0
- netexec_mcp/dynamic.py +595 -0
- netexec_mcp/executor.py +388 -0
- netexec_mcp/guardrails.py +228 -0
- netexec_mcp/meta.py +212 -0
- netexec_mcp/modules/__init__.py +39 -0
- netexec_mcp/modules/ftp.py +133 -0
- netexec_mcp/modules/ldap.py +910 -0
- netexec_mcp/modules/mssql.py +619 -0
- netexec_mcp/modules/nfs.py +174 -0
- netexec_mcp/modules/rdp.py +251 -0
- netexec_mcp/modules/smb.py +1687 -0
- netexec_mcp/modules/ssh.py +209 -0
- netexec_mcp/modules/vnc.py +96 -0
- netexec_mcp/modules/winrm.py +329 -0
- netexec_mcp/modules/wmi.py +259 -0
- netexec_mcp/modules/workspace.py +431 -0
- netexec_mcp/resources.py +319 -0
- netexec_mcp/results.py +829 -0
- netexec_mcp/server.py +242 -0
- netexec_mcp-1.0.0.dist-info/METADATA +206 -0
- netexec_mcp-1.0.0.dist-info/RECORD +27 -0
- netexec_mcp-1.0.0.dist-info/WHEEL +4 -0
- netexec_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- netexec_mcp-1.0.0.dist-info/licenses/LICENSE +23 -0
netexec_mcp/__init__.py
ADDED
netexec_mcp/auth.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Shared authentication model -> nxc flags.
|
|
2
|
+
|
|
3
|
+
Every protocol module accepts the same credential parameters; this module turns
|
|
4
|
+
them into the argv flags nxc expects, with light validation of nonsensical
|
|
5
|
+
combinations. It builds *only* the auth-related flags -- targets and per-tool
|
|
6
|
+
action flags (e.g. ``--shares``) are added by the caller.
|
|
7
|
+
|
|
8
|
+
Mapping (per PLAN.md "Shared auth model -> flags"):
|
|
9
|
+
username -> -u
|
|
10
|
+
password -> -p
|
|
11
|
+
ntlm_hash -> -H (pass-the-hash)
|
|
12
|
+
domain -> -d
|
|
13
|
+
local_auth -> --local-auth
|
|
14
|
+
kerberos -> -k
|
|
15
|
+
use_kcache -> --use-kcache (implies kerberos)
|
|
16
|
+
cred_id -> -id (use a credential already stored in nxc's DB)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthError(ValueError):
|
|
23
|
+
"""Raised for contradictory credential parameters (nothing runs)."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _as_list(value) -> list[str]:
|
|
27
|
+
"""Normalise a str | list[str] | None credential field to a list of tokens."""
|
|
28
|
+
if value is None:
|
|
29
|
+
return []
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
return [value]
|
|
32
|
+
return [str(v) for v in value]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# The blank/empty-LM hash. nxc's DB stores a pass-the-hash secret as `<LM>:<NT>`, and for a
|
|
36
|
+
# modern account the LM half is this constant. Passing the full `<blankLM>:<NT>` can fail
|
|
37
|
+
# auth (observed live: NTDS dump -> STATUS_LOGON_FAILURE on a DC that refuses LM) while the
|
|
38
|
+
# bare NT hash succeeds -- so reduce a blank-LM pair to just the NT half.
|
|
39
|
+
_BLANK_LM = "aad3b435b51404eeaad3b435b51404ee"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _nt_only(h: str) -> str:
|
|
43
|
+
"""Reduce a `LM:NT` hash whose LM half is the blank-LM value (or empty) to the bare NT
|
|
44
|
+
hash. A real (non-blank) LM half, a plain NT hash, and a non-hash token (e.g. a wordlist
|
|
45
|
+
path) are returned unchanged -- only an exact blank-LM prefix is stripped."""
|
|
46
|
+
lm, sep, nt = h.partition(":")
|
|
47
|
+
if sep and nt and (lm.lower() == _BLANK_LM or lm == ""):
|
|
48
|
+
return nt
|
|
49
|
+
return h
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_auth_flags(
|
|
53
|
+
username: "str | list[str] | None" = None,
|
|
54
|
+
password: "str | list[str] | None" = None,
|
|
55
|
+
ntlm_hash: "str | list[str] | None" = None,
|
|
56
|
+
domain: str | None = None,
|
|
57
|
+
local_auth: bool = False,
|
|
58
|
+
kerberos: bool = False,
|
|
59
|
+
use_kcache: bool = False,
|
|
60
|
+
cred_id: int | None = None,
|
|
61
|
+
laps: str | None = None,
|
|
62
|
+
kdc_host: str | None = None,
|
|
63
|
+
aes_key: str | None = None,
|
|
64
|
+
ccache: str | None = None,
|
|
65
|
+
pfx_cert: str | None = None,
|
|
66
|
+
pfx_base64: str | None = None,
|
|
67
|
+
pfx_pass: str | None = None,
|
|
68
|
+
pem_cert: str | None = None,
|
|
69
|
+
pem_key: str | None = None,
|
|
70
|
+
) -> list[str]:
|
|
71
|
+
"""Translate credential parameters into an nxc auth flag list.
|
|
72
|
+
|
|
73
|
+
username/password/ntlm_hash accept either a single value or a list (for
|
|
74
|
+
password spraying); each list entry may be a literal value or a path to a
|
|
75
|
+
wordlist file that nxc reads. `laps` enables LAPS auth (`--laps <account>`):
|
|
76
|
+
nxc reads the target's LAPS-managed local-admin password from AD using the
|
|
77
|
+
supplied creds, then authenticates with it -- pass the account name (use
|
|
78
|
+
"administrator" for the nxc default). Raises AuthError on contradictory
|
|
79
|
+
combinations so a bad call is rejected before any command is built.
|
|
80
|
+
"""
|
|
81
|
+
usernames = _as_list(username)
|
|
82
|
+
passwords = _as_list(password)
|
|
83
|
+
hashes = _as_list(ntlm_hash)
|
|
84
|
+
cert_auth = bool(pfx_cert or pfx_base64 or pem_cert)
|
|
85
|
+
|
|
86
|
+
if passwords and hashes:
|
|
87
|
+
raise AuthError("provide either password(s) or NTLM hash(es), not both.")
|
|
88
|
+
if local_auth and domain:
|
|
89
|
+
raise AuthError("--local-auth authenticates locally; do not also set a domain.")
|
|
90
|
+
if cred_id is not None and (usernames or passwords or hashes or domain or local_auth):
|
|
91
|
+
raise AuthError(
|
|
92
|
+
"cred_id uses a credential stored in nxc's DB; do not also pass "
|
|
93
|
+
"username/password/hash/domain/local_auth."
|
|
94
|
+
)
|
|
95
|
+
if cert_auth and not usernames:
|
|
96
|
+
raise AuthError("certificate authentication requires a username.")
|
|
97
|
+
if bool(pem_cert) != bool(pem_key):
|
|
98
|
+
raise AuthError("PEM certificate auth needs both pem_cert and pem_key.")
|
|
99
|
+
|
|
100
|
+
flags: list[str] = []
|
|
101
|
+
|
|
102
|
+
if cred_id is not None:
|
|
103
|
+
flags += ["-id", str(cred_id)]
|
|
104
|
+
else:
|
|
105
|
+
if domain:
|
|
106
|
+
flags += ["-d", domain]
|
|
107
|
+
for user in usernames:
|
|
108
|
+
flags += ["-u", user]
|
|
109
|
+
for pw in passwords:
|
|
110
|
+
flags += ["-p", pw]
|
|
111
|
+
if usernames and not passwords and not hashes and not (kerberos or use_kcache or aes_key or ccache or cert_auth):
|
|
112
|
+
# Username(s) with no secret = empty-password (guest/null) bind. nxc
|
|
113
|
+
# needs an explicit `-p ''`; emitting it here makes guest auth reliable
|
|
114
|
+
# without an agent having to serialise an empty string (which MCP
|
|
115
|
+
# clients routinely mangle). `-u guest` -> `-u guest -p ''`.
|
|
116
|
+
flags += ["-p", ""]
|
|
117
|
+
for h in hashes:
|
|
118
|
+
flags += ["-H", _nt_only(h)]
|
|
119
|
+
if local_auth:
|
|
120
|
+
flags.append("--local-auth")
|
|
121
|
+
|
|
122
|
+
# Certificate authentication (PKINIT / ADCS) -- a distinct method (requires a
|
|
123
|
+
# username, checked above). Provide a pfx (file or base64, optional --pfx-pass)
|
|
124
|
+
# OR a PEM cert+key pair.
|
|
125
|
+
if pfx_cert:
|
|
126
|
+
flags += ["--pfx-cert", pfx_cert]
|
|
127
|
+
if pfx_base64:
|
|
128
|
+
flags += ["--pfx-base64", pfx_base64]
|
|
129
|
+
if pfx_pass:
|
|
130
|
+
flags += ["--pfx-pass", pfx_pass]
|
|
131
|
+
if pem_cert:
|
|
132
|
+
flags += ["--pem-cert", pem_cert]
|
|
133
|
+
if pem_key:
|
|
134
|
+
flags += ["--pem-key", pem_key]
|
|
135
|
+
|
|
136
|
+
if laps:
|
|
137
|
+
flags += ["--laps", laps]
|
|
138
|
+
|
|
139
|
+
# Kerberos options. `--aesKey` is itself a Kerberos secret (overpass-the-key;
|
|
140
|
+
# pairs with the aes256/aes128 keys recovered by smb_lsa/smb_ntds) -- nxc treats
|
|
141
|
+
# its presence as enabling Kerberos, so no separate -k is needed. `--kdcHost`
|
|
142
|
+
# names the KDC and is often required for `-k` to resolve the DC.
|
|
143
|
+
if aes_key:
|
|
144
|
+
flags += ["--aesKey", aes_key]
|
|
145
|
+
if kdc_host:
|
|
146
|
+
flags += ["--kdcHost", kdc_host]
|
|
147
|
+
|
|
148
|
+
# Kerberos can layer on top of either path (e.g. -k with a username, or
|
|
149
|
+
# --use-kcache with a stored ticket). A `ccache` path implies --use-kcache
|
|
150
|
+
# (the executor points KRB5CCNAME at it; nxc has no ccache flag).
|
|
151
|
+
if use_kcache or ccache:
|
|
152
|
+
flags.append("--use-kcache")
|
|
153
|
+
elif kerberos:
|
|
154
|
+
flags.append("-k")
|
|
155
|
+
|
|
156
|
+
return flags
|
netexec_mcp/config.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Environment resolution and NetExec base-command detection.
|
|
2
|
+
|
|
3
|
+
The base command is how this server invokes nxc. Resolution order (per PLAN.md):
|
|
4
|
+
1. NXC_COMMAND env (shlex-parsed) if set -- covers uv/poetry/pipx/docker installs.
|
|
5
|
+
2. else auto-detect `nxc` / `netexec` on PATH.
|
|
6
|
+
3. else fail fast with setup help.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import configparser
|
|
12
|
+
import os
|
|
13
|
+
import shlex
|
|
14
|
+
import shutil
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# All protocol-modules nxc supports that we expose as tool groups. Enabled subset
|
|
19
|
+
# is controlled by NXC_PROTOCOLS; default is all of them.
|
|
20
|
+
DEFAULT_PROTOCOLS = [
|
|
21
|
+
"smb", "ldap", "winrm", "ssh", "mssql", "wmi", "rdp", "ftp", "nfs", "vnc",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# The single operating-mode control (NXC_MODE), with four escalating levels:
|
|
25
|
+
# suggest -- return the resolved command, never execute (the auditor runs it).
|
|
26
|
+
# Scope is still enforced; offensive commands *can* be previewed.
|
|
27
|
+
# recon -- execute read-only enumeration; everything beyond it is blocked. (default)
|
|
28
|
+
# loot -- also permit read-only *credential-dumping* (sam/lsa/ntds/gpp/roasting):
|
|
29
|
+
# no state change on the target, but it harvests credential material.
|
|
30
|
+
# full -- execute everything, including state-changing / privilege-escalation
|
|
31
|
+
# actions (exec, write, spray, coercion-with-listener, exploits).
|
|
32
|
+
# The recon<loot<full split mirrors nxc's own module taxonomy (ENUMERATION <
|
|
33
|
+
# CREDENTIAL_DUMPING < PRIVILEGE_ESCALATION).
|
|
34
|
+
VALID_MODES = ("suggest", "recon", "loot", "full")
|
|
35
|
+
|
|
36
|
+
# How the tool surface is presented (NXC_TOOL_MODE), independent of the operating
|
|
37
|
+
# mode above:
|
|
38
|
+
# dynamic -- (DEFAULT) expose only a few meta-tools (catalog/find/describe/call);
|
|
39
|
+
# the real tools stay registered + callable but are hidden from tools/list.
|
|
40
|
+
# Keeps the ~46k-token tool surface out of the model's context window --
|
|
41
|
+
# what makes the server usable on small-context models (and ~8x cheaper on
|
|
42
|
+
# big ones, same quality). See dynamic.py. Benchmark: dynamic is net-positive
|
|
43
|
+
# or neutral for 4/5 models tested and the ONLY viable mode for <=8B/32k.
|
|
44
|
+
# static -- register every enabled tool as a first-class MCP tool (opt-out). Best for
|
|
45
|
+
# a decisive one-shot model, or to avoid discovery round-trips on a
|
|
46
|
+
# big-context model. On a small context the ~46k surface overflows -> a
|
|
47
|
+
# startup warning is emitted.
|
|
48
|
+
VALID_TOOL_MODES = ("static", "dynamic")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ConfigError(RuntimeError):
|
|
52
|
+
"""Raised when the server cannot determine a valid configuration."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def nxc_home() -> Path:
|
|
56
|
+
"""nxc's home dir, mirroring ``nxc/paths.py``: ``$NXC_PATH`` if set, else
|
|
57
|
+
``~/.nxc``. Identical on Linux/macOS/Windows -- nxc uses ``expanduser`` (no
|
|
58
|
+
``%APPDATA%``/appdirs), so ``~`` resolves to ``C:\\Users\\<user>`` on Windows.
|
|
59
|
+
Honoring nxc's own env var keeps our reads aligned with wherever nxc writes.
|
|
60
|
+
"""
|
|
61
|
+
override = os.environ.get("NXC_PATH")
|
|
62
|
+
if override and override.strip():
|
|
63
|
+
return Path(os.path.expanduser(override.strip()))
|
|
64
|
+
return Path(os.path.expanduser("~/.nxc"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def nxc_config_path() -> Path:
|
|
68
|
+
"""Path to ``nxc.conf`` under the resolved nxc home."""
|
|
69
|
+
return nxc_home() / "nxc.conf"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def nxc_workspace_dir() -> Path:
|
|
73
|
+
"""The ``workspaces/`` dir (per-protocol ``.db`` files) under the nxc home."""
|
|
74
|
+
return nxc_home() / "workspaces"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# nxc's own fallbacks when nxc.conf is absent or a key is unset.
|
|
78
|
+
_NXC_CONF_DEFAULTS = {"pwn3d_label": "Pwn3d!", "workspace": "default"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_nxc_conf() -> dict:
|
|
82
|
+
"""Read the operator-configurable ``[nxc]`` ``pwn3d_label`` and ``workspace``.
|
|
83
|
+
|
|
84
|
+
Both live in ``nxc.conf`` and can be changed by the operator, so we read them
|
|
85
|
+
from that single source of truth instead of hardcoding. Falls back to nxc's
|
|
86
|
+
own defaults when the file is missing/unreadable. ``pwn3d_label`` is preserved
|
|
87
|
+
verbatim when the key exists (even if blank); a blank ``workspace`` collapses
|
|
88
|
+
to ``default``.
|
|
89
|
+
"""
|
|
90
|
+
parser = configparser.ConfigParser(interpolation=None) # values may contain %
|
|
91
|
+
try:
|
|
92
|
+
with open(nxc_config_path(), encoding="utf-8") as fh:
|
|
93
|
+
parser.read_file(fh)
|
|
94
|
+
except (OSError, configparser.Error):
|
|
95
|
+
return dict(_NXC_CONF_DEFAULTS)
|
|
96
|
+
if not parser.has_section("nxc"):
|
|
97
|
+
return dict(_NXC_CONF_DEFAULTS)
|
|
98
|
+
section = parser["nxc"]
|
|
99
|
+
label = section.get("pwn3d_label", _NXC_CONF_DEFAULTS["pwn3d_label"])
|
|
100
|
+
workspace = (section.get("workspace") or "").strip() or _NXC_CONF_DEFAULTS["workspace"]
|
|
101
|
+
return {"pwn3d_label": label, "workspace": workspace}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _env_list(name: str) -> list[str]:
|
|
105
|
+
"""Parse a comma/newline-separated env var into a clean list of tokens."""
|
|
106
|
+
raw = os.environ.get(name, "")
|
|
107
|
+
return [p.strip() for p in raw.replace("\n", ",").split(",") if p.strip()]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_base_command() -> list[str]:
|
|
111
|
+
"""Resolve the nxc base command as an argv list (never via a shell).
|
|
112
|
+
|
|
113
|
+
Tokens are user-expanded so values like
|
|
114
|
+
`uv run --directory ~/NetExec netexec` work as written.
|
|
115
|
+
"""
|
|
116
|
+
explicit = os.environ.get("NXC_COMMAND")
|
|
117
|
+
if explicit and explicit.strip():
|
|
118
|
+
argv = [os.path.expanduser(tok) for tok in shlex.split(explicit)]
|
|
119
|
+
if not argv:
|
|
120
|
+
raise ConfigError("NXC_COMMAND is set but empty after parsing.")
|
|
121
|
+
return argv
|
|
122
|
+
|
|
123
|
+
for candidate in ("nxc", "netexec"):
|
|
124
|
+
found = shutil.which(candidate)
|
|
125
|
+
if found:
|
|
126
|
+
return [found]
|
|
127
|
+
|
|
128
|
+
raise ConfigError(
|
|
129
|
+
"Could not find NetExec. Set NXC_COMMAND (e.g. "
|
|
130
|
+
"\"uv run --directory ~/NetExec netexec\") or put 'nxc'/'netexec' on PATH."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _env_int(name: str, default: int) -> int:
|
|
135
|
+
raw = os.environ.get(name)
|
|
136
|
+
if raw is None or not raw.strip():
|
|
137
|
+
return default
|
|
138
|
+
try:
|
|
139
|
+
value = int(raw)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
raise ConfigError(f"{name} must be an integer.") from exc
|
|
142
|
+
if value <= 0:
|
|
143
|
+
raise ConfigError(f"{name} must be a positive integer.")
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _resolve_mode() -> tuple[str, bool, bool]:
|
|
148
|
+
"""Resolve the operating mode -> (mode, dry_run, allow_offensive).
|
|
149
|
+
|
|
150
|
+
`NXC_MODE` (suggest|recon|full) is the canonical control; the `dry_run` and
|
|
151
|
+
`allow_offensive` booleans are derived from it. Defaults to `recon` when unset.
|
|
152
|
+
"""
|
|
153
|
+
raw = os.environ.get("NXC_MODE")
|
|
154
|
+
mode = raw.strip().lower() if raw and raw.strip() else "recon"
|
|
155
|
+
if mode not in VALID_MODES:
|
|
156
|
+
raise ConfigError(
|
|
157
|
+
f"NXC_MODE must be one of {', '.join(VALID_MODES)}; got {raw!r}."
|
|
158
|
+
)
|
|
159
|
+
return mode, mode == "suggest", mode == "full"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_tool_mode() -> str:
|
|
163
|
+
"""Resolve NXC_TOOL_MODE (static|dynamic); defaults to dynamic (progressive disclosure
|
|
164
|
+
-- usable on small-context models, cheaper on large ones; set static to opt out)."""
|
|
165
|
+
raw = (os.environ.get("NXC_TOOL_MODE") or "dynamic").strip().lower()
|
|
166
|
+
if raw not in VALID_TOOL_MODES:
|
|
167
|
+
raise ConfigError(
|
|
168
|
+
f"NXC_TOOL_MODE must be one of {', '.join(VALID_TOOL_MODES)}; got {raw!r}."
|
|
169
|
+
)
|
|
170
|
+
return raw
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class Config:
|
|
175
|
+
base_command: list[str]
|
|
176
|
+
protocols: list[str]
|
|
177
|
+
scope: list[str]
|
|
178
|
+
allow_offensive: bool
|
|
179
|
+
dry_run: bool
|
|
180
|
+
timeout: int
|
|
181
|
+
max_targets: int
|
|
182
|
+
audit_log: str | None
|
|
183
|
+
workspace: str | None
|
|
184
|
+
mode: str = "recon"
|
|
185
|
+
# loot mode permits credential-dumping (offensive but read-only) on top of recon;
|
|
186
|
+
# full implies loot. Derived from `mode` in from_env(); see check_offensive().
|
|
187
|
+
allow_loot: bool = False
|
|
188
|
+
# Tool-surface presentation (NXC_TOOL_MODE), orthogonal to `mode`. Default "dynamic"
|
|
189
|
+
# hides the full tool surface behind meta-tools so small-context models can use the
|
|
190
|
+
# server (see dynamic.py); "static" opts out to listing every tool.
|
|
191
|
+
tool_mode: str = "dynamic"
|
|
192
|
+
# From nxc.conf's `[nxc] pwn3d_label` (operator-configurable). Surfaced to the
|
|
193
|
+
# agent via the auth guide; the MCP itself keys auth on the `[+]` marker, which
|
|
194
|
+
# nxc does not make configurable.
|
|
195
|
+
pwn3d_label: str = "Pwn3d!"
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_env(cls) -> "Config":
|
|
199
|
+
mode, dry_run, allow_offensive = _resolve_mode()
|
|
200
|
+
conf = read_nxc_conf()
|
|
201
|
+
return cls(
|
|
202
|
+
base_command=resolve_base_command(),
|
|
203
|
+
protocols=_env_list("NXC_PROTOCOLS") or list(DEFAULT_PROTOCOLS),
|
|
204
|
+
scope=_env_list("NXC_SCOPE"),
|
|
205
|
+
allow_offensive=allow_offensive,
|
|
206
|
+
# loot and full both permit credential-dumping; full additionally permits
|
|
207
|
+
# state-changing/offensive actions (allow_offensive).
|
|
208
|
+
allow_loot=mode in ("loot", "full"),
|
|
209
|
+
dry_run=dry_run,
|
|
210
|
+
timeout=_env_int("NXC_TIMEOUT", 300),
|
|
211
|
+
max_targets=_env_int("NXC_MAX_TARGETS", 256),
|
|
212
|
+
audit_log=os.environ.get("NXC_AUDIT_LOG"),
|
|
213
|
+
# NXC_WORKSPACE wins; else default to whatever nxc.conf writes to, so
|
|
214
|
+
# our reads track nxc's writes without extra configuration.
|
|
215
|
+
workspace=os.environ.get("NXC_WORKSPACE") or conf["workspace"],
|
|
216
|
+
mode=mode,
|
|
217
|
+
tool_mode=_resolve_tool_mode(),
|
|
218
|
+
pwn3d_label=conf["pwn3d_label"],
|
|
219
|
+
)
|