diff --git a/injector_common/injector_common/data_helpers.py b/injector_common/injector_common/data_helpers.py
new file mode 100644
index 00000000..b78a815f
--- /dev/null
+++ b/injector_common/injector_common/data_helpers.py
@@ -0,0 +1,25 @@
+from typing import Dict
+
+
+class DataHelpers:
+
+ @staticmethod
+ def get_injector_contract_id(data: Dict) -> str:
+ try:
+ return data["injection"]["inject_injector_contract"]["injector_contract_id"]
+ except KeyError as e:
+ raise ValueError("Invalid data: missing injector contract id") from e
+
+ @staticmethod
+ def get_content(data: Dict) -> Dict:
+ try:
+ return data["injection"]["inject_content"]
+ except KeyError as e:
+ raise ValueError("Invalid data: missing inject content") from e
+
+ @staticmethod
+ def get_inject_id(data: Dict) -> str:
+ try:
+ return data["injection"]["inject_id"]
+ except KeyError as e:
+ raise ValueError("Invalid data: missing inject id") from e
diff --git a/injector_common/pyproject.toml b/injector_common/pyproject.toml
index 6bae4a70..70332438 100644
--- a/injector_common/pyproject.toml
+++ b/injector_common/pyproject.toml
@@ -3,7 +3,7 @@ name="injector_common"
version="1.0.0"
dependencies = [
- "pyoaev==2.1.0",
+ "pyoaev==2.1.3",
]
[project.optional-dependencies]
dev = [
diff --git a/netexec/.dockerignore b/netexec/.dockerignore
new file mode 100644
index 00000000..8f7bc48f
--- /dev/null
+++ b/netexec/.dockerignore
@@ -0,0 +1,3 @@
+config.yml
+src/__pycache__
+__pycache__
diff --git a/netexec/.gitignore b/netexec/.gitignore
new file mode 100644
index 00000000..b93baaf2
--- /dev/null
+++ b/netexec/.gitignore
@@ -0,0 +1,2 @@
+config.yml
+__pycache__
\ No newline at end of file
diff --git a/netexec/Dockerfile b/netexec/Dockerfile
new file mode 100644
index 00000000..9fb3dc13
--- /dev/null
+++ b/netexec/Dockerfile
@@ -0,0 +1,61 @@
+# Builder
+FROM python:3.13-alpine AS builder
+
+# System dependencies required by netexec
+RUN apk add --no-cache \
+ gcc \
+ musl-dev \
+ libffi-dev \
+ openssl-dev \
+ cargo
+
+# Install Poetry
+RUN pip install --no-cache-dir poetry==2.1.3
+
+# Copy shared injector code
+WORKDIR /opt/injector_common
+COPY --from=injector_common ./ ./
+
+# Copy injector source
+ARG installdir=/opt/injector
+ADD . ${installdir}
+WORKDIR ${installdir}
+
+RUN poetry build
+
+# Runner
+FROM python:3.13-alpine AS runner
+
+# Runtime deps only
+RUN apk add --no-cache \
+ gcc \
+ musl-dev \
+ python3-dev \
+ libffi-dev \
+ openssl-dev \
+ cargo \
+ git
+
+WORKDIR /opt/injector_common
+COPY --from=injector_common ./ ./
+
+ARG installdir=/opt/injector
+WORKDIR ${installdir}
+COPY --from=builder ${installdir}/dist ./dist
+
+RUN pip3 install --no-cache-dir "$(ls dist/*.whl)[prod]"
+
+# Optional: override client-python version
+ARG PYOAEV_GIT_BRANCH_OVERRIDE
+RUN if [ -n "${PYOAEV_GIT_BRANCH_OVERRIDE}" ]; then \
+ echo "Forcing specific version of client-python" && \
+ pip install pip3-autoremove && \
+ pip-autoremove pyoaev -y && \
+ pip install git+https://github.com/OpenAEV-Platform/client-python@${PYOAEV_GIT_BRANCH_OVERRIDE} ; \
+ fi
+
+# Install netexec
+RUN pip install --no-cache-dir git+https://github.com/Pennyw0rth/NetExec.git
+
+# Default command
+CMD ["python3", "-m", "netexec.openaev_netexec"]
diff --git a/netexec/README.md b/netexec/README.md
new file mode 100644
index 00000000..a06602da
--- /dev/null
+++ b/netexec/README.md
@@ -0,0 +1,222 @@
+# OpenAEV NetExec Injector
+
+## Table of Contents
+
+- [OpenAEV NetExec Injector](#openaev-netexec-injector)
+ - [Prerequisites](#prerequisites)
+ - [Configuration variables](#configuration-variables)
+ - [OpenAEV environment variables](#openaev-environment-variables)
+ - [Base injector environment variables](#base-injector-environment-variables)
+ - [Deployment](#deployment)
+ - [Docker Deployment](#docker-deployment)
+ - [Manual Deployment](#manual-deployment)
+ - [Behavior](#behavior)
+ - [Template Selection](#template-selection)
+ - [Target Selection](#target-selection)
+ - [Resources](#resources)
+
+---
+
+## Prerequisites
+
+This injector uses [NetExex](https://www.netexec.wiki/smb-protocol/enumeration).
+
+This injector communicates with the OpenAEV platform through **RabbitMQ**, using the configuration provided by OpenAEV.
+
+To function properly, the injector **must be able to reach the RabbitMQ service** (hostname and port) defined in the
+OpenAEV configuration.
+
+## Configuration
+
+Configuration values can be provided either:
+
+* via `docker-compose.yml` (Docker deployment), or
+* via `config.yml` (manual deployment).
+
+### OpenAEV Environment Variables
+
+The following parameters are required to connect the injector to the OpenAEV platform:
+
+| Parameter | `config.yml` | Docker Variable | Mandatory | Description |
+|---------------|--------------|-----------------|-----------|-----------------------------------------------------|
+| OpenAEV URL | `url` | `OPENAEV_URL` | Yes | Base URL of the OpenAEV platform. |
+| OpenAEV Token | `token` | `OPENAEV_TOKEN` | Yes | Admin API token configured in the OpenAEV platform. |
+
+### Injector Environment Variables
+
+The following parameters control the injector runtime behavior:
+
+| Parameter | `config.yml` | Docker Variable | Default | Mandatory | Description |
+|---------------|--------------|----------------------|---------|-----------|---------------------------------------------------------|
+| Injector ID | `id` | `INJECTOR_ID` | — | Yes | Unique `UUIDv4` identifying this injector instance. |
+| Injector Name | `name` | `INJECTOR_NAME` | — | Yes | Human-readable name of the injector. |
+| Log Level | `log_level` | `INJECTOR_LOG_LEVEL` | `info` | Yes | Logging verbosity: `debug`, `info`, `warn`, or `error`. |
+
+## Deployment
+
+### Docker Deployment
+
+Build the Docker image using the provided `Dockerfile`.
+
+```shell
+docker build --build-context injector_common=../injector_common . -t openaev/injector-netexec:latest
+```
+
+Then configure the environment variables in `docker-compose.yml` and start the injector:
+
+```shell
+docker compose up -d
+```
+
+> ✅ The Docker image **already contains NetExec**. No further installation is needed inside the container.
+
+### Manual Deployment
+
+1. Create a `config.yml` file based on `config.yml.sample`
+2. Adjust the configuration values to match your environment
+
+#### Prerequisites
+
+* **NetExec must be installed locally** and accessible via the command line (`netexec` command).
+* You can install it
+ from: [https://www.netexec.wiki/getting-started/installation](https://www.netexec.wiki/getting-started/installation)
+
+* Python package manager **Poetry** (version 2.1 or later)
+ 👉 [https://python-poetry.org/](https://python-poetry.org/)
+
+#### Installation
+
+**Production environment**
+
+```shell
+poetry install --extras prod
+```
+
+**Development environment**
+
+For development, you should also clone the `pyoaev` repository following the instructions provided in the OpenAEV
+documentation.
+
+```shell
+poetry install --extras dev
+```
+
+## Development
+
+This project follows strict code formatting rules to ensure consistency and readability across the OpenAEV ecosystem.
+
+Before submitting any **Pull Request**, contributors **must** format the codebase using **isort** and **black**.
+
+### Code Formatting
+
+The following tools are required (already included in the development dependencies):
+
+* **isort** – import sorting
+* **black** – code formatter
+
+Run them from the project root:
+
+```shell
+poetry run isort --profile black .
+poetry run black .
+```
+
+Both commands must complete **without errors or changes** before opening a PR.
+
+> ⚠️ Pull Requests that do not respect formatting rules may be rejected or require additional review cycles.
+
+#### Run the Injector
+
+```shell
+poetry run python -m netexec.openaev_netexec
+```
+
+#### Test it
+
+SMB de test
+
+```
+podman run -d --name smb-test --network openaev-dev_default -p 445:445 dperson/samba -u "testuser;testpass" -s "share;/share;yes;no;no;testuser"
+```
+
+Run command to test
+
+```
+netexec smb smb-test -u testuser -p testpass --shares
+```
+
+## Behavior
+
+The NetExec injector performs **contract-based network reconnaissance and authentication checks** by dynamically
+building and executing **NetExec commands** based on the contract configuration and user inputs.
+
+Each execution translates contract fields (targets, credentials, and options) into a NetExec command and runs it against
+the selected targets.
+
+NetExec is used as a **client-only tool**: it connects to remote services (such as SMB) and never exposes or hosts
+services itself.
+
+## Supported Contracts
+
+The injector currently supports **SMB-based contracts**, focused on authentication checks and safe enumeration actions.
+
+### SMB Authentication Contract
+
+This contract allows validating credentials and performing controlled SMB enumeration using NetExec.
+
+Supported SMB actions include:
+
+* Share enumeration
+* User enumeration
+* Group enumeration
+* Session enumeration
+* Logged-on user discovery
+
+Only **non-destructive, read-only actions** are exposed.
+
+## Target Selection
+
+Targets are resolved using the `target_selector` field defined in the contract.
+
+### When the target type is **Assets**
+
+| Selected Property | Asset Field Used |
+|-------------------|-------------------------------|
+| Seen IP | `endpoint_seen_ip` |
+| Local IP | First entry in `endpoint_ips` |
+| Hostname | `endpoint_hostname` |
+
+### When the target type is **Manual**
+
+Targets are provided directly as **comma-separated IP addresses or hostnames**.
+
+## Options
+
+The injector supports the following SMB options, mapped directly to NetExec arguments:
+
+* `--shares` — Enumerate SMB shares
+* `--users` — Enumerate users
+* `--groups` — Enumerate groups
+* `--sessions` — List active SMB sessions
+* `--loggedon-users` — List currently logged-on users
+
+If no option is explicitly selected, the injector defaults to a **safe enumeration action** (typically `--shares`) to
+ensure meaningful output.
+
+## Example Executions
+
+Basic SMB share enumeration:
+
+```bash
+netexec smb 192.168.1.50 --shares
+```
+
+SMB authentication test with credentials:
+
+```bash
+netexec smb 192.168.1.50 -u admin -p Password123
+```
+## Resources
+
+* [NetExec GitHub Repository](https://github.com/Pennyw0rth/NetExec)
+* [NetExec Documentation](https://github.com/Pennyw0rth/NetExec/wiki)
\ No newline at end of file
diff --git a/netexec/config.yml.sample b/netexec/config.yml.sample
new file mode 100644
index 00000000..284c08e6
--- /dev/null
+++ b/netexec/config.yml.sample
@@ -0,0 +1,9 @@
+openaev:
+ url: 'http://localhost:3001'
+ token: 'ChangeMe'
+
+injector:
+ id: 'changeme'
+ name: 'NetExec'
+ log_level: 'info'
+
diff --git a/netexec/docker-compose.yml b/netexec/docker-compose.yml
new file mode 100644
index 00000000..112558cc
--- /dev/null
+++ b/netexec/docker-compose.yml
@@ -0,0 +1,15 @@
+services:
+ injector-netexec:
+ image: openaev/openaev-netexec:${INJECTOR_VERSION}
+ environment:
+ - OPENAEV_URL=${OPENAEV_URL}
+ - OPENAEV_TOKEN=${OPENAEV_TOKEN}
+ - INJECTOR_ID=${INJECTOR_ID}
+ - INJECTOR_NAME=NetExec
+ - INJECTOR_LOG_LEVEL=debug
+ restart: always
+ networks:
+ - openaev-dev_default
+networks:
+ openaev-dev_default:
+ external: true
\ No newline at end of file
diff --git a/netexec/netexec/__init__.py b/netexec/netexec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/netexec/netexec/configuration/__init__.py b/netexec/netexec/configuration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/netexec/netexec/configuration/config_loader.py b/netexec/netexec/configuration/config_loader.py
new file mode 100644
index 00000000..33b94b11
--- /dev/null
+++ b/netexec/netexec/configuration/config_loader.py
@@ -0,0 +1,32 @@
+from pydantic import Field
+from pyoaev.configuration import ConfigLoaderOAEV, Configuration, SettingsLoader
+
+from netexec.configuration.injector_config_override import InjectorConfigOverride
+from netexec.contracts_netexec import NetExecContracts
+
+
+class ConfigLoader(SettingsLoader):
+ openaev: ConfigLoaderOAEV = Field(
+ default_factory=ConfigLoaderOAEV, description="OpenAEV platform configuration"
+ )
+ injector: InjectorConfigOverride = Field(
+ default_factory=InjectorConfigOverride,
+ description="NetExec injector configuration",
+ )
+
+ def to_daemon_config(self) -> Configuration:
+ return Configuration(
+ config_hints={
+ # OpenAEV configuration (flattened)
+ "openaev_url": {"data": str(self.openaev.url)},
+ "openaev_token": {"data": self.openaev.token},
+ # Injector configuration (flattened)
+ "injector_id": {"data": self.injector.id},
+ "injector_name": {"data": self.injector.name},
+ "injector_type": {"data": "openaev_netexec_injector"},
+ "injector_contracts": {"data": NetExecContracts.build()},
+ "injector_log_level": {"data": self.injector.log_level},
+ "injector_icon_filepath": {"data": self.injector.icon_filepath},
+ },
+ config_base_model=self,
+ )
diff --git a/netexec/netexec/configuration/injector_config_override.py b/netexec/netexec/configuration/injector_config_override.py
new file mode 100644
index 00000000..aeddd229
--- /dev/null
+++ b/netexec/netexec/configuration/injector_config_override.py
@@ -0,0 +1,23 @@
+from typing import Literal
+
+from pydantic import Field
+from pyoaev.configuration import ConfigLoaderCollector
+
+
+# To be change ConfigLoaderCollector
+class InjectorConfigOverride(ConfigLoaderCollector):
+ id: str = Field(
+ description="A unique UUIDv4 identifier for this injector instance.",
+ )
+ name: str = Field(
+ description="Name of the injector.",
+ default="NetExec",
+ )
+ icon_filepath: str = Field(
+ description="Path to the icon file",
+ default="netexec/img/icon-netexec.png",
+ )
+ log_level: Literal["debug", "info", "warn", "error"] = Field(
+ description="Determines the verbosity of the logs. Options: debug, info, warn, or error.",
+ default="info",
+ )
diff --git a/netexec/netexec/contracts_netexec.py b/netexec/netexec/contracts_netexec.py
new file mode 100644
index 00000000..07a89b6f
--- /dev/null
+++ b/netexec/netexec/contracts_netexec.py
@@ -0,0 +1,173 @@
+from typing import List
+
+from pyoaev.contracts import ContractBuilder
+from pyoaev.contracts.contract_config import (
+ Contract,
+ ContractAsset,
+ ContractAssetGroup,
+ ContractConfig,
+ ContractElement,
+ ContractExpectations,
+ ContractSelect,
+ ContractText,
+ Expectation,
+ ExpectationType,
+ SupportedLanguage,
+ prepare_contracts,
+)
+from pyoaev.contracts.contract_utils import ContractCardinality
+from pyoaev.security_domain.types import SecurityDomains
+
+from injector_common.constants import (
+ TARGET_PROPERTY_SELECTOR_KEY,
+ TARGET_SELECTOR_KEY,
+ TARGETS_KEY,
+)
+from injector_common.targets import TargetProperty, target_property_choices_dict
+
+CONTRACT_TYPE = "openaev_netexec"
+CONTRACT_ID_SMB_AUTH = "b6b9b5c4-1a0e-4b3c-9a8a-netexec-smb-auth"
+
+
+class NetExecContracts:
+
+ @staticmethod
+ def core_contract_fields():
+ # -- FIELDS --
+ target_selector = ContractSelect(
+ key=TARGET_SELECTOR_KEY,
+ label="Type of targets",
+ defaultValue=["asset-groups"],
+ mandatory=True,
+ choices={
+ "assets": "Assets",
+ "manual": "Manual",
+ "asset-groups": "Asset groups",
+ },
+ )
+ targets_assets = ContractAsset(
+ cardinality=ContractCardinality.Multiple,
+ label="Targeted assets",
+ mandatory=False,
+ mandatoryConditionFields=[target_selector.key],
+ mandatoryConditionValues={target_selector.key: "assets"},
+ visibleConditionFields=[target_selector.key],
+ visibleConditionValues={target_selector.key: "assets"},
+ )
+ target_asset_groups = ContractAssetGroup(
+ cardinality=ContractCardinality.Multiple,
+ label="Targeted asset groups",
+ mandatory=False,
+ mandatoryConditionFields=[target_selector.key],
+ mandatoryConditionValues={target_selector.key: "asset-groups"},
+ visibleConditionFields=[target_selector.key],
+ visibleConditionValues={target_selector.key: "asset-groups"},
+ )
+ target_property_selector = ContractSelect(
+ key=TARGET_PROPERTY_SELECTOR_KEY,
+ label="Targeted assets property",
+ defaultValue=[TargetProperty.AUTOMATIC.name.lower()],
+ mandatory=False,
+ choices=target_property_choices_dict,
+ mandatoryConditionFields=[target_selector.key],
+ mandatoryConditionValues={target_selector.key: ["assets", "asset-groups"]},
+ visibleConditionFields=[target_selector.key],
+ visibleConditionValues={target_selector.key: ["assets", "asset-groups"]},
+ )
+ targets_manual = ContractText(
+ key=TARGETS_KEY,
+ label="Manual targets (comma-separated)",
+ mandatory=False,
+ mandatoryConditionFields=[target_selector.key],
+ mandatoryConditionValues={target_selector.key: "manual"},
+ visibleConditionFields=[target_selector.key],
+ visibleConditionValues={target_selector.key: "manual"},
+ )
+ expectations = ContractExpectations(
+ key="expectations",
+ label="Expectations",
+ mandatory=False,
+ cardinality=ContractCardinality.Multiple,
+ predefinedExpectations=[
+ Expectation(
+ expectation_type=ExpectationType.vulnerability,
+ expectation_name="Not vulnerable",
+ expectation_description="",
+ expectation_score=100,
+ expectation_expectation_group=False,
+ )
+ ],
+ )
+
+ return [
+ target_selector,
+ targets_assets,
+ target_asset_groups,
+ target_property_selector,
+ targets_manual,
+ expectations,
+ ]
+
+ @staticmethod
+ def build() -> List[Contract]:
+ # Contract configuration
+ contract_config = ContractConfig(
+ type=CONTRACT_TYPE,
+ label={
+ SupportedLanguage.en: "NetExec",
+ SupportedLanguage.fr: "NetExec",
+ },
+ color_dark="#d32f2f",
+ color_light="#ffcdd2",
+ expose=True,
+ )
+
+ # Contract fields
+ smb_auth_fields: List[ContractElement] = (
+ ContractBuilder()
+ .optional(
+ ContractText(
+ key="username",
+ label="Username",
+ )
+ )
+ .optional(
+ ContractText(
+ key="password",
+ label="Password",
+ )
+ )
+ .optional(
+ ContractSelect(
+ key="options",
+ label="Options",
+ choices={
+ "--shares": "--shares",
+ "--pass-pol": "--pass-pol",
+ "--users": "--users",
+ "--groups": "--groups",
+ "--sessions": "--sessions",
+ "--loggedon-users": "--loggedon-users",
+ },
+ ),
+ )
+ .add_fields(NetExecContracts.core_contract_fields())
+ .build_fields()
+ )
+
+ smb_auth_contract = Contract(
+ contract_id=CONTRACT_ID_SMB_AUTH,
+ config=contract_config,
+ label={
+ SupportedLanguage.en: "NetExec - SMB authentication check",
+ SupportedLanguage.fr: "NetExec - Vérification authentification SMB",
+ },
+ fields=smb_auth_fields,
+ outputs=[],
+ manual=False,
+ domains=[
+ SecurityDomains.ENDPOINT.value,
+ ],
+ )
+
+ return prepare_contracts([smb_auth_contract])
diff --git a/netexec/netexec/helpers/__init__.py b/netexec/netexec/helpers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/netexec/netexec/helpers/netexec_command_builder.py b/netexec/netexec/helpers/netexec_command_builder.py
new file mode 100644
index 00000000..f4ce8590
--- /dev/null
+++ b/netexec/netexec/helpers/netexec_command_builder.py
@@ -0,0 +1,52 @@
+from typing import Dict, List, Optional
+
+
+def build_command_smb(
+ targets: List[str],
+ credentials: Optional[Dict[str, str]] = None,
+ options: Optional[List[str]] = None,
+) -> List[str]:
+ if not targets:
+ raise ValueError("At least one target is required for SMB command")
+
+ cmd = ["netexec", "smb"]
+
+ for target in targets:
+ cmd.append(target)
+
+ if credentials is not None:
+ cmd += ["-u", credentials["username"], "-p", credentials["password"]]
+
+ if options:
+ if isinstance(options, str):
+ cmd.append(options)
+ else:
+ cmd.extend(options)
+
+ return cmd
+
+
+def build_command_version() -> List[str]:
+ return ["netexec", "--version"]
+
+
+def extract_data(content: Dict) -> Optional[Dict[str, str]]:
+ username = content.get("username")
+ password = content.get("password")
+ options = content.get("options")
+
+ data: Dict = {}
+
+ if username and password:
+ data["credentials"] = {
+ "username": username,
+ "password": password,
+ }
+
+ if options:
+ if isinstance(options, list):
+ data["options"] = options
+ else:
+ data["options"] = [options]
+
+ return data or None
diff --git a/netexec/netexec/helpers/netexec_process.py b/netexec/netexec/helpers/netexec_process.py
new file mode 100644
index 00000000..74711b94
--- /dev/null
+++ b/netexec/netexec/helpers/netexec_process.py
@@ -0,0 +1,17 @@
+import subprocess
+from typing import Optional
+
+
+def execute_netexec(cmd: list[str], input_data: Optional[str] = None):
+ kwargs = {
+ "capture_output": True,
+ "check": False,
+ "text": True,
+ }
+
+ if input_data is not None:
+ kwargs["input"] = input_data
+
+ result = subprocess.run(cmd, **kwargs)
+
+ return result.stdout, result.stderr, result.returncode
diff --git a/netexec/netexec/img/icon-netexec.png b/netexec/netexec/img/icon-netexec.png
new file mode 100644
index 00000000..89736916
--- /dev/null
+++ b/netexec/netexec/img/icon-netexec.png
@@ -0,0 +1,78 @@
+
+
diff --git a/netexec/netexec/openaev_netexec.py b/netexec/netexec/openaev_netexec.py
new file mode 100644
index 00000000..c8f18e5f
--- /dev/null
+++ b/netexec/netexec/openaev_netexec.py
@@ -0,0 +1,168 @@
+import time
+from importlib.resources import files
+from typing import Dict
+
+from pyoaev.helpers import OpenAEVConfigHelper, OpenAEVInjectorHelper
+
+from injector_common.constants import TARGET_PROPERTY_SELECTOR_KEY, TARGET_SELECTOR_KEY
+from injector_common.data_helpers import DataHelpers
+from injector_common.dump_config import intercept_dump_argument
+from injector_common.targets import TargetProperty, Targets
+from netexec.configuration.config_loader import ConfigLoader
+from netexec.contracts_netexec import (
+ CONTRACT_ID_SMB_AUTH,
+)
+from netexec.helpers.netexec_command_builder import (
+ build_command_smb,
+ build_command_version,
+ extract_data,
+)
+from netexec.helpers.netexec_process import execute_netexec
+
+
+class OpenAEVNetExecInjector:
+ def __init__(self):
+ self.config = OpenAEVConfigHelper.from_configuration_object(
+ ConfigLoader().to_daemon_config()
+ )
+ intercept_dump_argument(self.config.get_config_obj())
+ icon_path = files("netexec").joinpath("img/icon-netexec.png")
+ with icon_path.open("rb") as icon_file:
+ icon_bytes = icon_file.read()
+ print(self.config)
+ self.helper = OpenAEVInjectorHelper(self.config, icon_bytes)
+
+ self._check_netexec_version()
+
+ def _check_netexec_version(self):
+ cmd = build_command_version()
+ stdout, stderr, returncode = execute_netexec(cmd)
+ if returncode != 0:
+ self.helper.injector_logger.warning(
+ f"Unable to determine NetExec version: {stderr}"
+ )
+ return
+ self.helper.injector_logger.info("NetExec version: " + stdout.strip())
+
+ def execute(self, start: float, data: Dict) -> Dict:
+ # Contract execution
+ inject_id = DataHelpers.get_inject_id(data)
+ inject_contract = DataHelpers.get_injector_contract_id(data)
+
+ available_contracts = {
+ CONTRACT_ID_SMB_AUTH: build_command_smb,
+ }
+
+ if inject_contract not in available_contracts:
+ raise ValueError(
+ f"Unsupported contract '{inject_contract}' for NetExec injector"
+ )
+
+ content = DataHelpers.get_content(data)
+ selector_key = content[TARGET_SELECTOR_KEY]
+ selector_property = content[TARGET_PROPERTY_SELECTOR_KEY]
+
+ target_results = Targets.extract_targets(
+ selector_key, selector_property, data, self.helper
+ )
+ # Deduplicate targets
+ targets = target_results.targets
+ # Handle empty targets as an error
+ if not targets:
+ message = f"No target identified for the property {TargetProperty[selector_property.upper()].value}"
+ raise ValueError(message)
+
+ # Build Arguments to execute
+ data = extract_data(content)
+
+ credentials = data.get("credentials") if data else None
+ options = data.get("options") if data else None
+ self.helper.injector_logger.info("Data: " + str(content))
+ cmd = build_command_smb(
+ targets=targets,
+ credentials=credentials,
+ options=options,
+ )
+
+ callback_data = {
+ "execution_message": Targets.build_execution_message(
+ selector_key=selector_key,
+ data=data,
+ command_args=cmd,
+ ),
+ "execution_status": "INFO",
+ "execution_duration": int(time.time() - start),
+ "execution_action": "command_execution",
+ }
+
+ self.helper.api.inject.execution_callback(
+ inject_id=inject_id,
+ data=callback_data,
+ )
+
+ stdout, stderr, returncode = execute_netexec(cmd)
+ return {
+ "success": returncode == 0,
+ "stdout": stdout,
+ "stderr": stderr,
+ }
+
+ def process_message(self, data: Dict) -> None:
+ start = time.time()
+ inject_id = DataHelpers.get_inject_id(data)
+
+ # Notify OpenAEV that execution started
+ self.helper.api.inject.execution_reception(
+ inject_id=inject_id,
+ data={"tracking_total_count": 1},
+ )
+
+ try:
+ result = self.execute(start, data)
+
+ stdout = (result.get("stdout") or "").strip()
+ stderr = (result.get("stderr") or "").strip()
+
+ if result["success"]:
+ if stdout:
+ execution_message = f"NetExec succeeded:\n{stdout}"
+ else:
+ execution_message = "NetExec succeeded: no results found"
+ else:
+ execution_message = (
+ f"NetExec failed:\n{stderr or stdout or 'No error output'}"
+ )
+
+ callback_data = {
+ "execution_message": execution_message,
+ # "execution_output_structured": "",
+ "execution_status": "SUCCESS" if result["success"] else "ERROR",
+ "execution_duration": int(time.time() - start),
+ "execution_action": "complete",
+ }
+
+ self.helper.api.inject.execution_callback(
+ inject_id=inject_id,
+ data=callback_data,
+ )
+
+ except Exception as e:
+ callback_data = {
+ "execution_message": str(e),
+ "execution_status": "ERROR",
+ "execution_duration": int(time.time() - start),
+ "execution_action": "complete",
+ }
+
+ self.helper.api.inject.execution_callback(
+ inject_id=inject_id,
+ data=callback_data,
+ )
+
+ def start(self):
+ self.helper.listen(message_callback=self.process_message)
+
+
+if __name__ == "__main__":
+ injector = OpenAEVNetExecInjector()
+ injector.start()
diff --git a/netexec/pyproject.toml b/netexec/pyproject.toml
new file mode 100644
index 00000000..a6c527ac
--- /dev/null
+++ b/netexec/pyproject.toml
@@ -0,0 +1,40 @@
+[tool.poetry]
+name = "openaev-netexec-injector"
+version = "2.1.3"
+description = "An injector for NetExec"
+authors = ["Filigran "]
+license = "Apache-2.0"
+readme = "README.md"
+packages = [
+ { include = "netexec" }
+]
+
+
+[tool.poetry.dependencies]
+python = "^3.11"
+pyoaev = [
+ { markers = "extra == 'prod' and extra != 'dev'", version = "2.1.3", source = "pypi" },
+ { markers = "extra == 'dev' and extra != 'prod'", path = "../../client-python", develop = true },
+]
+injector_common = { path = "../injector_common", develop = true }
+
+[tool.poetry.extras]
+prod = ["pyoaev"]
+dev = ["pyoaev"]
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.setuptools.package-data]
+netexec = ["img/*.png"]
+
+[tool.isort]
+profile = "black"
+known_local_folder = ["netexec"]
+
+[dependency-groups]
+dev = [
+ "black (>=25.12.0,<26.0.0)",
+ "isort (>=7.0.0,<8.0.0)"
+]