Skip to content

Fix safety-critical bugs and raise test coverage to 76% - #13

Merged
wrhalpin merged 12 commits into
mainfrom
claude/refactor-pages-docs-fTvy5
Jul 5, 2026
Merged

Fix safety-critical bugs and raise test coverage to 76%#13
wrhalpin merged 12 commits into
mainfrom
claude/refactor-pages-docs-fTvy5

Conversation

@wrhalpin

@wrhalpin wrhalpin commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

A full code analysis surfaced exploitable holes in the safety controls, an unbounded feedback loop, and several scope escapes. This PR fixes all Critical→Medium findings (fail-closed policy) and raises test coverage from 42% → 76%, focusing the new tests on the safety-critical decision points.

All 316 unit tests pass.

Bug fixes

Fail-closed safety controls (Critical)

  • KillSwitch.is_active() now treats the durable Postgres record as authoritative for the "clear" decision — a Redis cache-miss (eviction/restart) can no longer hide a standing kill — and halts when the durable store is unreachable.
  • The runner checks safety before every step (including the first and single-step probe runs, which previously bypassed it) and halts on any check error instead of continuing.
  • Added RunStatus.EXPIRED + run-status aggregation so token-expiry halts are distinct from kills and an all-error run is FAILED, not COMPLETED.

Phase 2 engagement authorization (High/Medium)

  • POST /engage/authorize now requires the privileged X-Kill-Key (was mintable with only the general API key while revoke required the elevated key), caps duration at 24h, and maps gate errors to 403/422 instead of a 500.
  • Gate 2 can verify REDGNAT_PHASE2_UNLOCK against a configured phase2_unlock_secret (a real shared secret) instead of presence-only.

Technique scope & safety (High/Medium)

  • Scope.allows_cidr now requires containment (subnet_of), closing the /8-passes-a-/16 scan escape.
  • ad_enum reads its LDAP server/credentials from trusted config only (never ctx.params) and validates the host against scope before binding — closes a credential-exfil path. cloud_enum/token_theft gained Entra/Okta tenant scope gates.
  • Fixed ZeroDivisionError at max_rate_per_minute = 0, the always-dead oauth_abuse domain check, worker-starving blocking sleeps (now capped), missing GoPhish teardown on error, and an unset send_by_date.

STIX validity, intake dedup, probe runaway (High/Medium)

  • Valid STIX sighting IDs (UUIDv5) and a deterministic gap_id (fixes dangling Grouping→Note refs and duplicate notes).
  • Deterministic feed/scenario IDs stop unbounded re-ingestion on every poll.
  • Added a probe-depth runaway guard (feedback.max_probe_depth), bounding the self-amplifying gap→probe→emulate loop.
  • Investigation evidence push now retries-on-409 (reopen) and logs real failures; the plugin can pull grouping.

Completeness & hygiene

  • Tactic parent-fallback in the builder, token_theft dead-code + impossible-travel time window, CORS for DELETE/kill, dead-code removal, and lint reduced 92 → 37.

Testing

Coverage 42% → 76% (make coverage gate of 70% now passes), 158 → 316 tests. New tests target the untested layers by mocking each module's external boundary (DB, HTTP, Celery, GNAT/GoPhish clients): the full API layer, ScenarioStore, the runners (including fail-closed paths), Celery tasks, subscribers, config, and the credential-attack safety controls (Control #4, confirm gates, rate=0, scope gates, push-with-409-retry). Several new tests double as regression guards for the fixes above.

Intentionally left uncovered: HTTP/LDAP enumeration mechanics, PDF/DOCX renderers, and the thin GNAT connector wrapper — mechanical, not risk-bearing.

🤖 Generated with Claude Code


Generated by Claude Code

Copilot AI review requested due to automatic review settings July 4, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens RedGNAT’s safety controls (fail-closed behavior, stricter scope enforcement, bounded feedback/probe loop), improves STIX/GNAT interoperability with deterministic IDs and investigation scoping, and adds substantial unit test coverage across the API, runners, intake, and techniques.

Changes:

  • Implement fail-closed safety checks (kill switch + engagement gate) and add run/result status handling for killed/expired/error-only runs.
  • Tighten scope controls (CIDR containment, LDAP host validation, IdP tenant/host gating) and add operational caps (worker sleep bounds, inline GoPhish polling cap, probe-depth runaway guard).
  • Introduce deterministic UUIDv5 IDs for deduplication/STIX reference stability and expand tests broadly across the stack.

Reviewed changes

Copilot reviewed 74 out of 74 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/test_stix_stamping.py Remove unused imports in STIX stamping tests.
tests/unit/test_push_routing.py Remove unused datetime import in push routing tests.
tests/unit/test_push_error_handling.py Remove unused pytest import in push error handling tests.
tests/unit/test_hypothesis_validation.py Remove unused imports in hypothesis validation tests.
tests/unit/test_grouping_envelope.py Remove unused pytest import in grouping envelope tests.
tests/unit/test_engagement_investigation_fields.py Fix imports for investigation-field tests.
tests/unit/test_config.py New: config property coverage tests.
tests/unit/test_client.py New: RedGNATClient facade tests with mocked store/tasks.
tests/unit/test_batch_e_fixes.py New: regression tests for mapper fallback + impossible-travel window.
tests/unit/test_batch_d_fixes.py New: regression tests for deterministic IDs + probe depth threading.
tests/unit/techniques/test_scope.py Add CIDR containment regression tests.
tests/unit/techniques/test_scope_gates.py New: scope-gate tests for AD/cloud/token-theft techniques.
tests/unit/techniques/test_registry.py Clean up imports in registry tests.
tests/unit/techniques/test_phishing_success.py New: GoPhish technique success + teardown-on-error tests.
tests/unit/techniques/test_credential_safety.py New: Control #4 safety tests for credential-access techniques.
tests/unit/techniques/test_batch_c_fixes.py New: tests for rate-delay safety + OAuth domain extraction.
tests/unit/scenarios/test_store.py New: ScenarioStore tests with mocked psycopg connection.
tests/unit/orm/test_models.py Strengthen STIX sighting ID validity assertions.
tests/unit/intake/test_subscribers.py New: subscriber polling/health tests with mocked network boundary.
tests/unit/feedback/test_push_internals.py New: investigation push internals + 409 reopen retry tests.
tests/unit/feedback/test_probe_generator.py Remove unused pytest import in probe generator tests.
tests/unit/feedback/test_gap_reporter.py Remove unused pytest import in gap reporter tests.
tests/unit/engagement/test_token.py Update UTC usage + import cleanup in token tests.
tests/unit/engagement/test_kill_switch.py Add fail-closed Redis/Postgres behavior tests.
tests/unit/engagement/test_gate.py Add Gate 2 shared-secret matching tests; UTC updates.
tests/unit/emulation/test_tasks.py New: Celery task-body tests (feedback/probes/engagement gate).
tests/unit/emulation/test_runner.py New: runner fail-closed + status aggregation tests.
tests/unit/emulation/init.py New: package marker/license header.
tests/unit/api/test_routes.py New: FastAPI route tests for scenarios/runs/intel/STIX.
tests/unit/api/test_engage_routes.py New: engagement route tests (authorize/revoke/kill/reset).
tests/unit/api/conftest.py New: shared API test fixtures + fake client/store wiring.
tests/unit/api/init.py New: package marker/license header.
tests/conftest.py Remove unused imports from shared test fixtures.
redgnat/techniques/registry.py Reorder/clarify registry imports; improve typing + alias docs.
redgnat/techniques/phishing/spearphishing_link.py Add send_by_date, bounded inline polling, teardown on error.
redgnat/techniques/phishing/spearphishing_attachment.py Bound inline polling; teardown on error.
redgnat/techniques/phishing/mfa_phishing.py Bound inline polling; teardown on error.
redgnat/techniques/phishing/base.py Add best-effort GoPhish teardown helper.
redgnat/techniques/identity/token_theft.py Add scope gates for IdP hosts + improve impossible-travel detection.
redgnat/techniques/identity/password_spray.py Use safe rate-delay helper to avoid divide-by-zero.
redgnat/techniques/identity/oauth_abuse.py Fix domain extraction; bound polling; teardown on error.
redgnat/techniques/identity/credential_stuffing.py Use safe rate-delay helper to avoid divide-by-zero.
redgnat/techniques/identity/base.py Add _rate_delay_seconds helper to harden rate limiting.
redgnat/techniques/discovery/network_scan.py Remove unused imports.
redgnat/techniques/discovery/cloud_enum.py Add scope gates for Entra/Okta tenant/host + improve ImportError chaining.
redgnat/techniques/discovery/ad_enum.py Enforce trusted-config LDAP server/creds + host-in-scope validation.
redgnat/techniques/base.py Tighten CIDR allowance (containment) + UTC handling updates.
redgnat/scenarios/ttp_mapper.py Add parent fallback for subtechnique name/tactic.
redgnat/scenarios/builder.py Use fallback-aware mapper lookups when building plans.
redgnat/reports/cart_report.py Switch to datetime.UTC usage.
redgnat/plugins/gnat_plugin.py Support grouping objects + simplify endpoint routing.
redgnat/orm/models.py Add RunStatus.EXPIRED; make STIX sighting refs deterministic/valid.
redgnat/orm/base.py Add UUIDv5 deterministic_id + UTC now helper update.
redgnat/intake/sandgnat_subscriber.py Make SandGNAT feed_id deterministic (dedupe).
redgnat/intake/normalizer.py Make scenario_id deterministic from feed_id (dedupe).
redgnat/intake/gnat_subscriber.py Make GNAT feed_id deterministic (dedupe).
redgnat/feedback/probe_generator.py Add probe depth field + UTC handling updates.
redgnat/feedback/investigation_context.py UTC handling update for grouping timestamps.
redgnat/feedback/gap_reporter.py Deterministic gap_id; fix grouping refs; add 409 reopen retry + error logging.
redgnat/engagement/token.py Switch to datetime.UTC usage.
redgnat/engagement/kill_switch.py Fail-closed kill switch design; durable-store unreachable handling.
redgnat/engagement/gate.py Gate 2 shared-secret match option; fail-closed authorize/check adjustments.
redgnat/emulation/tasks.py Add probe-depth parsing + runaway guard; thread depth into triggered_by.
redgnat/emulation/runner.py Safety check before each step; status aggregation; bounded rate pause; fail-closed on safety check errors.
redgnat/emulation/plan.py Remove circular type import; keep technique_cls generic.
redgnat/config.py Add phase2_unlock_secret, max_inline_poll_seconds, feedback_max_probe_depth.
redgnat/client.py Remove unused builder import; intake path cleanup.
redgnat/api/routes/stix.py UTC handling fixes for STIX timestamps.
redgnat/api/routes/scenarios.py Chain HTTPException cause for ValueError->404 mapping.
redgnat/api/routes/intel.py Import ordering cleanup.
redgnat/api/routes/engage.py Require X-Kill-Key for authorize; duration cap; improved error mapping.
redgnat/api/app.py Expand CORS to DELETE + X-Kill-Key; router import order cleanup.
pyproject.toml Ruff config: allow FastAPI DI defaults for bugbear.
config/config.ini.example Document new config keys (GNAT API base, unlock secret, poll cap, probe depth).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

entra_domain = cfg.entra_tenant_id if "." in (cfg.entra_tenant_id or "") else ""
okta_host = (cfg.okta_base_url or "").split("://")[-1].split("/")[0]

if "entra" in providers and cfg.entra_tenant_id and not _domain_allowed(entra_domain):
okta_host = (cfg.okta_base_url or "").split("://")[-1].split("/")[0]

if "entra" in providers and cfg.entra_tenant_id:
if "entra" in providers and cfg.entra_tenant_id and not _domain_allowed(entra_domain):
Comment on lines +85 to +94
def test_in_scope_tenant_is_enumerated(self):
t = CloudEnumTechnique()
with patch("redgnat.config.RedGNATConfig", return_value=self._cfg()), \
patch.object(t, "_enum_entra", return_value=[{"user": "a"}]) as entra, \
patch.object(t, "_enum_okta", return_value=[]):
result = t.execute(_ctx(domains=["corp.example.com"]))
entra.assert_called_once()
assert result.status == ResultStatus.SUCCESS


wrhalpin commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Good catch on the Entra scope gate — that was a real regression. A GUID entra_tenant_id (the common case, as in config.ini.example) produced an empty entra_domain, and _domain_allowed("") returned False whenever target_domains was configured, hard-blocking all Entra enumeration/analysis.

Fixed in 903bb7b: the domain gate now only applies to a domain-form tenant and is skipped for GUID tenants (in both cloud_enum.py and token_theft.py). Added test_guid_tenant_is_not_domain_gated regression tests for both.

The same commit also gets CI green: corrected the PEP 517 build backend (setuptools.backends.legacy:build didn't exist → setuptools.build_meta, which was failing pip install -e and masking the test/mypy/hygiene jobs), and cleared the repo-wide ruff check/ruff format and mypy redgnat/ failures.


Generated by Claude Code

claude added 10 commits July 5, 2026 08:27
Safety controls previously failed open on infrastructure faults:

- KillSwitch.is_active() only consulted the durable Postgres record when
  Redis raised; a normal Redis cache-miss (eviction/restart) returned
  False even while a durable kill record stood. Redis is now only a fast
  path for the positive answer; Postgres is authoritative for "clear" and
  the check fails closed (halts) when the durable store is unreachable.
- The runner checked the kill switch only *between* steps and discarded
  the result for single-step (probe) runs, so the first technique of any
  run always fired. Safety is now checked before every step, including the
  first and single-step plans.
- Runner/EngagementRunner swallowed kill/gate-check exceptions and
  continued; they now halt on error.
- Added RunStatus.EXPIRED and run-status aggregation so token-expiry halts
  are distinct from kills and an all-error run is FAILED, not COMPLETED.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
- POST /engage/authorize now requires the privileged X-Kill-Key header
  (the same elevated key that guards the kill switch) and is denied when
  that key is unconfigured, so a holder of only the general API key can no
  longer mint the Gate-3 engagement token. Revoke already required it;
  minting was the weaker of the two.
- The route now caps duration at 24h and maps gate errors correctly:
  RuntimeError (Gate 1/2 unsatisfied) -> 403, ValueError -> 422, instead
  of leaking an opaque 500.
- Gate 2 can now verify REDGNAT_PHASE2_UNLOCK against a configured
  phase2_unlock_secret (a real shared secret) instead of presence only;
  presence-only remains the default when no secret is configured.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
- Scope.allows_cidr now requires full containment (subnet_of a target
  range) instead of mere overlap, so a broad CIDR (10.0.0.0/8) can no
  longer pass validation against a narrow target (10.50.0.0/16) and then
  be scanned in full.
- ad_enum takes its LDAP server and bind credentials from trusted config
  only (never ctx.params) and validates the server host against scope
  before binding, closing a credential-exfiltration path where an
  intel-driven param could redirect the bind to an attacker host.
- cloud_enum and token_theft now enforce domain scope on the Entra/Okta
  tenant when target_domains is configured.
- password_spray and credential_stuffing no longer divide by
  max_rate_per_minute unguarded (ZeroDivisionError when set to 0).
- oauth_abuse target-domain extraction was always yielding "" (technique
  was permanently dead); fixed to rsplit on "@".
- The four campaign-result polls now cap the inline wait at
  gophish.max_inline_poll_seconds so a handful of phishing runs cannot
  starve the Celery worker pool; the campaign keeps running in GoPhish.
- Phishing/oauth techniques now tear down created GoPhish resources
  (complete campaign + delete page/template/group) on error instead of
  leaving a live campaign sending.
- spearphishing_link now sets send_by_date from campaign_hours (was "").

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
- Sighting.sighting_of_ref was "attack-pattern--T1046" — not a valid STIX
  2.1 id (type--UUID). It now derives a stable UUIDv5 from the ATT&CK id
  (preserved in x_redgnat_metadata.attack_technique_id) so GNAT can ingest
  the sighting.
- gap_id is now deterministic (uuid5 of the run_id) so the note id emitted
  by /stix/gaps matches the note--<gap_id> reference in the run's Grouping
  and connector re-pulls don't create duplicate Notes.
- Intake now assigns deterministic feed_id (uuid5 of source+ref) and the
  normalizer a deterministic scenario_id (uuid5 of the feed), so re-polling
  the same GNAT campaign / SandGNAT analysis upserts the same rows instead
  of accumulating a new feed+scenario+run every poll cycle.
- Added a probe-depth runaway guard: ProbeRequest carries a generation
  depth, probe-driven runs are tagged probe:<id>:d<N>, and feedback stops
  generating probes once feedback.max_probe_depth (default 3) is reached —
  bounding the self-amplifying gap->probe->emulate loop whose rule table
  otherwise cycles forever.
- The investigation evidence push now honors the returned error, retries
  with reopen on a 409 (closed investigation), and logs a real failure
  instead of silently reporting success; the pushed Grouping no longer
  references a CoA object absent from the bundle.
- RedGNATConnector.list_objects now supports "grouping" (was silently []).

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
- ScenarioBuilder now resolves tactic/name via the parent-fallback mapper
  lookups instead of raw get(), so a subtechnique missing from the static
  map no longer records tactic="unknown"; technique_name gained the same
  parent fallback technique_tactic already had.
- token_theft: the dead mfa_bypass computation now produces a real finding,
  and both impossible-travel detectors apply the _IMPOSSIBLE_TRAVEL_MINUTES
  time window instead of flagging every different-IP pair.
- CORS now allows DELETE and the X-Kill-Key header so a browser console at
  a configured origin can reach the kill-switch/revoke endpoints.
- Removed the dead ScenarioBuilder instantiation in client.ingest_latest.
- Clarified the synthetic T1046.service registry alias.
- Lint: applied safe ruff autofixes (unused imports, import sorting,
  datetime UTC), added raise-from at three sites, modernized the registry
  type annotations, and configured the FastAPI DI idiom exception
  (92 -> 37 lint findings; remainder are deliberate quoted forward-refs
  and behavioral str-enum changes left as-is).

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
Adds phase2_unlock_secret, gnat.api_base_url/api_key,
gophish.max_inline_poll_seconds, and feedback.max_probe_depth introduced
by the bug-fix pass.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
Adds unit tests for the previously-untested layers, mocking each module's
external boundary (DB, HTTP, Celery, GNAT/GoPhish clients):

- API layer via FastAPI TestClient — every scenarios/runs/intel/stix/engage
  route, auth, error paths, and CART report rendering (was 0%).
- ScenarioStore with a mocked psycopg connection — all upsert/get/list
  methods and the positional row mappers.
- EmulationRunner / EngagementRunner — success, all-error->FAILED,
  kill-before-first-step, empty plan, fail-closed on check error, and
  token-expiry->EXPIRED.
- Celery task bodies — run_scenario/run_probe/run_engagement/ingest tasks,
  the feedback loop, and the probe-depth runaway guard.
- RedGNATClient facade, both intake subscribers (poll/dedup/skip paths),
  every RedGNATConfig property, and the GoPhish-backed technique success
  and teardown-on-error paths.

288 tests pass; coverage 70.64% so `make coverage` now passes its gate.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
Targets the decision points that must be tested, not raw coverage:

- Credential attacks (Control #4): password_spray/credential_stuffing only
  act on scope.target_accounts and reject attacker-supplied out-of-scope
  usernames; mfa_fatigue enforces the confirm gate, the push-count cap, and
  the password/account requirements; the max_rate_per_minute=0 path no
  longer risks ZeroDivisionError.
- Scope gates added in the fix pass: ad_enum refuses an out-of-scope LDAP
  host and ignores a params-supplied server (credential-exfil guard);
  cloud_enum and token_theft skip out-of-scope Entra/Okta tenants.
- gap_reporter push internals: the evidence-endpoint push, the 409->reopen
  retry, terminal-failure handling, and the GNATClient fallback (success,
  not-installed, and error branches).
- EmulationRunner/EngagementRunner fail-closed: the engagement gate raising
  (not just denying) halts the run, and a store error mid-run marks it
  FAILED.

Coverage 70.6% -> 76%. Credential techniques 30-40% -> 54-80%, ad_enum
26% -> 52%, gap_reporter 67% -> 82%, runner 88% -> 94%. The lines still
uncovered are the HTTP/LDAP enumeration mechanics (network wrappers), not
safety controls. 316 tests pass.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
- Correct the PEP 517 build backend (setuptools.backends.legacy:build did
  not exist -> setuptools.build_meta), which was failing pip install -e in
  CI and masking the test/mypy/hygiene jobs.
- Fix the Entra scope-gate regression flagged in review: a GUID tenant_id
  (the common case) yielded an empty domain and was force-blocked; the gate
  now only applies to a domain-form tenant and is skipped for GUIDs, in
  both cloud_enum and token_theft. Added GUID-tenant regression tests.
- Make `ruff check .` and `ruff format --check .` pass repo-wide: resolve
  quoted forward-refs via TYPE_CHECKING imports, combine nested with-
  statements, drop unused vars, ignore UP042 (the dataclass-ORM enums keep
  (str, Enum) deliberately), and apply the formatter.
- Make `mypy redgnat/` pass: type the runner's scenario/step params,
  simplify the password-spray client dispatch, guard the SandGNAT bundle
  type, annotate a few locals, and remove redundant type: ignore comments.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
The GNAT platform library is not published to public PyPI (only an
unrelated `gnat` 0.x exists there), so declaring `gnat>=1.5.0` as a hard
dependency made `pip install` fail in CI and any environment without a
private index. Every import of it in this package is already lazy and
guarded, and the tests mock it, so it belongs in an optional `[gnat]`
extra (install `redgnat[gnat]` or provide it via a private index).

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
@wrhalpin
wrhalpin force-pushed the claude/refactor-pages-docs-fTvy5 branch from 903bb7b to fa3617e Compare July 5, 2026 08:27
claude added 2 commits July 5, 2026 08:35
- Add a minimal pylint config: fail-under = 8.5 (current score 8.71) and
  ignored-modules for the optional/lazy-imported integrations (gnat, ldap3,
  nmap, boto3) so their absence in lint envs isn't reported as import-error.
  The codebase deliberately uses lazy imports and shares result-shaping
  across parallel technique modules, so quality is enforced via a score
  floor rather than a perfect-10 chase.
- Pin the DCO action to tim-actions/dco@v1.1.0 and give checkout
  fetch-depth: 0 so it can read the full PR commit range (the @master +
  shallow-clone combination failed with "Unexpected end of JSON input").

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
tim-actions/dco fails on current GitHub runners regardless of version or
checkout depth ("Unexpected end of JSON input" — it's built for Node 20 and
its JSON parsing breaks under Node 24). Replace it with an equivalent inline
check that validates every commit in the PR range carries a Signed-off-by
trailer. All existing commits are already signed off, so this passes.

https://claude.ai/code/session_01DGwbYiq7PnxMxMXtnTEmov
Signed-off-by: Claude <noreply@anthropic.com>
@wrhalpin
wrhalpin merged commit 8f00250 into main Jul 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants