Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
83544f9
Update registry read and write eldritch functions to merge hive and path
google-labs-jules[bot] Mar 7, 2026
8b5078d
Update registry read and write eldritch functions to merge hive and path
google-labs-jules[bot] Mar 7, 2026
83f02bd
Consolidate write_reg functions in eldritch sys library
google-labs-jules[bot] Mar 7, 2026
32db384
Support both double and single backslash in registry paths
google-labs-jules[bot] Mar 7, 2026
aedda86
Fix parsing of double backslashes in registry paths
google-labs-jules[bot] Mar 7, 2026
7bb0733
Fix formatting issues in implants/lib/eldritch/stdlib/eldritch-libsys
google-labs-jules[bot] Mar 7, 2026
8f63e67
Update registry docs
google-labs-jules[bot] Mar 8, 2026
bf48318
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 8, 2026
6181b3e
Update documentation for new registry API functions
google-labs-jules[bot] Mar 8, 2026
1281303
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 8, 2026
d23cd25
Fix flaky test issues in Go portals integration tests
google-labs-jules[bot] Mar 8, 2026
481479c
Update auth.go
hulto Mar 14, 2026
2a05357
Update time.sleep method signature to use float
hulto Mar 14, 2026
09b867a
Remove slop
hulto Mar 14, 2026
497de20
Unslop
hulto Mar 14, 2026
d990898
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 14, 2026
e42324d
Fix formatting issue in write_reg_impl.rs
google-labs-jules[bot] Mar 14, 2026
f7d43d4
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 14, 2026
072546c
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 14, 2026
050960d
Merge branch 'main' into update-reg-eldritch-functions-11136848008787…
hulto Mar 14, 2026
2a4669f
Fix undefined 'status' variable in process_list_impl.rs
google-labs-jules[bot] Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM mcr.microsoft.com/devcontainers/base:trixie

ARG ZIG_VERSION=0.16.0-dev.2877+627f03af9
ARG ZIG_VERSION=0.16.0-dev.2349+204fa8959

ENV GOPATH=/go
ENV RUSTUP_HOME=/usr/local/rustup \
Expand Down
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ cargo test
# Regenerate code after ent schemas, GraphQL, or frontend changes
go generate ./...
```
**Note** If go generate fails run it a second time.

### Formatting

Expand Down
2 changes: 1 addition & 1 deletion bin/socks5/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// EnvAPIKey is the name of the environment variable to optionally provide an API key
const EnvAPIKey = "TAVERN_API_TOKEN"
const EnvAPIKey = "TAVERN_API_KEY"

func getAuthToken(ctx context.Context, tavernURL, cachePath string) (auth.Token, error) {
return auth.Authenticate(
Expand Down
4 changes: 2 additions & 2 deletions docs/_docs/admin-guide/tavern.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ After installing the gcloud CLI, run `gcloud auth application-default login` to
2. Clone [the repo](https://github.com/spellshift/realm) and navigate to the `terraform` directory.
3. Checkout the latest stable release `git checkout -b latest $(git tag | tail -1)`
4. Run `terraform init` to install the Google provider for terraform.
5. Run `terraform apply -var="gcp_project=<PROJECT_ID>" -var="oauth_client_id=<OAUTH_CLIENT_ID>" -var="oauth_client_secret=<OAUTH_CLIENT_SECRET>" -var="oauth_domain=<OAUTH_DOMAIN>" -var="tavern_container_image=spellshift/tavern:$(git tag | tail -1)"` to deploy Tavern!
5. Run `terraform apply -var="gcp_project=<PROJECT_ID>" -var="oauth_client_id=<OAUTH_CLIENT_ID>" -var="oauth_client_secret=<OAUTH_CLIENT_SECRET>" -var="oauth_domain=<OAUTH_DOMAIN>" -var="tavern_container_image=$(git tag | tail -1)"` to deploy Tavern!

**Example:**

```sh
terraform apply -var="gcp_project=new-realm-deployment" -var="oauth_client_id=12345.apps.googleusercontent.com" -var="oauth_client_secret=ABCDEFG" -var="oauth_domain=test-tavern.redteam.toys" -var="tavern_container_image=spellshift/tavern:$(git tag | tail -1)"
terraform apply -var="gcp_project=new-realm-deployment" -var="oauth_client_id=12345.apps.googleusercontent.com" -var="oauth_client_secret=ABCDEFG" -var="oauth_domain=test-tavern.redteam.toys" -var="tavern_container_image=$(git tag | tail -1)"
```

After terraform completes successfully, head to the [DNS mappings for Cloud Run](https://console.cloud.google.com/run/domains) and wait for a certificate to successfully provision. This may take a while, so go enjoy a nice cup of coffee ☕
Expand Down
21 changes: 1 addition & 20 deletions docs/_docs/dev-guide/tavern.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,9 @@ Before reading this guide, please check out the [admin guide](/admin-guide/taver

### Creating a New Model

1. Initialize the schema `cd tavern/internal && go run entgo.io/ent/cmd/ent new <NAME>`
1. Initialize the schema `cd tavern && go run entgo.io/ent/cmd/ent init <NAME>`
2. Update the generated file in `tavern/internal/ent/schema/<NAME>.go`
3. Ensure you include a `func (<NAME>) Annotations() []schema.Annotation` method which returns a `entgql.QueryField()` annotation to tell entgo to generate a GraphQL root query for this model (if you'd like it to be queryable from the root query)
```go
// Examples of Annotations we've used throughout the codebase.
// Additional annotations can be found:
// https://entgo.io/docs/schema-annotations/
// Annotations describes additional information for the ent.
func (<NAME>) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.RelayConnection(), // Recommended - Required for pagination
entgql.MultiOrder(), // Recommended - Required for pagination
entgql.Mutations(
entgql.MutationCreate(), // Auto generate create mutation
entgql.MutationUpdate(), // Auto generate update mutation
),
entsql.Annotation{
Collation: "utf8mb4_general_ci", // Recommended - requried for case insensitive searching
},
}
}
```
4. Update `tavern/internal/graphql/gqlgen.yml` to include the ent types in the `autobind:` section (e.g.`- github.com/spellshift/realm/tavern/internal/ent/<NAME>`)
5. **Optionally** update the `models:` section of `tavern/internal/graphql/gqlgen.yml` to bind any GraphQL enum types to their respective `entgo` generated types (e.g. `github.com/spellshift/realm/tavern/internal/ent/<NAME>.<ENUM_FIELD>`)
6. Run `go generate ./tavern/...` from the project root
Expand Down
112 changes: 9 additions & 103 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,18 +513,6 @@ The **crypto.decode_b64** method encodes the given text using the given base64 d
- URL_SAFE
- URL_SAFE_NO_PAD

### crypto.encode_utf16le

`crypto.encode_utf16le(content: str) -> Bytes`

The **crypto.encode_utf16le** method encodes a UTF-8 string into UTF-16LE bytes, which are commonly used in Windows environments.

### crypto.decode_utf16le

`crypto.decode_utf16le(content: Bytes) -> str`

The **crypto.decode_utf16le** method decodes UTF-16LE bytes into a standard UTF-8 string. Errors if the bytes are not valid UTF-16.

### crypto.from_json

`crypto.from_json(content: str) -> Value`
Expand Down Expand Up @@ -1455,105 +1443,23 @@ sys.shell("ls /nofile")
}
```

### sys.write_reg_hex

`sys.write_reg_hex(reghive: str, regpath: str, regname: str, regtype: str, regvalue: str) -> Bool`

The **sys.write_reg_hex** method returns `True` if registry values are written to the requested registry path and accepts a hexstring as the value argument.
An example is below:

```python
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_SZ","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_BINARY","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_NONE","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_EXPAND_SZ","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD_BIG_ENDIAN","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_LINK","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_MULTI_SZ","dead,beef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_LIST","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_FULL_RESOURCE_DESCRIPTOR","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_REQUIREMENTS_LIST","deadbeef")
True
$> sys.write_reg_hex("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_QWORD","deadbeefdeadbeef")
True
```

### sys.write_reg_int
### sys.write_reg

`sys.write_reg_int(reghive: str, regpath: str, regname: str, regtype: str, regvalue: int) -> Bool`
`sys.write_reg(path: str, regname: str, regtype: str, regvalue: any) -> Bool`

The **sys.write_reg_int** method returns `True` if registry values are written to the requested registry path and accepts an integer as the value argument.
The **sys.write_reg** method returns `True` if registry values are written to the requested registry path. It accepts dynamically-typed values depending on the specific registry type.
An example is below:

```python
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_SZ",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_BINARY",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_NONE",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_EXPAND_SZ",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD_BIG_ENDIAN",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_LINK",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_MULTI_SZ",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_LIST",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_FULL_RESOURCE_DESCRIPTOR",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_REQUIREMENTS_LIST",12345678)
True
$> sys.write_reg_int("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_QWORD",12345678)
True
```

### sys.write_reg_str

`sys.write_reg_str(reghive: str, regpath: str, regname: str, regtype: str, regvalue: str) -> Bool`

The **sys.write_reg_str** method returns `True` if registry values are written to the requested registry path and accepts a string as the value argument.
An example is below:

```python
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_SZ","BAR1")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_BINARY","DEADBEEF")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_NONE","DEADBEEF")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_EXPAND_SZ","BAR2")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD","12345678")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_DWORD_BIG_ENDIAN","12345678")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_LINK","A PLAIN STRING")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_MULTI_SZ","BAR1,BAR2,BAR3")
$> sys.write_reg("HKCU\SOFTWARE\TEST1","FOO1","REG_SZ","BAR1")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_LIST","DEADBEEF")
$> sys.write_reg("HKCU\SOFTWARE\TEST1","FOO1","REG_BINARY","deadbeef")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_FULL_RESOURCE_DESCRIPTOR","DEADBEEF")
$> sys.write_reg("HKCU\SOFTWARE\TEST1","FOO1","REG_DWORD",12345678)
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_RESOURCE_REQUIREMENTS_LIST","DEADBEEF")
$> sys.write_reg("HKCU\SOFTWARE\TEST1","FOO1","REG_MULTI_SZ","BAR1,BAR2,BAR3")
True
$> sys.write_reg_str("HKEY_CURRENT_USER","SOFTWARE\\TEST1","FOO1","REG_QWORD","1234567812345678")
$> sys.write_reg("HKCU\SOFTWARE\TEST1","FOO1","REG_QWORD",12345678)
True
```

Expand Down Expand Up @@ -1595,6 +1501,6 @@ The **time.now** method returns the time since UNIX EPOCH (Jan 01 1970). This us

### time.sleep

`time.sleep(secs: float)`
`time.sleep(secs: int)`

The **time.sleep** method sleeps the task for the given number of seconds.
89 changes: 2 additions & 87 deletions docs/_docs/user-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,53 +231,6 @@ See the [Eldritch User Guide](/user-guide/eldritch) for more information.
Imix can execute up to 127 threads concurrently after that the main imix thread will block behind other threads.
Every callback interval imix will query each active thread for new output and relay that back to the c2. This means even long running tasks will report their status as new data comes in.

## Guardrails

Guardrails allow operators to ensure that Imix only runs on approved or expected hosts. This is particularly useful for preventing accidental execution in the wrong environment or ensuring that a payload only activates when specific conditions are met (e.g., a specific file exists, a process is running, or a registry key is set).

By default, Imix compiles with no guardrails, meaning it will run on any host it lands on. Guardrails are evaluated at startup, and if any guardrail fails to validate, Imix will immediately exit. If multiple guardrails are configured, only **one** guardrail needs to successfully pass for the agent to continue execution (an OR condition).

Guardrails are configured at build time using the `IMIX_GUARDRAILS` environment variable. Similar to host uniqueness, it takes a JSON list of objects specifying the guardrails.

### Example Guardrails

```bash
export IMIX_GUARDRAILS='[
{
"type": "file",
"args": {
"path": "/etc/expected_file.txt"
}
},
{
"type": "process",
"args": {
"name": "explorer.exe"
}
},
{
"type": "registry",
"args": {
"subkey": "SOFTWARE\\MyCompany\\ExpectedKey",
"value_name": "ExpectedValue"
}
}
]'
```

### Available Guardrails

* `file`: Checks if a specific file exists on disk. (Case-insensitive check is performed by converting to lower case).
* `path` (string, required): The full path to the file.
* `process`: Checks if a specific process is currently running. (Case-insensitive check is performed by converting to lower case).
* `name` (string, required): The name of the process (e.g., `explorer.exe`).
* `registry` (Windows only): Checks if a specific registry key or value exists.
* `subkey` (string, required): The path to the registry subkey under `HKEY_CURRENT_USER` or `HKEY_LOCAL_MACHINE`.
* `value_name` (string, optional): The name of a specific value to look for within the subkey. If omitted, only checks if the subkey itself exists.
- **`env`**: Uses the `IMIX_HOST_ID` environment variable at runtime.
- **`file`**: Uses a unique ID generated and saved to a file on disk. Accepts an optional `args` parameter `path_override` to specify a custom file path.
- **`macaddr`**: Uses the MAC address of the first non-loopback network interface.
- **`registry`**: Uses a registry key (Windows only). Must be enabled via `win_service` feature flag. Accepts `args` parameters `subkey` and optionally `value_name`.

## Identifying unique hosts

Expand All @@ -300,47 +253,9 @@ This isn't ideal as in the UI each new beacon will appear as though it were on a

To change the default uniqueness behavior you can set the `IMIX_UNIQUE` environment variable at build time.

`IMIX_UNIQUE` should be a JSON array containing a list of JSON objects, where `type` is a required field and `args` is an optional field. The `args` object structure is dependent on the `type` selected. The selectors will be evaluated in the order they are specified.
`IMIX_UNIQUE` should be a list of JSON objects with `type` as a required field with args as an optional field.

### Available Selectors
By default IMIX_UNIQUE is about equal to: `export IMIX_UNIQUE='[{"type":"env"},{"type":"file"},{"type":"file","args":{"path_override":"/etc/system-id"}},{"type":"macaddr"}]'`

To proiritize stealth we reccomend removing the file uniqueness selectors: `export IMIX_UNIQUE='[{"type":"env"},{"type":"macaddr"}]'`
If you know the environment will have VMs cloned without sysprep we recommend proritizing the file selectors and removing macaddr: `export IMIX_UNIQUE='[{"type":"env"},{"type":"file"},{"type":"file","args":{"path_override":"/etc/system-id"}}]'`

### Default Behavior

By default `IMIX_UNIQUE` is equivalent to the following:

```bash
export IMIX_UNIQUE='[
{"type": "env"},
{"type": "file"},
{"type": "file", "args": {"path_override": "/etc/system-id"}},
{"type": "macaddr"}
]'
```
*(Note: the JSON must be minified/single-line when passed as an environment variable in practice. This is formatted for readability.)*

### Example: Prioritize Stealth

To prioritize stealth we recommend removing the file uniqueness selectors to prevent dropping arbitrary files to disk. Imix will first try checking the `IMIX_HOST_ID` environment variable, and if it is not set, it will fallback to using the network interface's MAC address.

```bash
export IMIX_UNIQUE='[{"type":"env"},{"type":"macaddr"}]'
```

### Example: VM Cloning Without Sysprep

If you know the environment will have VMs cloned without sysprep (resulting in duplicate MAC addresses across instances), we recommend prioritizing the file selectors and removing `macaddr`:

```bash
export IMIX_UNIQUE='[{"type":"env"},{"type":"file"},{"type":"file","args":{"path_override":"/etc/system-id"}}]'
```

### Example: Registry Key (Windows)

On Windows, you can optionally configure Imix to fetch the uniqueness ID from a registry value. Note that the `registry` type is only valid for Windows targets.

```bash
export IMIX_UNIQUE='[{"type":"env"},{"type":"registry","args":{"subkey":"SOFTWARE\\MyCompany","value_name":"InstallID"}}]'
```
2 changes: 0 additions & 2 deletions implants/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ members = [
"lib/transport",
"lib/pb",
"lib/host_unique",
"lib/guardrails",
"lib/netstat",
"lib/eldritch/eldritch-core",
"lib/eldritch/eldritch-macros",
Expand Down Expand Up @@ -35,7 +34,6 @@ resolver = "2"
[workspace.dependencies]
transport = { path = "./lib/transport" }
host_unique = { path = "./lib/host_unique" }
guardrails = { path = "./lib/guardrails" }
pb = { path = "./lib/pb" }
netstat = { path = "./lib/netstat" }

Expand Down
Loading
Loading