Skip to content

bug(coverage): dynamic-group counts are always 0 #113

Closed

Description

@2000game

ct coverage reports no dynamic groups on an instance that has 70.

$ ct coverage --env prod
645 groups · 37 managed · 583 authored grants over 58 role instances

by type         total  managed  dynamic  unmanaged+grants
  Struktur         11       10        0                 1
  Merkmal         293        9        0                 3
  Flow             44        0        0                 0
  …every row reads 0…
$ ct coverage --env prod --json
"groups": { "total": 645, "managed": 37, "dynamic": 0, "managedDynamic": 0 }

Ground truth on the same host, same moment:

$ ct get raw "/dynamicgroups" --env prod | …count…
70

And 9 of those are under management by this config (skidscheckinopsmz, skidscheckinopskoblenz,
sintegrationmeeting, sflow2_5viewer, sflow0_6viewer, sflow0_6editor, sll_eqrm,
sll_koblenz, merkmal_alle_2_5_mz — each has a file in rulesets/), so managedDynamic: 0 is
wrong too.

A hand-rolled audit of the same instance yesterday found the 70 split as: Merkmal 48, Struktur 8,
AppModule 6, Flow 2, Community 2, Newsletter 1, others 3.

Why it matters

Auto-groups are the highest-consequence resource this tool manages — a ruleset silently grants
membership, and in this instance Struktur membership carries 33 type-level grants. "Which auto-groups
am I not managing?" is one of the main questions ct coverage exists to answer, and it currently
answers "none, everywhere" on a host with 70 of them. A zero is the one wrong answer that looks like
good news.

Guess at the cause

The group list is probably fetched without the dynamic flag being resolved — /groups does not mark
dynamic groups, so the count likely needs the /dynamicgroups id list (a single cheap request
returning a flat array of ids) intersected with the group set, the way an external audit has to do it.

Acceptance

  • dynamic per type sums to the /dynamicgroups count.
  • managedDynamic counts those also present in the state file.
  • Covered by a test with a fixture where the two sets only partially overlap.

Unrelated note while I was here: --json is clean on stdout with the catalog line on stderr — that
part is correct, redirecting 2>&1 was my own mistake.

Activity

  1. 2000game commented on Aug 14, 2026

    @2000game
    MemberAuthor

    Root cause found while chasing a different symptom — see #124.

    GET /dynamicgroups returns an array of bare ids, not objects:

    [159, 1698, 1704, 1707, 1710, 1713, 1740, 1748, 1753]

    Any reader doing Number(row.id ?? row.groupId) gets NaN for every row, so the id set is empty on every host. That is exactly a dynamic column of 0 on a host with 70 dynamic groups, and it is also why ct refresh reports "not a dynamic group" for groups that are one.

    Both should be fixable at whatever parses this endpoint:

    const id = typeof row === "number" ? row : Number(row?.id ?? row?.groupId);

    Verified live on two hosts, CT 3.135.2, ct-cli 1.7.0.

  2. removed
    triageUnsorted intake — decide in the weekly sweep
    on Aug 17, 2026
  3. added a commit that references this issue on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions