Severity: MEDIUM (evidence integrity / forensic-conclusion corruption via the admin)
AuditGroupAdmin declares no readonly_fields, no exclude, and no fields/fieldsets, so Django renders every editable model field on the change form: name, slug, group_ref, divergent_message_count, and notes. Two of those are not operator-owned metadata — group_ref is the group's forensic identity key, and divergent_message_count is a derived rollup — yet both are freely writable and a Save persists whatever an operator types with no audit trail.
Location
forensics/admin.py:33-37:
@admin.register(AuditGroup)
class AuditGroupAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "group_ref", "created_at", "updated_at")
search_fields = ("name", "slug", "group_ref", "notes")
prepopulated_fields = {"slug": ("name",)}
Model fields (forensics/models.py:13-18):
class AuditGroup(models.Model):
...
group_ref = models.CharField(max_length=512, blank=True, db_index=True)
divergent_message_count = models.PositiveIntegerField(default=0)
notes = models.TextField(blank=True)
(created_at/updated_at are auto fields and are auto-excluded from the form; everything else renders as an editable widget.)
Failure scenario
1. Editing group_ref splits a group's evidence. A staff operator opens a group and changes group_ref (e.g. "correcting" a perceived typo, or pasting the wrong value) and Saves. Ingestion resolves a group by exact group_ref:
# forensics/ingest.py:1032-1035 (group_for_ref)
def group_for_ref(group_ref: str) -> AuditGroup:
existing = AuditGroup.objects.filter(group_ref=group_ref).first()
if existing is not None:
return existing
... # else mint a brand-new AuditGroup
The next upload that references the original group_ref no longer matches the mutated row, so ingest mints a new AuditGroup. The group's evidence is now split across two workspace rows — exactly the identity-fragmentation hazard the app treats as a real forensic problem elsewhere (cf. the case-variant/slug-merge issues #195 / #236 and the closed "Avoid merging distinct long group_ref values through truncated slugs").
2. Editing divergent_message_count fabricates the divergence badge. This column is a derived rollup, recomputed only on the next upload for the group (see #257). An operator can type any integer and Save; the group-header divergence badge then reports an operator-fabricated count — with no indication it is not machine-derived — until the next upload recomputes it. Between uploads the UI presents fabricated forensic data as if it were computed evidence.
Why this is not a duplicate
The existing "editable evidence in the admin" cluster is explicitly scoped to other tables and does not touch AuditGroup:
The only existing AuditGroupAdmin items are #158 (show_full_result_count, performance) and #175 (a rebuild-command group-resolution bug) — neither concerns editable derived/identity columns on the change form. So the AuditGroup change form is an uncovered gap in that cluster, and it is arguably the highest-value one to close because group_ref is a group's identity key.
Suggested fix
Mark the identity/derived columns read-only on the change form, mirroring the treatment UploadTokenAdmin / AuditFileAdmin already apply to their evidence columns:
@admin.register(AuditGroup)
class AuditGroupAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "group_ref", "divergent_message_count", "created_at", "updated_at")
search_fields = ("name", "slug", "group_ref", "notes")
prepopulated_fields = {"slug": ("name",)}
readonly_fields = ("group_ref", "divergent_message_count", "created_at", "updated_at")
name, slug, and notes remain editable (they are legitimately operator-owned); group_ref and divergent_message_count become read-only so a Save can no longer split a group or fabricate its divergence rollup.
Severity: MEDIUM (evidence integrity / forensic-conclusion corruption via the admin)
AuditGroupAdmindeclares noreadonly_fields, noexclude, and nofields/fieldsets, so Django renders every editable model field on the change form:name,slug,group_ref,divergent_message_count, andnotes. Two of those are not operator-owned metadata —group_refis the group's forensic identity key, anddivergent_message_countis a derived rollup — yet both are freely writable and a Save persists whatever an operator types with no audit trail.Location
forensics/admin.py:33-37:Model fields (
forensics/models.py:13-18):(
created_at/updated_atare auto fields and are auto-excluded from the form; everything else renders as an editable widget.)Failure scenario
1. Editing
group_refsplits a group's evidence. A staff operator opens a group and changesgroup_ref(e.g. "correcting" a perceived typo, or pasting the wrong value) and Saves. Ingestion resolves a group by exactgroup_ref:The next upload that references the original
group_refno longer matches the mutated row, so ingest mints a newAuditGroup. The group's evidence is now split across two workspace rows — exactly the identity-fragmentation hazard the app treats as a real forensic problem elsewhere (cf. the case-variant/slug-merge issues #195 / #236 and the closed "Avoid merging distinct long group_ref values through truncated slugs").2. Editing
divergent_message_countfabricates the divergence badge. This column is a derived rollup, recomputed only on the next upload for the group (see #257). An operator can type any integer and Save; the group-header divergence badge then reports an operator-fabricated count — with no indication it is not machine-derived — until the next upload recomputes it. Between uploads the UI presents fabricated forensic data as if it were computed evidence.Why this is not a duplicate
The existing "editable evidence in the admin" cluster is explicitly scoped to other tables and does not touch
AuditGroup:AuditEvent,AuditFile) leave every extracted/provenance column editable — a Save silently rewrites primary forensic evidence #276 names only the raw-evidence tablesAuditEventandAuditFile.AnalysisRun.report_json.The only existing
AuditGroupAdminitems are #158 (show_full_result_count, performance) and #175 (a rebuild-command group-resolution bug) — neither concerns editable derived/identity columns on the change form. So theAuditGroupchange form is an uncovered gap in that cluster, and it is arguably the highest-value one to close becausegroup_refis a group's identity key.Suggested fix
Mark the identity/derived columns read-only on the change form, mirroring the treatment
UploadTokenAdmin/AuditFileAdminalready apply to their evidence columns:name,slug, andnotesremain editable (they are legitimately operator-owned);group_refanddivergent_message_countbecome read-only so a Save can no longer split a group or fabricate its divergence rollup.