Skip to content

Custom Deny ACEs BED-8117#298

Draft
JonasBK wants to merge 7 commits intov4from
BED-8117-deny-aces
Draft

Custom Deny ACEs BED-8117#298
JonasBK wants to merge 7 commits intov4from
BED-8117-deny-aces

Conversation

@JonasBK
Copy link
Copy Markdown
Contributor

@JonasBK JonasBK commented Apr 23, 2026

Description

Adds support for a new BloodHound node property, customdenyaces, which captures filtered deny ACEs from an AD object's nTSecurityDescriptor as individual SDDL strings. The property is emitted for ACL-backed AD objects only when qualifying deny ACEs remain after filtering.

This change also adds a shared LdapConfig.SkipDenyAces switch so SharpHound can disable collection of this property if it causes unexpected issues.

The change also disables DocFX builds by default on non-Windows environments to avoid unnecessary cross-platform build issues.

Motivation and Context

This adds auditing-only visibility into custom deny ACEs (no edge impact). It helps surface potentially interesting deny entries while excluding known noisy/default patterns such as Exchange-related denies, accidental deletion protection, and a small set of default AD deny ACEs.

This PR addresses: BED-8117

This PR relates to the following PRs:

  • BloodHound:
  • SharpHound:
  • SharpHoundEnterprise:

How Has This Been Tested?

Added and updated unit tests in test/unit covering:

  • qualifying deny ACE is emitted
  • Exchange trustee deny ACEs are skipped
  • deny ACEs under the Exchange configuration path are skipped
  • accidental deletion deny ACEs are skipped
  • default AD deny patterns are skipped
  • multiple qualifying ACEs are emitted as multiple list entries
  • deterministic ordering is preserved
  • SkipDenyAces suppresses emission of customdenyaces

I also updated existing property-reader tests affected by the LdapPropertyProcessor changes.

Screenshots (if appropriate):

N/A

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have met the contributing prerequisites
  • I have ensured that related documentation is up-to-date
    • Open API docs
    • Code comments
  • I have followed proper test practices
    • Added/updated tests to cover my changes
    • All new and existing tests passed

Summary by CodeRabbit

  • New Features

    • Extract and emit custom "deny" ACEs from object security descriptors with filtering for Exchange- and default-system patterns
    • Add a SkipDenyAces setting and expose it through LDAP configuration to disable deny-ACE enrichment
  • Chores

    • Conditional DocFX build disabled on non-Windows environments when unspecified
  • Tests

    • Added unit tests covering deny-ACE extraction, filtering, ordering, and the SkipDenyAces behavior

@JonasBK JonasBK self-assigned this Apr 23, 2026
@JonasBK JonasBK added the enhancement New feature or request label Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@JonasBK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 49 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 22 minutes and 49 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84deea08-1af7-41b2-81b5-cff0d496b0a7

📥 Commits

Reviewing files that changed from the base of the PR and between 686b781 and 820d80d.

📒 Files selected for processing (2)
  • src/CommonLib/Processors/LdapPropertyProcessor.cs
  • test/unit/LdapPropertyTests.cs

Walkthrough

Adds extraction and filtering of “custom deny” ACEs from object security descriptors, configurable via a new SkipDenyAces setting. Multiple LDAP property readers were converted to async to await deny-ACE enrichment; new ACL parsing and filtering logic excludes Exchange- and default-AD deny patterns.

Changes

Cohort / File(s) Summary
Build / Config & Constants
docfx/Docfx.csproj, src/CommonLib/Enums/DirectoryPaths.cs, src/CommonLib/LdapConfig.cs, src/CommonLib/WellKnownPrincipal.cs
Added OS-conditional MSBuild BuildDocFx default, ExchangeLocation LDAP constant, SkipDenyAces config property (and included in ToString), and EveryoneSid constant.
LDAP Interface & Utilities
src/CommonLib/ILdapUtils.cs, src/CommonLib/LdapUtils.cs
Added SkipDenyAces property surface on ILdapUtils and exposed via LdapUtils; updated local well-known SID comparisons to use EveryoneSid.
ACL Processing
src/CommonLib/Processors/ACLProcessor.cs
Introduced full custom deny-ACE extraction/filtering: parse DACL, resolve trustees, apply Exchange/domain and default-AD exclusion rules, serialize kept deny ACEs to SDDL, and helpers for ACE rehydration and cloning.
Property Processing & Async Conversion
src/CommonLib/Processors/LdapPropertyProcessor.cs
Integrated ACLProcessor instance; added AddCustomDenyAceProperty enrichment; converted multiple Read* property methods from synchronous to async Task<Dictionary<string, object>> to await deny-ACE enrichment and honor SkipDenyAces.
Tests & Mocks
test/unit/ACLProcessorTest.cs, test/unit/LdapPropertyTests.cs, test/unit/Facades/MockLdapUtils.cs
Added comprehensive tests for deny-ACE extraction/filtering and deterministic ordering; updated LdapProperty tests to async and added coverage for SkipDenyAces; MockLdapUtils now stores LdapConfig and exposes SkipDenyAces.

Sequence Diagram

sequenceDiagram
    participant LP as LdapPropertyProcessor
    participant ACLP as ACLProcessor
    participant LU as LdapUtils
    participant SD as RawSecurityDescriptor

    LP->>ACLP: AddCustomDenyAcesProperty(props, ntSecurityDescriptor, objectDomain, label, distinguishedName, isMSA)
    alt SkipDenyAces true or descriptor missing
        ACLP-->>LP: return (no enrichment)
    else Process deny ACEs
        ACLP->>SD: Parse DiscretionaryAcl from descriptor
        SD-->>ACLP: list of deny ACEs
        loop for each deny ACE
            ACLP->>LU: ResolveAccountName(trusteeSid)
            LU-->>ACLP: resolvedName / domain info
            ACLP->>ACLP: Apply filters (Exchange DN, Exchange trustee names per-domain cache, default-AD deny patterns, OU/container delete-tree exceptions)
            alt ACE kept
                ACLP->>ACLP: Rehydrate ACE into minimal ACL -> Serialize to SDDL fragment
            end
        end
        ACLP->>LP: props["customdenyaces"] = [SDDL fragments]
    end
    LP-->>LP: Return enriched property dictionary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibble through ACEs at dawn,

Skipping the weeds the admins have drawn,
Async hops make properties bloom,
Exchange shadows politely meet their doom,
A carrot of SDDL tidy and bright 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Custom Deny ACEs BED-8117' is partially related but not clearly descriptive; it lacks context about what the change does. Consider a more descriptive title like 'Add customdenyaces property for filtered deny ACEs from nTSecurityDescriptor' to better convey the main change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description covers all major template sections: a clear description of the feature, motivation/context with issue reference, comprehensive testing details, and appropriate checklist completion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch BED-8117-deny-aces

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/CommonLib/LdapConfig.cs (1)

16-16: Log the new switch with the rest of the LDAP config.

Line 16 adds a runtime collection switch, but ToString() omits it even though LDAP config changes are logged. Including it makes customdenyaces suppression easier to diagnose.

Proposed observability tweak
             sb.AppendLine($"AuthType: {AuthType.ToString()}");
             sb.AppendLine($"MaxConcurrentQueries: {MaxConcurrentQueries}");
+            sb.AppendLine($"SkipDenyAces: {SkipDenyAces}");
             if (!string.IsNullOrWhiteSpace(Username)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/LdapConfig.cs` at line 16, The ToString() for class LdapConfig
is missing the new SkipDenyAces property so runtime logs don't show the custom
deny-aces switch; update the LdapConfig.ToString() method to include the
SkipDenyAces value (use the same naming convention used for other settings—e.g.,
"SkipDenyAces" or "customdenyaces") so when an LdapConfig instance is logged it
prints the boolean state of SkipDenyAces along with the other properties.
src/CommonLib/ILdapUtils.cs (1)

160-162: Prefer a narrow read-only accessor for the new flag.

Line 162 exposes the full mutable LDAP config just so callers can read SkipDenyAces. Since LdapUtils.SetLdapConfig() rebuilds connection state, callers mutating the returned config could bypass those side effects. Prefer a narrower accessor or immutable snapshot.

Example narrower API
-        LdapConfig GetLdapConfig();
+        bool ShouldSkipDenyAces();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/ILdapUtils.cs` around lines 160 - 162, The interface currently
exposes a mutable LdapConfig via ILdapUtils.GetLdapConfig(), which lets callers
mutate config and bypass SetLdapConfig() side effects; replace this with a
narrow read-only accessor (for example add a bool SkipDenyAces { get; } or a
GetSkipDenyAces() method) or return an immutable snapshot type (e.g.,
ReadOnlyLdapConfig) instead of LdapConfig, update callers to read the specific
flag from the new accessor, and ensure SetLdapConfig() remains the only way to
change runtime config/connection state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CommonLib/Processors/ACLProcessor.cs`:
- Around line 914-923: The try/catch around RawSecurityDescriptor construction
in ACLProcessor should also catch ArgumentException (not just OverflowException)
because RawSecurityDescriptor(byte[], int) can throw ArgumentException for
malformed self-relative SECURITY_DESCRIPTOR bytes; update the catch block that
wraps new RawSecurityDescriptor(ntSecurityDescriptor, 0) to handle both
OverflowException and ArgumentException (or add a separate catch for
ArgumentException) and log the same warning (including objectName) and return
Array.Empty<string>() so malformed descriptors from LDAP don't propagate an
unhandled exception.
- Around line 995-1011: The Everyone DeleteChild deny check is too narrow (only
Label.Domain); update the condition in ACLProcessor.cs inside the "Filter
default Everyone Deny ACEs" block so DeleteChild denies from Everyone are
treated as default accidental-deletion for container-like parents as well (e.g.,
Label.Domain OR Label.OU OR Label.Container) instead of only Label.Domain —
modify the objectType check that guards ActiveDirectoryRights.DeleteChild to
include those additional Label values and keep the existing return true behavior
so such default parent DeleteChild denies are filtered out.

In `@test/unit/LdapPropertyTests.cs`:
- Around line 154-157: Add a positive control run before the existing assertion:
call LdapPropertyProcessor.ReadOUProperties with the same mock/ldapUtils under
default configuration (i.e., without SkipDenyAces enabled) and assert that the
returned dictionary contains the "customdenyaces" key; then run the existing
test path that enables SkipDenyAces and assert that "customdenyaces" is not
present. Use the same mock variable and LdapPropertyProcessor instance creation
pattern so you verify the property is emitted normally and only suppressed when
SkipDenyAces is applied.
- Around line 139-143: The test method
LDAPPropertyProcessor_ReadOUProperties_SkipsCustomDenyAces_WhenLdapConfigRequestsIt
constructs a SecurityIdentifier (Windows-only API) but lacks platform gating;
annotate the test to run only on Windows by replacing or supplementing the
[Fact] attribute with the Windows-only attributes used elsewhere (e.g., add
[SupportedOSPlatform("windows")] and use [WindowsOnlyFact] or equivalent) so the
SecurityIdentifier construction and related assertions are skipped on
non-Windows platforms.

---

Nitpick comments:
In `@src/CommonLib/ILdapUtils.cs`:
- Around line 160-162: The interface currently exposes a mutable LdapConfig via
ILdapUtils.GetLdapConfig(), which lets callers mutate config and bypass
SetLdapConfig() side effects; replace this with a narrow read-only accessor (for
example add a bool SkipDenyAces { get; } or a GetSkipDenyAces() method) or
return an immutable snapshot type (e.g., ReadOnlyLdapConfig) instead of
LdapConfig, update callers to read the specific flag from the new accessor, and
ensure SetLdapConfig() remains the only way to change runtime config/connection
state.

In `@src/CommonLib/LdapConfig.cs`:
- Line 16: The ToString() for class LdapConfig is missing the new SkipDenyAces
property so runtime logs don't show the custom deny-aces switch; update the
LdapConfig.ToString() method to include the SkipDenyAces value (use the same
naming convention used for other settings—e.g., "SkipDenyAces" or
"customdenyaces") so when an LdapConfig instance is logged it prints the boolean
state of SkipDenyAces along with the other properties.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 258ec52f-a3e7-470f-b1ac-bcebba1bd691

📥 Commits

Reviewing files that changed from the base of the PR and between aa69a02 and fd53dac.

📒 Files selected for processing (11)
  • docfx/Docfx.csproj
  • src/CommonLib/Enums/DirectoryPaths.cs
  • src/CommonLib/ILdapUtils.cs
  • src/CommonLib/LdapConfig.cs
  • src/CommonLib/LdapUtils.cs
  • src/CommonLib/Processors/ACLProcessor.cs
  • src/CommonLib/Processors/LdapPropertyProcessor.cs
  • src/CommonLib/WellKnownPrincipal.cs
  • test/unit/ACLProcessorTest.cs
  • test/unit/Facades/MockLdapUtils.cs
  • test/unit/LdapPropertyTests.cs

Comment thread src/CommonLib/Processors/ACLProcessor.cs
Comment thread src/CommonLib/Processors/ACLProcessor.cs
Comment thread test/unit/LdapPropertyTests.cs Outdated
Comment thread test/unit/LdapPropertyTests.cs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
test/unit/LdapPropertyTests.cs (1)

139-143: ⚠️ Potential issue | 🟠 Major

Gate this test as Windows-only.

Line 142 constructs a SecurityIdentifier, while similar tests in this file use [SupportedOSPlatform("windows")] and [WindowsOnlyFact]. This can still break non-Windows test runs or platform analyzer enforcement.

🛡️ Proposed fix
-        [Fact]
+        [SupportedOSPlatform("windows")]
+        [WindowsOnlyFact]
         public async Task LDAPPropertyProcessor_ReadOUProperties_SkipsCustomDenyAces_WhenLdapConfigRequestsIt()

Run this read-only check to confirm remaining ungated SecurityIdentifier test usage:

#!/bin/bash
set -euo pipefail

rg -n -C2 '\[Fact\]|\[Theory\]|\[WindowsOnlyFact\]|new SecurityIdentifier\(' --iglob '*.cs' test/unit
rg -n '<TreatWarningsAsErrors|EnableNETAnalyzers|AnalysisLevel|SupportedOSPlatform' --iglob '*.csproj' --iglob '*.props' --iglob '*.targets'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/LdapPropertyTests.cs` around lines 139 - 143, The test method
LDAPPropertyProcessor_ReadOUProperties_SkipsCustomDenyAces_WhenLdapConfigRequestsIt
creates a SecurityIdentifier which is Windows-only; mark this test with the
appropriate platform attributes (e.g., add [SupportedOSPlatform("windows")] and
replace [Fact] with [WindowsOnlyFact] or add [WindowsOnlyFact] alongside [Fact]
per project conventions) so the SecurityIdentifier construction is gated on
Windows; update the method declaration attributes accordingly to match other
tests in this file (refer to the method name and the SecurityIdentifier
instantiation) to prevent non-Windows runs or analyzer failures.
src/CommonLib/Processors/ACLProcessor.cs (1)

995-1011: ⚠️ Potential issue | 🟠 Major

Tighten the default Everyone deny filter.

These HasFlag checks suppress ACEs that contain the default bits plus additional custom deny rights, so customdenyaces can silently miss relevant ACEs. Also, the DeleteChild default still only filters domains, leaving accidental-deletion parent ACEs on OUs/containers to be emitted.

🛡️ Proposed fix
             // Filter default Everyone Deny ACEs
             if (principalSid.Equals(WellKnownPrincipal.EveryoneSid, StringComparison.OrdinalIgnoreCase)) {
                 if ((objectType is Label.OU or Label.Container) &&
-                    rights.HasFlag(ActiveDirectoryRights.Delete) &&
-                    rights.HasFlag(ActiveDirectoryRights.DeleteTree)) {
+                    HasOnlyRights(rights, ActiveDirectoryRights.Delete | ActiveDirectoryRights.DeleteTree)) {
                     return true;
                 }
 
                 if (isMSA &&
-                    rights.HasFlag(ActiveDirectoryRights.ExtendedRight) &&
+                    HasOnlyRights(rights, ActiveDirectoryRights.ExtendedRight) &&
                     objectAceType.Equals(new Guid(ACEGuids.UserForceChangePassword))) {
                     return true;
                 }
 
-                if (objectType == Label.Domain && rights.HasFlag(ActiveDirectoryRights.DeleteChild)) {
+                if ((objectType is Label.Domain or Label.OU or Label.Container or Label.Configuration) &&
+                    HasOnlyRights(rights, ActiveDirectoryRights.DeleteChild)) {
                     return true;
                 }
             }

Add this helper near the filtering helpers:

private static bool HasOnlyRights(ActiveDirectoryRights actual, ActiveDirectoryRights expected) {
    return (actual & expected) == expected && (actual & ~expected) == 0;
}

Based on learnings, properties added to dictionaries returned by methods in SharpHoundCommon may be consumed by dependent projects like SharpHound and SharpHoundEnterprise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/Processors/ACLProcessor.cs` around lines 995 - 1011, The
Everyone-deny filtering in ACLProcessor (the block that checks principalSid ==
WellKnownPrincipal.EveryoneSid and inspects objectType, rights, isMSA, and
objectAceType) currently uses HasFlag which allows extra custom deny bits to be
suppressed; add the proposed helper HasOnlyRights(ActiveDirectoryRights actual,
ActiveDirectoryRights expected) and replace the HasFlag checks with calls to
HasOnlyRights for the Delete/DeleteTree, ExtendedRight/UserForceChangePassword,
and DeleteChild checks so only ACEs that match exactly the default deny bits are
filtered; also broaden the DeleteChild default filter so it applies to OU and
Container objectType values (not just Domain) to prevent accidental suppression
of parent-deletion ACEs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CommonLib/Processors/LdapPropertyProcessor.cs`:
- Around line 670-683: The AddCustomDenyAceProperty method is passing the
distinguished name into both the 5th and 7th parameters of
_aclProcessor.AddCustomDenyAcesProperty (distinguishedName and objectName);
change the 7th argument to pass the object's actual name (e.g., sAMAccountName
retrieved via entry.TryGetStringProperty("sAMAccountName", out var objectName)
or the entry.TryGetSamAccountName helper if available) instead of using
distinguishedName, and handle the TryGetDistinguishedName return (use a safe
default like string.Empty if it returns false) so the 5th parameter never
unexpectedly becomes null. Ensure you update the call site in
AddCustomDenyAceProperty to pass the fetched objectName variable and not
distinguishedName.

---

Duplicate comments:
In `@src/CommonLib/Processors/ACLProcessor.cs`:
- Around line 995-1011: The Everyone-deny filtering in ACLProcessor (the block
that checks principalSid == WellKnownPrincipal.EveryoneSid and inspects
objectType, rights, isMSA, and objectAceType) currently uses HasFlag which
allows extra custom deny bits to be suppressed; add the proposed helper
HasOnlyRights(ActiveDirectoryRights actual, ActiveDirectoryRights expected) and
replace the HasFlag checks with calls to HasOnlyRights for the
Delete/DeleteTree, ExtendedRight/UserForceChangePassword, and DeleteChild checks
so only ACEs that match exactly the default deny bits are filtered; also broaden
the DeleteChild default filter so it applies to OU and Container objectType
values (not just Domain) to prevent accidental suppression of parent-deletion
ACEs.

In `@test/unit/LdapPropertyTests.cs`:
- Around line 139-143: The test method
LDAPPropertyProcessor_ReadOUProperties_SkipsCustomDenyAces_WhenLdapConfigRequestsIt
creates a SecurityIdentifier which is Windows-only; mark this test with the
appropriate platform attributes (e.g., add [SupportedOSPlatform("windows")] and
replace [Fact] with [WindowsOnlyFact] or add [WindowsOnlyFact] alongside [Fact]
per project conventions) so the SecurityIdentifier construction is gated on
Windows; update the method declaration attributes accordingly to match other
tests in this file (refer to the method name and the SecurityIdentifier
instantiation) to prevent non-Windows runs or analyzer failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c6a129b-8e11-46d8-a9b6-54653652f8bf

📥 Commits

Reviewing files that changed from the base of the PR and between fd53dac and 686b781.

📒 Files selected for processing (7)
  • src/CommonLib/ILdapUtils.cs
  • src/CommonLib/LdapConfig.cs
  • src/CommonLib/LdapUtils.cs
  • src/CommonLib/Processors/ACLProcessor.cs
  • src/CommonLib/Processors/LdapPropertyProcessor.cs
  • test/unit/Facades/MockLdapUtils.cs
  • test/unit/LdapPropertyTests.cs
✅ Files skipped from review due to trivial changes (1)
  • src/CommonLib/LdapConfig.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/CommonLib/ILdapUtils.cs

Comment thread src/CommonLib/Processors/LdapPropertyProcessor.cs
@JonasBK JonasBK marked this pull request as draft April 24, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant