October 29, 2026
Building METIS: Inside a Modular Exploitation Framework in Python
By K3ysTr0K3R

By Jared Brits
14 min read
A framework is only as good as the code holding it together.
You can write an exploit. You can write a scanner. You can glue five scripts together and call it a toolkit.
But once you start adding modules, handling targets, tracking sessions, and managing configuration, the shortcuts start catching up with you.
That's the problem I wanted to tackle with METIS.
METIS is a Python framework for security research, built around a core that handles shared functionality and modules that implement individual tasks. Instead of making every module solve the same problems, the framework gives them a common interface.
In this article, I'm breaking down how that design works, how to use the console, and how to start building modules of your own.
No mystery. Let's look at the code.
Inspired by Metasploit
METIS takes some inspiration from Metasploit, particularly the idea of organising security tools into modules with consistent options and execution flows.
If you've used Metasploit before, parts of METIS will feel familiar: select a module, configure its options, and run it through a common interface.
The goal isn't to clone Metasploit or compete with its massive ecosystem.
METIS is my own Python-based project, built around a modular approach to exploitation research, auxiliary tooling, and threat intelligence. It's smaller, still evolving, and designed to give contributors room to build and experiment without making the framework unnecessarily complicated.
The goal isn't to clone an established framework. It's to build something modular, understandable, and open to improvement.
1. Start With the Module Manager
A modular framework needs a way to find modules without requiring every new module to be manually wired into the console.
Python's pathlib makes recursive discovery straightforward. Here's the basic idea:
from pathlib import Path
MODULE_ROOT = Path("modules")
for path in MODULE_ROOT.rglob("*.py"):
if path.name.startswith("_"):
continue
print(path)from pathlib import Path
MODULE_ROOT = Path("modules")
for path in MODULE_ROOT.rglob("*.py"):
if path.name.startswith("_"):
continue
print(path)This is an illustrative discovery snippet, not METIS's complete module loader.
Finding a Python file is only the first step. A real loader must also import the module, identify the appropriate class, handle import errors, and register it under a predictable name.
That's where a consistent module structure becomes important.
The framework should handle the common machinery. Individual modules should focus on their own jobs.
2. The Module Contract
METIS uses different base classes for different types of work.
An exploit module inherits from Exploit. An auxiliary module inherits from Auxiliary.
Both can declare metadata and options, but their execution paths differ.
The examples in METIS demonstrate this distinction:
Exploitprovides the base for exploit modules.Auxiliaryprovides the base for auxiliary tasks such as scanning and enumeration.Optiondefines configuration that the console can expose.CheckResultcommunicates the outcome of a module's assessment.action(),check(), andexploit()serve different purposes depending on the module type.
A simplified auxiliary module looks like this:
from core.auxiliary import Auxiliary
from core.module import Option
class ExampleScanner(Auxiliary):
NAME = "Example Scanner"
DESCRIPTION = "A demonstration scanner."
AUTHOR = "K3ysTr0K3R"
OPTIONS = [
Option(
"RHOST",
required=True,
description="Target hostname or IP address",
),
Option(
"RPORT",
default=80,
type=int,
description="Target port",
),
]
def action(self):
host = self.options["RHOST"]
port = self.options["RPORT"]
print(f"Configured target: {host}:{port}")from core.auxiliary import Auxiliary
from core.module import Option
class ExampleScanner(Auxiliary):
NAME = "Example Scanner"
DESCRIPTION = "A demonstration scanner."
AUTHOR = "K3ysTr0K3R"
OPTIONS = [
Option(
"RHOST",
required=True,
description="Target hostname or IP address",
),
Option(
"RPORT",
default=80,
type=int,
description="Target port",
),
]
def action(self):
host = self.options["RHOST"]
port = self.options["RPORT"]
print(f"Configured target: {host}:{port}")This example illustrates the interface pattern visible in the project. It is not a substitute for verifying the module loader and console behaviour in the current checkout.
Notice where the configuration lives.
Instead of scattering hard-coded values throughout the implementation, the module declares its options in one place. The framework can then expose those options through its configuration workflow.
That separation makes modules easier to configure, test, and maintain.
3. Running a Module
The intended workflow is simple: search for a module, select it, configure its options, and run it.
For example, a Redis scanner workflow might look like this:
metis > search redis
metis > use auxiliary/scanner/redis_cve_2022_0543
metis auxiliary(scanner/redis_cve_2022_0543) > options
metis auxiliary(scanner/redis_cve_2022_0543) > set RHOSTS 192.168.56.101
metis auxiliary(scanner/redis_cve_2022_0543) > set RPORT 6379
metis auxiliary(scanner/redis_cve_2022_0543) > runmetis > search redis
metis > use auxiliary/scanner/redis_cve_2022_0543
metis auxiliary(scanner/redis_cve_2022_0543) > options
metis auxiliary(scanner/redis_cve_2022_0543) > set RHOSTS 192.168.56.101
metis auxiliary(scanner/redis_cve_2022_0543) > set RPORT 6379
metis auxiliary(scanner/redis_cve_2022_0543) > runThese commands illustrate the documented module workflow. Exact module names and console syntax should be checked against the current version of METIS.
The point is consistency.
A researcher shouldn't need to learn a completely different interface every time they change tools. Module selection, configuration, and execution should follow a predictable pattern.
That becomes increasingly important as the number of available modules grows.
4. When an Exploit Actually Works
A vulnerability check and a successful exploit are two different things.
A check might find indicators that a target is vulnerable. That's useful evidence, but it doesn't automatically mean the exploit will work.
Network conditions, configuration differences, patched components, permissions, and environmental restrictions can all affect the result.
The METIS README documents an example involving the Redis vulnerability CVE-2022โ0543. The transcript shows the scanner identifying a vulnerable lab target, followed by the exploit module creating a session and returning command output.
The documented session interaction looks like this:
metis exploit(linux/redis/redis_cve_2022_0543_rce) > sessions
a3f2b1c4 192.168.56.101:51234 (alive)
metis exploit(linux/redis/redis_cve_2022_0543_rce) > interact a3f2b1c4
[*] interacting with a3f2b1c4. 'background' returns, '!cmd' runs locally.
a3f2b1c4 $ id
uid=105(redis) gid=108(redis) groups=108(redis)
a3f2b1c4 $ background
metis >metis exploit(linux/redis/redis_cve_2022_0543_rce) > sessions
a3f2b1c4 192.168.56.101:51234 (alive)
metis exploit(linux/redis/redis_cve_2022_0543_rce) > interact a3f2b1c4
[*] interacting with a3f2b1c4. 'background' returns, '!cmd' runs locally.
a3f2b1c4 $ id
uid=105(redis) gid=108(redis) groups=108(redis)
a3f2b1c4 $ background
metis >This is a documented example from the project README, not a claim that I independently reproduced the session.
Why include it?
Because it demonstrates the distinction between a preliminary assessment and stronger evidence of successful execution. A VULNERABLE result is the module's assessment; a usable session and meaningful command output provide additional evidence that the demonstrated operation succeeded.
That distinction matters when building reliable security tools.
Report what you observed, not what you hoped would happen.
A module's output is part of its evidence. If it cannot verify a result, it shouldn't pretend otherwise.
5. Auxiliary Modules and Exploits Aren't the Same Thing
This distinction affects the entire framework.
An auxiliary module performs a task such as scanning, enumeration, or service identification. Its primary operation is commonly implemented in action().
An exploit module has a different purpose. It may assess a target through check(), then implement its main operation in exploit(). Depending on the module, it may also need payload handling or session management.
The Redis scanner illustrates auxiliary functionality. It defines options, expands target specifications, probes Redis, processes results, and optionally records findings.
The Joomla JCE module illustrates a more complex exploit-module structure. It builds an HTTP session, handles requests, extracts a token, and integrates with METIS session management.
These aren't interchangeable implementations.
The base class and execution method should match the module's actual purpose.
Keeping that distinction clear helps contributors understand where their code belongs and what the framework expects from it.
6. Checks: Don't Confuse UNKNOWN With SAFE
A check is only as good as the evidence behind it.
METIS uses CheckResult to represent the outcome of a module's assessment. In practice, a check should distinguish between evidence supporting a vulnerability, evidence against it, and situations where the available evidence is insufficient.
For example:
from core.check import CheckResult
def check(self):
# Perform a suitable assessment here.
# The actual logic depends on the vulnerability.
return CheckResult.UNKNOWNfrom core.check import CheckResult
def check(self):
# Perform a suitable assessment here.
# The actual logic depends on the vulnerability.
return CheckResult.UNKNOWNThis is a structural example, not a complete vulnerability check.
Consider a network request that times out. That doesn't prove a target is safe. The service might be unavailable, a firewall might be filtering traffic, or the timeout might simply be too short.
Likewise, identifying an application by its banner doesn't prove that a particular vulnerability exists.
A good check uses evidence relevant to the vulnerability being assessed and reports uncertainty honestly.
The Joomla module provides a useful lesson here. Its current check looks for Joomla signatures and attempts to retrieve a CSRF token. Those indicators may help identify the application and establish that a token is present, but they do not conclusively establish that the specific JCE vulnerability is exploitable.
That check should be described as a preliminary heuristic unless it has been validated against the vulnerability's actual conditions.
UNKNOWN isn't SAFE. A failed or inconclusive check is not proof that a vulnerability doesn't exist.
That mindset matters when building scanners other people may rely on.
7. How Target Parsing Works
Scanners often need to handle more than one hostname.
The Redis scanner uses resolve_targets() and supports target specifications such as single hosts, comma-separated lists, CIDR ranges, address ranges, and files.
Target expansion belongs in a reusable helper instead of being duplicated across every scanner.
Python's ipaddress module is useful when implementing address-range handling:
import ipaddress
network = ipaddress.ip_network("192.168.56.0/24")
for address in network.hosts():
print(address)import ipaddress
network = ipaddress.ip_network("192.168.56.0/24")
for address in network.hosts():
print(address)This example handles a CIDR network. It doesn't implement METIS's full target syntax, which includes additional forms and should be handled by the framework's own target-resolution code.
The scanner should also make it clear when a check examines only one target while a full scan processes many. The Redis scanner explicitly warns when a multi-target configuration is used with its single-target check() method.
That's a small detail, but it prevents confusing results.
Target handling should be predictable, reusable, and transparent about what is actually being scanned.
8. Sessions and Connection Handling
Some exploit modules finish after sending a request and interpreting the response. Others need to manage a longer-lived connection.
The Redis example in the README illustrates how METIS can track an interactive session. The Joomla module also demonstrates a session integration point:
self.framework.sessions.add_webshell(
shell_url,
method="get",
cmd_param="cmd",
session=session,
)self.framework.sessions.add_webshell(
shell_url,
method="get",
cmd_param="cmd",
session=session,
)This snippet is taken from the supplied Joomla module. It illustrates the framework integration point, not a general-purpose session implementation.
The framework should own shared session management where possible. That avoids every module implementing its own session registry, interaction commands, and lifecycle handling.
Session creation must still be based on successful verification.
Receiving an HTTP 200 response does not, by itself, prove that an interactive session works. Modules should report failures honestly and account for cleanup when sessions or other resources are no longer needed.
This is another reason to keep responsibilities separate: the module handles its specific task, while shared framework components manage the functionality that multiple modules need.
9. Reporting Findings
A scanner that identifies a problem should be able to preserve its findings rather than printing everything to the terminal and losing the information when the process exits.
The Redis scanner demonstrates reporting methods such as:
self.report_host(h.host, state="up")
self.report_service(
h.host,
h.port,
proto="tcp",
state="open",
name="redis",
banner="",
)
self.report_vuln(
name=self.NAME,
address=h.host,
severity="critical",
refs="CVE-2022-0543",
info="Lua sandbox escape via package.loadlib",
)self.report_host(h.host, state="up")
self.report_service(
h.host,
h.port,
proto="tcp",
state="open",
name="redis",
banner="",
)
self.report_vuln(
name=self.NAME,
address=h.host,
severity="critical",
refs="CVE-2022-0543",
info="Lua sandbox escape via package.loadlib",
)These calls come from the supplied Redis module. They show how a module can pass host, service, and vulnerability information into framework reporting functionality.
The module should report only findings supported by its evidence.
A failed connection is not proof of a vulnerability, and an open port is not proof that a service is exploitable.
The Redis example also checks whether a database workspace is active before recording findings. That keeps reporting tied to the framework's current state rather than assuming a database is always available.
Good reporting is more than saving output. It's preserving findings with enough context to understand what was observed and why.
10. Make the Workflow Repeatable
Security research often involves running the same workflow more than once.
Manually entering every option becomes tedious and increases the chance of mistakes.
Resource files can make a console workflow repeatable. A simple illustrative file might look like this:
use auxiliary/scanner/redis_cve_2022_0543
set RHOSTS 192.168.56.101
set RPORT 6379
runuse auxiliary/scanner/redis_cve_2022_0543
set RHOSTS 192.168.56.101
set RPORT 6379
runSave a workflow in a resource file only after confirming that the current METIS console supports the commands and file syntax used in it.
Repeatability is useful for lab work, regression testing, and comparing behaviour across versions. Keep target lists and configuration appropriate to the environment you're authorised to test.
The bigger idea is straightforward: a good workflow should be repeatable without relying on someone remembering every step.
11. Build Your First METIS Module
Understanding the architecture is one thing. Writing a module that fits into it is where the design becomes practical.
We'll look at both module types: an auxiliary scanner and an exploit module. The examples use the conventions visible in the supplied METIS code, while avoiding the assumption that illustrative snippets have been independently tested against the complete framework.
Part A: Build an Auxiliary Module
Let's start with a simple TCP connection check.
It doesn't exploit anything. It checks whether a TCP connection can be established to a configured host and port.
Step 1: Create the File
In the project's auxiliary scanner directory, create:
auxiliary/
โโโ scanner/
โโโ tcp_connect_check.pyauxiliary/
โโโ scanner/
โโโ tcp_connect_check.pyUse the directory layout present in your checkout. The path shown here follows the organisation of the supplied Redis scanner.
Step 2: Add the Code
import socketfrom core import colors
from core.auxiliary import Auxiliary
from core.module import Option
class TCPConnectCheck(Auxiliary):
NAME = "TCP Connection Check"
DESCRIPTION = (
"Checks whether a TCP connection can be established "
"to a specified host and port."
)
AUTHOR = "K3ysTr0K3R"
OPTIONS = [
Option(
"RHOST",
required=True,
description="Target hostname or IP address",
),
Option(
"RPORT",
default=80,
type=int,
description="Target TCP port",
),
Option(
"TIMEOUT",
default=3.0,
type=float,
description="Connection timeout in seconds",
),
]
def action(self):
host = self.options["RHOST"]
port = self.options["RPORT"]
timeout = self.options["TIMEOUT"]
print(colors.info(f"Checking {host}:{port}"))
try:
with socket.create_connection(
(host, port),
timeout=timeout,
):
print(colors.good(
f"TCP connection succeeded: {host}:{port}"
))
except socket.timeout:
print(colors.warn(
f"Connection timed out: {host}:{port}"
))
except OSError as exc:
print(colors.bad(
f"Connection failed: {type(exc).__name__}"
))import socketfrom core import colors
from core.auxiliary import Auxiliary
from core.module import Option
class TCPConnectCheck(Auxiliary):
NAME = "TCP Connection Check"
DESCRIPTION = (
"Checks whether a TCP connection can be established "
"to a specified host and port."
)
AUTHOR = "K3ysTr0K3R"
OPTIONS = [
Option(
"RHOST",
required=True,
description="Target hostname or IP address",
),
Option(
"RPORT",
default=80,
type=int,
description="Target TCP port",
),
Option(
"TIMEOUT",
default=3.0,
type=float,
description="Connection timeout in seconds",
),
]
def action(self):
host = self.options["RHOST"]
port = self.options["RPORT"]
timeout = self.options["TIMEOUT"]
print(colors.info(f"Checking {host}:{port}"))
try:
with socket.create_connection(
(host, port),
timeout=timeout,
):
print(colors.good(
f"TCP connection succeeded: {host}:{port}"
))
except socket.timeout:
print(colors.warn(
f"Connection timed out: {host}:{port}"
))
except OSError as exc:
print(colors.bad(
f"Connection failed: {type(exc).__name__}"
))Step 3: Understand the Code
The class inherits from Auxiliary, placing it in the auxiliary module category.
OPTIONS declares the values the user can configure. RHOST is required, while RPORT and TIMEOUT have defaults.
action() contains the main operation. It reads those values, attempts a TCP connection, and prints the result.
The with statement ensures the successful connection is closed after the check. Exception handling separates a timeout from other operating-system connection errors.
And here's the limitation: a successful TCP connection only tells you that the connection was established. It doesn't establish which service is listening or whether a vulnerability exists.
That's exactly the kind of distinction a well-designed module should communicate.
Step 4: Load and Run It
The intended workflow is to search for the module, select it, configure the options, and run it:
metis > search tcp
metis > use auxiliary/scanner/tcp_connect_check
metis auxiliary(scanner/tcp_connect_check) > options
metis auxiliary(scanner/tcp_connect_check) > set RHOST 192.168.56.101
metis auxiliary(scanner/tcp_connect_check) > set RPORT 80
metis auxiliary(scanner/tcp_connect_check) > set TIMEOUT 3
metis auxiliary(scanner/tcp_connect_check) > runmetis > search tcp
metis > use auxiliary/scanner/tcp_connect_check
metis auxiliary(scanner/tcp_connect_check) > options
metis auxiliary(scanner/tcp_connect_check) > set RHOST 192.168.56.101
metis auxiliary(scanner/tcp_connect_check) > set RPORT 80
metis auxiliary(scanner/tcp_connect_check) > set TIMEOUT 3
metis auxiliary(scanner/tcp_connect_check) > runThese commands are illustrative. Verify the exact module name, discovery path, and console syntax against your current METIS checkout before publishing this as a tested walkthrough.
If the module doesn't load, check its location, imports, class inheritance, and the loader's expectations.
If it loads but doesn't run, inspect how the console dispatches auxiliary modules.
The point of this exercise isn't to build the world's most advanced scanner. It's to understand how a small, self-contained module fits into the framework.
Part B: Understand the Exploit-Module Structure
An exploit module uses the Exploit base class and typically implements exploit() for its main operation. It may also implement check() when the framework supports pre-exploitation checks.
The Joomla JCE module demonstrates this structure. Its class defines metadata and options, constructs an HTTP base URL, creates a request session, wraps HTTP requests for tracing, and implements separate methods for assessment and exploitation.
Rather than reproducing its operational webshell-upload behaviour as a beginner exercise, we can use a non-operational template to show the class structure.
Step 1: Define the Class and Options
from core import colors
from core.check import CheckResult
from core.exploit import Exploit
from core.module import Option
class ExampleExploit(Exploit):
NAME = "Example HTTP Exploit"
DESCRIPTION = (
"Demonstrates the structure of a METIS exploit module."
)
AUTHOR = "K3ysTr0K3R"
CHECK = TrueOPTIONS = [
Option(
"RHOST",
required=True,
description="Target host",
),
Option(
"RPORT",
default=80,
type=int,
description="HTTP port",
),
Option(
"SSL",
default=False,
type=bool,
description="Use HTTPS",
),
Option(
"TIMEOUT",
default=10.0,
type=float,
description="HTTP timeout in seconds",
),
]
def check(self):
# Implement a vulnerability-specific assessment.
return CheckResult.UNKNOWN
def exploit(self):
print(colors.info(
"Exploit module selected and configured."
))
print(colors.warn(
"No exploitation action is implemented."
))
return Nonefrom core import colors
from core.check import CheckResult
from core.exploit import Exploit
from core.module import Option
class ExampleExploit(Exploit):
NAME = "Example HTTP Exploit"
DESCRIPTION = (
"Demonstrates the structure of a METIS exploit module."
)
AUTHOR = "K3ysTr0K3R"
CHECK = TrueOPTIONS = [
Option(
"RHOST",
required=True,
description="Target host",
),
Option(
"RPORT",
default=80,
type=int,
description="HTTP port",
),
Option(
"SSL",
default=False,
type=bool,
description="Use HTTPS",
),
Option(
"TIMEOUT",
default=10.0,
type=float,
description="HTTP timeout in seconds",
),
]
def check(self):
# Implement a vulnerability-specific assessment.
return CheckResult.UNKNOWN
def exploit(self):
print(colors.info(
"Exploit module selected and configured."
))
print(colors.warn(
"No exploitation action is implemented."
))
return NoneThis is a structural example, not a functional exploit. It illustrates the interface visible in the supplied Joomla module without pretending that an actual vulnerability check or exploitation operation has been implemented.
Step 2: Separate Configuration From Logic
The Joomla module defines RHOST, RPORT, SSL, TIMEOUT, and VERIFY_SHELL.
This lets the console manage configuration rather than requiring users to edit the source code for every target.
Its _base() helper centralises construction of the HTTP base URL. Its _session() helper configures a reusable requests.Session. Its _req() helper wraps HTTP requests and integrates with the framework's tracing functionality.
Those are useful design patterns, but they should only be added to a module when the module needs them.
A small check doesn't need a large abstraction layer simply because a more complex exploit has one.
The supplied Joomla example disables TLS certificate verification. That can be convenient in a controlled lab, but it weakens certificate validation. Avoid copying that setting automatically; document the trade-off and use certificate verification where practical.
Step 3: Implement an Evidence-Based Check
A check should gather evidence relevant to the vulnerability being assessed.
The Joomla module currently searches for Joomla-related signatures and attempts to extract a CSRF token. That may indicate that Joomla is present and that a token can be retrieved, but it does not conclusively prove that the JCE vulnerability exists or that exploitation will succeed.
A stronger check should be tied to the vulnerability's documented conditions and validated against suitable test cases.
If the available evidence is inconclusive, return UNKNOWN rather than making an unsupported claim.
Step 4: Handle Requests and Failures Cleanly
When a module communicates over HTTP, consider connection errors, timeouts, unexpected status codes, redirects, response content, and TLS verification.
Keep request handling in reusable helpers when doing so improves consistency.
Avoid swallowing exceptions without explaining what failed, and don't label an operation successful simply because the server returned a response.
The result should reflect what the module actually verified.
Step 5: Understand Session Integration
The Joomla module demonstrates integration with the framework's session manager using self.framework.sessions.add_webshell(...).
This is a framework-level feature, not a requirement for every exploit module.
Session registration should only occur after the module has verified that a usable session exists. The implementation should also account for session cleanup and failure handling.
For a first module, concentrate on the class contract, options, check behaviour, and error handling. Add more advanced integrations only when the module requires them and the framework API is understood.
Part C: Test Before Contributing
Before contributing either kind of module, work through this checklist:
- The module imports successfully.
- METIS discovers and loads it from the intended directory.
- Its options appear with the expected defaults and types.
- Required options are handled correctly.
- Network errors and timeouts are handled cleanly.
- The output reflects the evidence actually collected.
- Checks distinguish uncertainty from confirmed findings.
- Session management is only used when necessary and correctly implemented.
- Testing takes place against authorised systems.
- The module's purpose, references, requirements, and limitations are documented.
If a module writes findings to a database, test that behaviour separately.
If it creates sessions, test their lifecycle and cleanup.
If it performs vulnerability checks, test positive, negative, and inconclusive cases.
A module isn't finished just because it runs once.
It should also behave predictably when something goes wrong.
Part D: Contribute to METIS
Once your module is ready, compare it with existing modules that perform similar tasks. Keep the naming, metadata, options, and output consistent with the rest of the project.
METIS is hosted on Gitea:
Repository: https://gitea.com/K3ysTr0K3R/METIS
Use the project's contribution workflow to propose your changes. Include a clear explanation of what the module does, its dependencies, how it was tested, and what limitations remain.
The goal isn't to add another Python file just to increase the module count.
The goal is to add something another researcher can understand, reproduce in an authorised environment, and maintain.
That's the point of a modular framework: the core handles the shared machinery, while each module stays responsible for one clearly defined task.
12. The Rough Edges Matter
A modular framework introduces its own engineering challenges.
Dynamic imports can fail. A module may declare an option that the console doesn't handle as expected. A vulnerability check can produce false positives. A scanner can overwhelm a target if concurrency isn't controlled. A session can become stale or fail to clean up properly.
These aren't minor details.
They affect whether other researchers can trust the tool.
Some areas worth testing as METIS evolves include:
- Module discovery: clear errors for failed imports and invalid classes.
- Option validation: predictable handling of missing values and incorrect types.
- Check accuracy: honest distinctions between positive, negative, and inconclusive evidence.
- TLS handling: secure defaults and clear configuration.
- Target expansion: guardrails around large ranges and concurrent requests.
- Session lifecycle: reliable registration, interaction, and cleanup.
- Reporting: findings that match the evidence and integrate correctly with workspaces.
The framework should make common tasks easier without hiding important failure conditions.
That's the balance worth aiming for: less repeated work, but no loss of transparency.
13. Where METIS Goes From Here
METIS is a continuing project, not a finished product.
The direction is to keep the framework modular, make it easier to contribute new research modules, and improve the shared infrastructure without forcing every module to reinvent it.
That means improving module discovery, tightening option handling, testing the session lifecycle, and making findings more consistent.
It also means documenting the framework well enough that contributors can work with the code instead of guessing how it behaves.
A framework becomes more useful when people can extend it without needing to understand every line of the core first.
That's the idea behind METIS.
Final Thoughts
Building an exploit is one challenge.
Building a framework that can support different modules, handle configuration, preserve findings, and manage sessions is another.
METIS brings those pieces together in a Python-based project shaped around modular security research. Its architecture takes inspiration from established frameworks such as Metasploit while leaving room to develop its own approach.
There's still work to do. That's part of building a real project: testing assumptions, fixing rough edges, and making the code easier for the next researcher to understand.
If you're interested in the implementation, want to inspect the modules, or want to contribute, check out the project on Gitea:
Build carefully. Test honestly. Make the next module better than the last.
METIS is intended for authorised security research, education, and testing on systems you own or have explicit permission to assess. Contributors are responsible for ensuring their modules are used lawfully and responsibly.