Skip to content

Commit 4714d25

Browse files
auditlog: switch to pghistory
1 parent e1eef7c commit 4714d25

15 files changed

Lines changed: 230 additions & 540 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,12 @@ name: Integration tests
22

33
on:
44
workflow_call:
5-
inputs:
6-
auditlog_type:
7-
type: string
8-
default: "django-auditlog"
95

106
jobs:
117
integration_tests:
128
# run tests with docker compose
139
name: User Interface Tests
1410
runs-on: ubuntu-latest
15-
env:
16-
AUDITLOG_TYPE: ${{ inputs.auditlog_type }}
1711
strategy:
1812
matrix:
1913
test-case: [

.github/workflows/rest-framework-tests.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@ on:
66
platform:
77
type: string
88
default: "linux/amd64"
9-
auditlog_type:
10-
type: string
11-
default: "django-auditlog"
129

1310
jobs:
1411
unit_tests:
1512
name: Rest Framework Unit Tests
1613
runs-on: ${{ inputs.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
17-
env:
18-
AUDITLOG_TYPE: ${{ inputs.auditlog_type }}
1914

2015
strategy:
2116
matrix:

.github/workflows/unit-tests.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,18 @@ jobs:
2525
strategy:
2626
matrix:
2727
platform: ['linux/amd64', 'linux/arm64']
28-
auditlog_type: ['django-auditlog', 'django-pghistory']
2928
fail-fast: false
3029
needs: build-docker-containers
3130
uses: ./.github/workflows/rest-framework-tests.yml
3231
secrets: inherit
3332
with:
3433
platform: ${{ matrix.platform}}
35-
auditlog_type: ${{ matrix.auditlog_type }}
3634

3735
# only run integration tests for linux/amd64 (default)
3836
test-user-interface:
3937
needs: build-docker-containers
4038
uses: ./.github/workflows/integration-tests.yml
4139
secrets: inherit
42-
strategy:
43-
matrix:
44-
auditlog_type: ['django-auditlog', 'django-pghistory']
45-
fail-fast: false
46-
with:
47-
auditlog_type: ${{ matrix.auditlog_type }}
4840

4941
# only run k8s tests for linux/amd64 (default)
5042
test-k8s:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: "Upgrading to DefectDojo Version 2.53.x"
3+
toc_hide: true
4+
weight: -20251101
5+
description: Removal of django-auditlog and exclusive use of django-pghistory for audit logging.
6+
---
7+
8+
## Breaking Change: Removal of django-auditlog
9+
10+
Starting with DefectDojo 2.53, `django-auditlog` support has been removed in favour of `django-pghistory`.
11+
This is designed to be a backwards compatible change, unless:
12+
- You're querying the database directly for auditlog events, or,
13+
- You've set the `DD_AUDITLOG_TYPE` environment variable (or `AUDITLOG_TYPE` settings field)
14+
15+
### Required Actions
16+
17+
If you're using `DD_AUDITLOG_TYPE`, remove it from your configuration/environment.
18+
19+
### Existing Records Preserved
20+
21+
Historical audit log entries stored in the `auditlog_logentry` table will continue to be displayed in the action history view for backward compatibility. No data migration is required.
22+
23+
### Benefits of django-pghistory
24+
25+
The switch to `django-pghistory` provides several advantages:
26+
27+
- **Better performance**: Database-level triggers reduce overhead compared to Django signal-based auditing
28+
- **More features**: Enhanced context tracking and better support for complex queries
29+
- **Better data integrity**: PostgreSQL-native implementation ensures consistency
30+
31+
### Migration Notes
32+
33+
- A one-time data migration will take place to populate the `django-pghistory` tables with the initial snapshot of the tracked models.
34+
35+
---
36+
37+
Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.53.0) for the complete contents of this release.
38+

dojo/apps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ def ready(self):
9191
# Configure audit system after all models are loaded
9292
# This must be done in ready() to avoid "Models aren't loaded yet" errors
9393
# Note: pghistory models are registered here (no database access), but trigger
94-
# enabling is handled via management command to avoid database access warnings
94+
# enabling is handled in the entrpoint script to avoid database access warnings
95+
# during startup
9596
register_django_pghistory_models()
9697
configure_audit_system()
9798

dojo/auditlog.py

Lines changed: 54 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""
22
Audit logging configuration for DefectDojo.
33
4-
This module handles conditional registration of models with either django-auditlog
5-
or django-pghistory based on the DD_AUDITLOG_TYPE setting.
4+
This module handles registration of models with django-pghistory.
5+
django-auditlog support has been removed.
66
"""
7-
import contextlib
87
import logging
8+
import os
99
import sys
1010

1111
import pghistory
@@ -80,13 +80,6 @@ def _flush_models_in_batches(models_to_flush, timestamp_field: str, retention_pe
8080
return total_deleted, total_batches, reached_any_limit
8181

8282

83-
def _flush_django_auditlog(retention_period: int, batch_size: int, max_batches: int, *, dry_run: bool = False) -> tuple[int, int, bool]:
84-
# Import inside to avoid model import issues at startup
85-
from auditlog.models import LogEntry # noqa: PLC0415
86-
87-
return _flush_models_in_batches([LogEntry], "timestamp", retention_period, batch_size, max_batches, dry_run=dry_run)
88-
89-
9083
def _iter_pghistory_event_models():
9184
"""Yield pghistory Event models registered under the dojo app."""
9285
for model in apps.get_app_config("dojo").get_models():
@@ -107,8 +100,7 @@ def run_flush_auditlog(retention_period: int | None = None,
107100
*,
108101
dry_run: bool = False) -> tuple[int, int, bool]:
109102
"""
110-
Deletes audit entries older than the configured retention from both
111-
django-auditlog and django-pghistory log entries.
103+
Deletes audit entries older than the configured retention from django-pghistory log entries.
112104
113105
Returns a tuple of (deleted_total, batches_done, reached_limit).
114106
"""
@@ -121,93 +113,13 @@ def run_flush_auditlog(retention_period: int | None = None,
121113
max_batches = max_batches if max_batches is not None else getattr(settings, "AUDITLOG_FLUSH_MAX_BATCHES", 100)
122114

123115
phase = "DRY RUN" if dry_run else "Cleanup"
124-
logger.info("Running %s for django-auditlog entries with %d Months retention across all backends", phase, retention_period)
125-
d_deleted, d_batches, d_limit = _flush_django_auditlog(retention_period, batch_size, max_batches, dry_run=dry_run)
126116
logger.info("Running %s for django-pghistory entries with %d Months retention across all backends", phase, retention_period)
127117
p_deleted, p_batches, p_limit = _flush_pghistory_events(retention_period, batch_size, max_batches, dry_run=dry_run)
128118

129-
total_deleted = d_deleted + p_deleted
130-
total_batches = d_batches + p_batches
131-
reached_limit = bool(d_limit or p_limit)
132-
133119
verb = "would delete" if dry_run else "deleted"
134-
logger.info("Audit flush summary: django-auditlog %s=%s batches=%s; pghistory %s=%s batches=%s; total_%s=%s total_batches=%s",
135-
verb, d_deleted, d_batches, verb, p_deleted, p_batches, verb.replace(" ", "_"), total_deleted, total_batches)
136-
137-
return total_deleted, total_batches, reached_limit
138-
139-
140-
def enable_django_auditlog():
141-
"""Enable django-auditlog by registering models."""
142-
# Import inside function to avoid AppRegistryNotReady errors
143-
from auditlog.registry import auditlog # noqa: PLC0415
144-
145-
from dojo.models import ( # noqa: PLC0415
146-
Cred_User,
147-
Dojo_User,
148-
Endpoint,
149-
Engagement,
150-
Finding,
151-
Finding_Group,
152-
Finding_Template,
153-
Notification_Webhooks,
154-
Product,
155-
Product_Type,
156-
Risk_Acceptance,
157-
Test,
158-
)
159-
160-
logger.info("Enabling django-auditlog: Registering models")
161-
auditlog.register(Dojo_User, exclude_fields=["password"])
162-
auditlog.register(Endpoint)
163-
auditlog.register(Engagement)
164-
auditlog.register(Finding, m2m_fields={"reviewers"})
165-
auditlog.register(Finding_Group)
166-
auditlog.register(Product_Type)
167-
auditlog.register(Product)
168-
auditlog.register(Test)
169-
auditlog.register(Risk_Acceptance)
170-
auditlog.register(Finding_Template)
171-
auditlog.register(Cred_User, exclude_fields=["password"])
172-
auditlog.register(Notification_Webhooks, exclude_fields=["header_name", "header_value"])
173-
logger.info("Successfully enabled django-auditlog")
174-
175-
176-
def disable_django_auditlog():
177-
"""Disable django-auditlog by unregistering models."""
178-
# Import inside function to avoid AppRegistryNotReady errors
179-
from auditlog.registry import auditlog # noqa: PLC0415
180-
181-
from dojo.models import ( # noqa: PLC0415
182-
Cred_User,
183-
Dojo_User,
184-
Endpoint,
185-
Engagement,
186-
Finding,
187-
Finding_Group,
188-
Finding_Template,
189-
Notification_Webhooks,
190-
Product,
191-
Product_Type,
192-
Risk_Acceptance,
193-
Test,
194-
)
120+
logger.info("Audit flush summary: pghistory %s=%s batches=%s", verb, p_deleted, p_batches)
195121

196-
# Only log during actual application startup, not during shell commands
197-
if "shell" not in sys.argv:
198-
logger.info("Django-auditlog disabled - unregistering models")
199-
200-
# Unregister all models from auditlog
201-
models_to_unregister = [
202-
Dojo_User, Endpoint, Engagement, Finding, Finding_Group,
203-
Product_Type, Product, Test, Risk_Acceptance, Finding_Template,
204-
Cred_User, Notification_Webhooks,
205-
]
206-
207-
for model in models_to_unregister:
208-
with contextlib.suppress(Exception):
209-
# Model might not be registered, ignore the error
210-
auditlog.unregister(model)
122+
return p_deleted, p_batches, bool(p_limit)
211123

212124

213125
def register_django_pghistory_models():
@@ -308,6 +220,26 @@ def register_django_pghistory_models():
308220
},
309221
)(Finding)
310222

223+
# # Track the reviewers ManyToMany relationship through table
224+
# # This tracks additions/removals of reviewers from findings
225+
# reviewers_through = Finding._meta.get_field("reviewers").remote_field.through
226+
# if reviewers_through:
227+
# logger.info(f"Tracking reviewers M2M through table: {reviewers_through} (db_table: {reviewers_through._meta.db_table})")
228+
# pghistory.track(
229+
# pghistory.InsertEvent(),
230+
# pghistory.DeleteEvent(),
231+
# meta={
232+
# "indexes": [
233+
# models.Index(fields=["pgh_created_at"]),
234+
# models.Index(fields=["pgh_label"]),
235+
# models.Index(fields=["pgh_context_id"]),
236+
# ],
237+
# },
238+
# )(reviewers_through)
239+
# logger.info("Successfully registered pghistory tracking for reviewers through table")
240+
# else:
241+
# logger.warning("Could not find reviewers through table for Finding model!")
242+
311243
pghistory.track(
312244
pghistory.InsertEvent(),
313245
pghistory.UpdateEvent(condition=pghistory.AnyChange(exclude_auto=True)),
@@ -427,30 +359,6 @@ def register_django_pghistory_models():
427359
logger.info("Successfully registered models with django-pghistory")
428360

429361

430-
def enable_django_pghistory():
431-
"""Enable django-pghistory by enabling triggers."""
432-
logger.info("Enabling django-pghistory: Enabling triggers")
433-
434-
# Enable pghistory triggers
435-
try:
436-
call_command("pgtrigger", "enable")
437-
logger.info("Successfully enabled pghistory triggers")
438-
except Exception as e:
439-
logger.warning(f"Failed to enable pgtrigger triggers: {e}")
440-
# Don't raise the exception as this shouldn't prevent Django from starting
441-
442-
443-
def disable_django_pghistory():
444-
"""Disable django-pghistory by disabling triggers."""
445-
logger.info("Disabling django-pghistory: Disabling triggers")
446-
try:
447-
call_command("pgtrigger", "disable")
448-
logger.info("Successfully disabled pghistory triggers")
449-
except Exception as e:
450-
logger.warning(f"Failed to disable pgtrigger triggers: {e}")
451-
# Don't raise the exception as this shouldn't prevent Django from starting
452-
453-
454362
def configure_pghistory_triggers():
455363
"""
456364
Configure pghistory triggers based on audit settings.
@@ -466,44 +374,52 @@ def configure_pghistory_triggers():
466374
except Exception as e:
467375
logger.error(f"Failed to disable pghistory triggers: {e}")
468376
raise
469-
elif settings.AUDITLOG_TYPE == "django-pghistory":
377+
else:
378+
# Only pghistory is supported now
470379
try:
471380
call_command("pgtrigger", "enable")
472381
logger.info("Successfully enabled pghistory triggers")
473382
except Exception as e:
474383
logger.error(f"Failed to enable pghistory triggers: {e}")
475384
raise
476-
else:
477-
try:
478-
call_command("pgtrigger", "disable")
479-
logger.info("Successfully disabled pghistory triggers")
480-
except Exception as e:
481-
logger.error(f"Failed to disable pghistory triggers: {e}")
482-
raise
483385

484386

485387
def configure_audit_system():
486388
"""
487389
Configure the audit system based on settings.
488390
489-
Note: This function only handles auditlog registration. pghistory model registration
490-
is handled in apps.py, and trigger management should be done via the
491-
configure_pghistory_triggers() function to avoid database access during initialization.
391+
django-auditlog is no longer supported. Only django-pghistory is allowed.
492392
"""
493393
# Only log during actual application startup, not during shell commands
494394
log_enabled = "shell" not in sys.argv
495395

396+
# Fail if DD_AUDITLOG_TYPE is still configured (removed setting)
397+
auditlog_type_env = os.environ.get("DD_AUDITLOG_TYPE")
398+
if auditlog_type_env:
399+
error_msg = (
400+
"DD_AUDITLOG_TYPE environment variable is no longer supported. "
401+
"DefectDojo now exclusively uses django-pghistory for audit logging. "
402+
"Please remove DD_AUDITLOG_TYPE from your environment configuration. "
403+
"All new audit entries will be created using django-pghistory automatically."
404+
)
405+
logger.error(error_msg)
406+
raise ValueError(error_msg)
407+
408+
# Fail if AUDITLOG_TYPE is manually set in settings files (removed setting)
409+
if hasattr(settings, "AUDITLOG_TYPE"):
410+
error_msg = (
411+
"AUDITLOG_TYPE setting is no longer supported. "
412+
"DefectDojo now exclusively uses django-pghistory for audit logging. "
413+
"Please remove AUDITLOG_TYPE from your settings file (settings.dist.py or local_settings.py). "
414+
"All new audit entries will be created using django-pghistory automatically."
415+
)
416+
logger.error(error_msg)
417+
raise ValueError(error_msg)
418+
496419
if not settings.ENABLE_AUDITLOG:
497420
if log_enabled:
498421
logger.info("Audit logging disabled")
499-
disable_django_auditlog()
500422
return
501423

502-
if settings.AUDITLOG_TYPE == "django-auditlog":
503-
if log_enabled:
504-
logger.info("Configuring audit system: django-auditlog enabled")
505-
enable_django_auditlog()
506-
else:
507-
if log_enabled:
508-
logger.info("django-auditlog disabled (pghistory or other audit type selected)")
509-
disable_django_auditlog()
424+
if log_enabled:
425+
logger.info("Audit logging configured: django-pghistory")

dojo/management/commands/pghistory_backfill.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,10 @@ def disable_db_logging(self):
140140
)
141141

142142
def handle(self, *args, **options):
143-
if not settings.ENABLE_AUDITLOG or settings.AUDITLOG_TYPE != "django-pghistory":
143+
if not settings.ENABLE_AUDITLOG:
144144
self.stdout.write(
145145
self.style.WARNING(
146-
"pghistory is not enabled. Set DD_ENABLE_AUDITLOG=True and "
147-
"DD_AUDITLOG_TYPE=django-pghistory",
146+
"pghistory is not enabled. Set DD_ENABLE_AUDITLOG=True",
148147
),
149148
)
150149
return

0 commit comments

Comments
 (0)