Initial CMMC audit plugin

This commit is contained in:
Matthew Fricke 2026-05-27 14:39:00 -06:00
parent 871dd020e3
commit 6fec682e12
13 changed files with 373 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
.DS_Store
.pytest_cache/
.coverage
htmlcov/

1
management/__init__.py Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,96 @@
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from coldfront.plugins.cmmc_audit.backfill import RUN_NAME, run_backfill
from coldfront.plugins.cmmc_audit.models import BackfillRun
class Command(BaseCommand):
help = "Reconstruct best-effort historical CMMC audit events from existing records."
def add_arguments(self, parser):
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument(
"--dry-run",
action="store_true",
help="Report events that would be reconstructed without writing rows.",
)
mode.add_argument(
"--commit",
action="store_true",
help="Create reconstructed audit rows.",
)
parser.add_argument(
"--force",
action="store_true",
help="Rerun after a completed backfill. Duplicate source/action checks still apply.",
)
def handle(self, *args, **options):
dry_run = options["dry_run"]
commit = options["commit"]
force = options["force"]
if force and dry_run:
raise CommandError("--force is only valid with --commit")
existing_run = BackfillRun.objects.filter(name=RUN_NAME).first()
if commit and existing_run and existing_run.completed and not force:
self.stdout.write(
self.style.WARNING(
f"Backfill '{RUN_NAME}' completed at {existing_run.completed}; use --force to rerun."
)
)
return
if dry_run:
report = run_backfill(dry_run=True)
self._print_report(report, dry_run=True)
return
with transaction.atomic():
if existing_run is None:
backfill_run = BackfillRun.objects.create(name=RUN_NAME, dry_run=False)
else:
backfill_run = existing_run
backfill_run.completed = None
backfill_run.created_events = 0
backfill_run.dry_run = False
backfill_run.notes = "Forced rerun." if force else ""
backfill_run.save(update_fields=["completed", "created_events", "dry_run", "notes"])
report = run_backfill(dry_run=False)
backfill_run.completed = timezone.now()
backfill_run.created_events = report.created_events
backfill_run.notes = self._summary_notes(report, forced=force)
backfill_run.save(update_fields=["completed", "created_events", "notes"])
self._print_report(report, dry_run=False)
def _summary_notes(self, report, *, forced):
prefix = "Forced rerun. " if forced else ""
return (
f"{prefix}Created {report.created_events} events; "
f"duplicates skipped {report.duplicates}; ambiguous skipped {report.ambiguous}."
)
def _print_report(self, report, *, dry_run):
self.stdout.write("CMMC audit historical reconstruction report")
self.stdout.write(f"Mode: {'dry-run' if dry_run else 'commit'}")
self.stdout.write("Records examined by source:")
for source, count in sorted(report.examined.items()):
self.stdout.write(f" {source}: {count}")
self.stdout.write(f"Events that would be created: {report.would_create_events}")
self.stdout.write(f"Events created: {report.created_events}")
self.stdout.write(f"Duplicates skipped: {report.duplicates}")
self.stdout.write(f"Ambiguous records skipped: {report.ambiguous}")
self.stdout.write("Counts by action:")
for action, count in sorted(report.by_action.items(), key=lambda item: str(item[0])):
self.stdout.write(f" {action}: {count}")
self.stdout.write("Counts by evidence category:")
for category, count in sorted(report.by_evidence.items()):
self.stdout.write(f" {category}: {count}")

View file

@ -0,0 +1,38 @@
# Generated by Django 5.2.14 on 2026-05-19 20:29
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AuditEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('action', models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion')], max_length=64)),
('target_type', models.CharField(blank=True, max_length=128)),
('target_id', models.CharField(blank=True, max_length=128)),
('target_repr', models.CharField(blank=True, max_length=255)),
('old_values', models.JSONField(blank=True, default=dict)),
('new_values', models.JSONField(blank=True, default=dict)),
('message', models.TextField(blank=True)),
('request_path', models.CharField(blank=True, max_length=255)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='carc_audit_events', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-timestamp'],
'indexes': [models.Index(fields=['timestamp'], name='carc_audit__timesta_8969da_idx'), models.Index(fields=['action'], name='carc_audit__action_80997f_idx'), models.Index(fields=['actor'], name='carc_audit__actor_i_6096a0_idx'), models.Index(fields=['target_type', 'target_id'], name='carc_audit__target__4e5508_idx')],
},
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-05-19 20:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='auditevent',
name='action',
field=models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion'), ('user_pi_upgraded', 'User upgraded to PI'), ('pi_status_revoked', 'PI status revoked'), ('allocation_requested', 'Allocation created/requested'), ('allocation_status_changed', 'Allocation status changed'), ('allocation_renewed', 'Allocation renewed'), ('project_user_added', 'Project user added'), ('project_user_removed', 'Project user removed'), ('project_user_role_changed', 'Project user role changed')], max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-05-19 20:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0002_alter_auditevent_action'),
]
operations = [
migrations.AlterField(
model_name='auditevent',
name='action',
field=models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion'), ('user_pi_upgraded', 'User upgraded to PI'), ('pi_status_revoked', 'PI status revoked'), ('user_admin_privileges_changed', 'User admin privileges changed'), ('allocation_requested', 'Allocation created/requested'), ('allocation_change_requested', 'Allocation change requested'), ('allocation_status_changed', 'Allocation status changed'), ('renewal_requested', 'Renewal requested'), ('renewal_approved', 'Renewal approved'), ('allocation_renewed', 'Allocation renewed'), ('project_user_added', 'Project user added'), ('project_user_removed', 'Project user removed'), ('project_user_role_changed', 'Project user role changed'), ('resource_created', 'Resource created'), ('resource_changed', 'Resource changed'), ('resource_deleted', 'Resource deleted')], max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-05-20 02:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0003_alter_auditevent_action'),
]
operations = [
migrations.AlterField(
model_name='auditevent',
name='action',
field=models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion'), ('user_pi_upgraded', 'User upgraded to PI'), ('pi_status_revoked', 'PI status revoked'), ('user_admin_privileges_changed', 'User admin privileges changed'), ('allocation_requested', 'Allocation created/requested'), ('allocation_change_requested', 'Allocation change requested'), ('allocation_status_changed', 'Allocation status changed'), ('allocation_disabled', 'Allocation disabled'), ('renewal_requested', 'Renewal requested'), ('renewal_approved', 'Renewal approved'), ('allocation_renewed', 'Allocation renewed'), ('project_user_added', 'Project user added'), ('project_user_removed', 'Project user removed'), ('project_user_role_changed', 'Project user role changed'), ('resource_created', 'Resource created'), ('resource_changed', 'Resource changed'), ('resource_deleted', 'Resource deleted')], max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-05-20 04:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0004_alter_auditevent_action'),
]
operations = [
migrations.AlterField(
model_name='auditevent',
name='action',
field=models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion'), ('user_pi_upgraded', 'User upgraded to PI'), ('pi_status_revoked', 'PI status revoked'), ('user_admin_privileges_changed', 'User admin privileges changed'), ('allocation_requested', 'Allocation created/requested'), ('allocation_change_requested', 'Allocation change requested'), ('allocation_status_changed', 'Allocation status changed'), ('allocation_disabled', 'Allocation disabled'), ('renewal_requested', 'Renewal requested'), ('renewal_approved', 'Renewal approved'), ('allocation_renewed', 'Allocation renewed'), ('project_user_added', 'Project user added'), ('project_user_removed', 'Project user removed'), ('project_user_role_changed', 'Project user role changed'), ('project_review_forced', 'Project review forced'), ('project_review_submitted', 'Project review submitted'), ('project_review_completed', 'Project review completed'), ('project_review_status_changed', 'Project review status changed'), ('resource_created', 'Resource created'), ('resource_changed', 'Resource changed'), ('resource_deleted', 'Resource deleted')], max_length=64),
),
]

View file

@ -0,0 +1,108 @@
# Generated by Django 5.2.14 on 2026-05-20 22:17
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
ACTION_EVIDENCE_MAP = {
"admin_addition": ("AU-Audit-Accountability", "AU", "django_admin"),
"admin_change": ("AU-Audit-Accountability", "AU", "django_admin"),
"admin_deletion": ("AU-Audit-Accountability", "AU", "django_admin"),
"user_pi_upgraded": ("AC-Access-Control", "AC", "runtime"),
"pi_status_revoked": ("AC-Access-Control", "AC", "runtime"),
"user_admin_privileges_changed": ("AC-Access-Control", "AC", "runtime"),
"allocation_requested": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"allocation_change_requested": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"allocation_status_changed": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"allocation_disabled": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"renewal_requested": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"renewal_approved": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"allocation_renewed": ("AC-Access-Control; AU-Audit-Accountability", "AC,AU", "runtime"),
"project_user_added": ("AC-Access-Control", "AC", "runtime"),
"project_user_removed": ("AC-Access-Control", "AC", "runtime"),
"project_user_role_changed": ("AC-Access-Control", "AC", "runtime"),
"project_review_forced": ("AU-Audit-Accountability", "AU", "runtime"),
"project_review_submitted": ("AU-Audit-Accountability", "AU", "runtime"),
"project_review_completed": ("AU-Audit-Accountability", "AU", "runtime"),
"project_review_status_changed": ("AU-Audit-Accountability", "AU", "runtime"),
"resource_created": ("CM-Configuration-Management", "CM", "runtime"),
"resource_changed": ("CM-Configuration-Management", "CM", "runtime"),
"resource_deleted": ("CM-Configuration-Management", "CM", "runtime"),
}
def backfill_cmmc_fields(apps, schema_editor):
AuditEvent = apps.get_model("cmmc_audit", "AuditEvent")
for event in AuditEvent.objects.all().iterator():
evidence_category, control_family, source = ACTION_EVIDENCE_MAP.get(event.action, ("", "", "runtime"))
event.event_time = event.timestamp
event.evidence_category = evidence_category
event.control_family = control_family
event.source = source
event.save(
update_fields=[
"event_time",
"evidence_category",
"control_family",
"source",
]
)
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0005_alter_auditevent_action'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveIndex(
model_name='auditevent',
name='carc_audit__timesta_8969da_idx',
),
migrations.AddField(
model_name='auditevent',
name='control_family',
field=models.CharField(blank=True, max_length=16),
),
migrations.AddField(
model_name='auditevent',
name='event_time',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='auditevent',
name='evidence_category',
field=models.CharField(blank=True, choices=[('AC-Access-Control', 'AC - Access Control'), ('AU-Audit-Accountability', 'AU - Audit and Accountability'), ('AC-Access-Control; AU-Audit-Accountability', 'AC/AU - Access and Audit'), ('CM-Configuration-Management', 'CM - Configuration Management'), ('IA-Identification-Authentication', 'IA - Identification and Authentication')], max_length=128),
),
migrations.AddField(
model_name='auditevent',
name='is_reconstructed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='auditevent',
name='source',
field=models.CharField(choices=[('runtime', 'Runtime'), ('django_admin', 'Django admin'), ('coldfront_signal', 'ColdFront signal'), ('coldfront_workflow', 'ColdFront workflow'), ('reconstructed_django_admin_log', 'Reconstructed Django admin log')], default='runtime', max_length=64),
),
migrations.AddField(
model_name='auditevent',
name='source_id',
field=models.CharField(blank=True, max_length=128),
),
migrations.RunPython(backfill_cmmc_fields, migrations.RunPython.noop),
migrations.AddIndex(
model_name='auditevent',
index=models.Index(fields=['event_time'], name='carc_audit__event_t_752462_idx'),
),
migrations.AddIndex(
model_name='auditevent',
index=models.Index(fields=['evidence_category'], name='carc_audit__evidenc_404a75_idx'),
),
migrations.AddIndex(
model_name='auditevent',
index=models.Index(fields=['source'], name='carc_audit__source_21deb0_idx'),
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 5.2.14 on 2026-05-20 23:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0006_add_cmmc_evidence_fields'),
]
operations = [
migrations.CreateModel(
name='BackfillRun',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True)),
('started', models.DateTimeField(auto_now_add=True)),
('completed', models.DateTimeField(blank=True, null=True)),
('created_events', models.PositiveIntegerField(default=0)),
('dry_run', models.BooleanField(default=False)),
('notes', models.TextField(blank=True)),
],
options={
'ordering': ['-started'],
},
),
migrations.AlterField(
model_name='auditevent',
name='source',
field=models.CharField(choices=[('runtime', 'Runtime'), ('django_admin', 'Django admin'), ('coldfront_signal', 'ColdFront signal'), ('coldfront_workflow', 'ColdFront workflow'), ('reconstructed_django_admin_log', 'Reconstructed Django admin log'), ('django_admin_log', 'Django admin log'), ('coldfront_history', 'ColdFront history')], default='runtime', max_length=64),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-05-21 02:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cmmc_audit', '0007_backfillrun_alter_auditevent_source'),
]
operations = [
migrations.AlterField(
model_name='auditevent',
name='action',
field=models.CharField(choices=[('admin_addition', 'Admin addition'), ('admin_change', 'Admin change'), ('admin_deletion', 'Admin deletion'), ('user_pi_upgraded', 'User upgraded to PI'), ('pi_status_revoked', 'PI status revoked'), ('user_admin_privileges_changed', 'User admin privileges changed'), ('allocation_requested', 'Allocation created/requested'), ('allocation_change_requested', 'Allocation change requested'), ('allocation_status_changed', 'Allocation status changed'), ('allocation_disabled', 'Allocation disabled'), ('renewal_requested', 'Renewal requested'), ('renewal_approved', 'Renewal approved'), ('allocation_renewed', 'Allocation renewed'), ('project_user_added', 'Project user added'), ('project_user_removed', 'Project user removed'), ('project_user_role_changed', 'Project user role changed'), ('project_created', 'Project created'), ('project_archived', 'Project archived'), ('project_deleted', 'Project deleted'), ('project_status_changed', 'Project status changed'), ('project_pi_changed', 'Project PI changed'), ('project_review_forced', 'Project review forced'), ('project_review_submitted', 'Project review submitted'), ('project_review_completed', 'Project review completed'), ('project_review_status_changed', 'Project review status changed'), ('resource_created', 'Resource created'), ('resource_changed', 'Resource changed'), ('resource_deleted', 'Resource deleted')], max_length=64),
),
]

0
migrations/__init__.py Normal file
View file