├── conformity ├── __init__.py ├── tests │ ├── __init__.py │ └── test_auth.py ├── migrations │ ├── __init__.py │ ├── 0054_remove_finding_status.py │ ├── 0049_remove_action_attachment.py │ ├── 0003_rename_mesure_measure.py │ ├── 0048_remove_finding_attachment.py │ ├── 0009_rename_auditors_audit_auditor.py │ ├── 0010_rename_type_finding_severity.py │ ├── 0056_audit_name.py │ ├── 0057_alter_audit_name.py │ ├── 0002_rename_mesure_conformity_measure.py │ ├── 0055_finding_archived.py │ ├── 0022_action_status_comment.py │ ├── 0047_alter_attachment_create_date.py │ ├── 0039_control_control.py │ ├── 0018_rename_plan_end_sate_action_plan_end_date.py │ ├── 0037_alter_controlpoint_control_date.py │ ├── 0040_alter_control_level.py │ ├── 0034_alter_controlpoint_control_date.py │ ├── 0046_alter_attachment_mime_type.py │ ├── 0033_alter_controlpoint_control_date.py │ ├── 0025_alter_conformity_comment.py │ ├── 0035_action_associated_controlpoints.py │ ├── 0065_alter_control_conformity.py │ ├── 0027_alter_organization_description.py │ ├── 0031_alter_control_frequency.py │ ├── 0050_conformity_status_origin.py │ ├── 0023_alter_action_organization.py │ ├── 0006_alter_measure_parent.py │ ├── 0021_alter_action_status.py │ ├── 0060_pre_MPTT_migration.py │ ├── 0036_alter_controlpoint_status.py │ ├── 0020_alter_action_status.py │ ├── 0019_alter_conformity_status.py │ ├── 0053_finding_status.py │ ├── 0004_conformity_comment_alter_measure_description.py │ ├── 0038_action_reference_control_level.py │ ├── 0014_alter_finding_description_alter_finding_reference.py │ ├── 0045_alter_attachment_create_date_and_more.py │ ├── 0017_alter_action_create_date_alter_action_update_date.py │ ├── 0024_action_active_alter_action_status.py │ ├── 0028_alter_conformity_measure_and_more.py │ ├── 0016_alter_action_create_date_alter_action_update_date.py │ ├── 0012_alter_audit_end_date_alter_audit_report_date_and_more.py │ ├── 0013_alter_audit_end_date_alter_audit_report_date_and_more.py │ ├── 0043_attachment.py │ ├── 0063_conformity_status_justification_and_more.py │ ├── 0032_remove_control_owner_control_organization_and_more.py │ ├── 0041_rename_policy_framework_alter_framework_options_and_more.py │ ├── 0042_rename_measure_requirement_alter_conformity_options_and_more.py │ ├── 0005_alter_measure_name_alter_organization_name_and_more.py │ ├── 0059_alter_finding_cvss_alter_finding_cvss_descriptor_and_more.py │ ├── 0007_alter_conformity_options_alter_measure_options_and_more.py │ ├── 0058_finding_cvss_finding_cvss_descriptor_finding_name_and_more.py │ ├── 0051_alter_action_options_alter_attachment_options_and_more.py │ ├── 0062_alter_conformity_options_alter_requirement_options_and_more.py │ ├── 0011_alter_audit_conclusion_alter_audit_description_and_more.py │ ├── 0044_action_attachment_audit_attachment_and_more.py │ ├── 0026_alter_action_control_comment_and_more.py │ ├── 0030_control_controlpoint.py │ ├── 0008_audit_finding.py │ ├── 0061_remove_requirement_legacy_is_parent_and_more.py │ ├── 0029_alter_action_control_comment_and_more.py │ ├── 0064_indicator_indicatorpoint.py │ ├── 0001_squashed_0014_mesure_is_parent.py │ ├── 0015_alter_finding_severity_alter_policy_type_action.py │ └── 0052_framework_language.py ├── static │ ├── favicon.ico │ ├── conformity │ │ ├── bootstrap-icons-1.8.3 │ │ │ └── fonts │ │ │ │ ├── bootstrap-icons.woff │ │ │ │ └── bootstrap-icons.woff2 │ │ ├── main.css │ │ └── conformity.js │ └── registration │ │ └── login.css ├── apps.py ├── templates │ ├── conformity │ │ ├── control_form.html │ │ ├── indicator_form.html │ │ ├── finding_form.html │ │ ├── indicatorpoint_form.html │ │ ├── audit_form.html │ │ ├── organization_form.html │ │ ├── organization_detail.html │ │ ├── framework_list.html │ │ ├── conformity_list.html │ │ ├── framework_detail.html │ │ ├── controlpoint_form.html │ │ ├── organization_list.html │ │ ├── conformity_detail_list.html │ │ ├── attachment_list.html │ │ ├── indicator_detail.html │ │ ├── control_detail_list.html │ │ ├── finding_detail.html │ │ ├── audit_list.html │ │ ├── conformity_detail_list_item.html │ │ ├── action_list.html │ │ ├── controlpoint_list.html │ │ ├── finding_list.html │ │ ├── indicator_list.html │ │ └── action_form.html │ ├── includes │ │ ├── filter_row.html │ │ └── pagination.html │ ├── registration │ │ └── login.html │ └── auditlog │ │ └── logentry_list.html ├── fixture │ └── Template-Policy-Test.yaml ├── middleware.py ├── filterset.py ├── admin.py ├── urls.py ├── signals.py └── forms.py ├── oxomium ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .sonar-project.properties.swp ├── .github ├── workflows │ ├── .SonarCloud.yml.swp │ ├── django.yml │ ├── pylint.yml │ ├── dependency-review.yml │ └── SonarCloud.yml └── dependabot.yml ├── SECURITY.md ├── misc ├── oxomium.socket ├── generate_django_secret_key.py ├── nginx.conf └── oxomium.service ├── urls.py ├── requirements.txt ├── sonar-project.properties ├── env-exemple ├── manage.py ├── .gitignore └── README.md /conformity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oxomium/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conformity/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conformity/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.sonar-project.properties.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pep-un/Oxomium/HEAD/.sonar-project.properties.swp -------------------------------------------------------------------------------- /conformity/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pep-un/Oxomium/HEAD/conformity/static/favicon.ico -------------------------------------------------------------------------------- /.github/workflows/.SonarCloud.yml.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pep-un/Oxomium/HEAD/.github/workflows/.SonarCloud.yml.swp -------------------------------------------------------------------------------- /conformity/static/conformity/bootstrap-icons-1.8.3/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pep-un/Oxomium/HEAD/conformity/static/conformity/bootstrap-icons-1.8.3/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /conformity/static/conformity/bootstrap-icons-1.8.3/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pep-un/Oxomium/HEAD/conformity/static/conformity/bootstrap-icons-1.8.3/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the last release is suported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please report vulnerability by opening an issues. 10 | -------------------------------------------------------------------------------- /misc/oxomium.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=oxomium socket 3 | 4 | [Socket] 5 | ListenStream=/run/oxomium.sock 6 | SocketUser=www-data 7 | SocketMode=600 8 | 9 | [Install] 10 | WantedBy=sockets.target 11 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global django instance URL router 3 | """ 4 | 5 | from django.contrib import admin 6 | from django.urls import include, path 7 | 8 | urlpatterns = [ 9 | path('admin/', admin.site.urls), 10 | ] 11 | -------------------------------------------------------------------------------- /conformity/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class ConformityConfig(AppConfig): 4 | default_auto_field = "django.db.models.BigAutoField" 5 | name = "conformity" 6 | 7 | def ready(self): 8 | from . import signals # noqa: F401 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5.2,<5.3 2 | django-bootstrap5 3 | django-import-export 4 | django-auditlog 5 | django-tinymce 6 | django-filter 7 | django-tables2 8 | python-magic 9 | python-decouple 10 | python-dateutil 11 | django-mptt 12 | django-constance 13 | pycountry 14 | tablib[xlsx,ods] -------------------------------------------------------------------------------- /misc/generate_django_secret_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script will generate a random 50-character string suitable for use as a SECRET_KEY. 3 | import secrets 4 | 5 | charset = 'abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' 6 | print(''.join(secrets.choice(charset) for _ in range(50))) 7 | -------------------------------------------------------------------------------- /conformity/migrations/0054_remove_finding_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-07-27 10:54 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0053_finding_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='finding', 15 | name='status', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /conformity/migrations/0049_remove_action_attachment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-13 01:16 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0048_remove_finding_attachment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='action', 15 | name='attachment', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /conformity/migrations/0003_rename_mesure_measure.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-26 12:39 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0002_rename_mesure_conformity_measure'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameModel( 14 | old_name='Mesure', 15 | new_name='Measure', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /conformity/migrations/0048_remove_finding_attachment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-13 01:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0047_alter_attachment_create_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='finding', 15 | name='attachment', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /oxomium/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for oxomium project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxomium.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /oxomium/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for oxomium project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxomium.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /conformity/migrations/0009_rename_auditors_audit_auditor.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-07-09 16:13 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0008_audit_finding'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='audit', 15 | old_name='auditors', 16 | new_name='auditor', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0010_rename_type_finding_severity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-07-09 16:40 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0009_rename_auditors_audit_auditor'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='finding', 15 | old_name='type', 16 | new_name='severity', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0056_audit_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-08-01 21:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0055_finding_archived'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='audit', 15 | name='name', 16 | field=models.TextField(blank=True, max_length=256), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=pep-un_Oxomium 2 | sonar.organization=pep-un 3 | sonar.sourceEncoding=UTF-8 4 | sonar.python.version=3.10,3.11,3.12 5 | sonar.sources=. 6 | sonar.tests=conformity/tests 7 | sonar.test.inclusions=conformity/tests/**/*.py 8 | sonar.exclusions=**/migrations/**,**/__pycache__/**,**/settings/*.py,**/wsgi.py,**/asgi.py,**/manage.py 9 | sonar.python.coverage.reportPaths=coverage.xml 10 | sonar.coverage.exclusions=**/migrations/**,**/settings/*.py,**/wsgi.py,**/asgi.py,**/manage.py -------------------------------------------------------------------------------- /conformity/migrations/0057_alter_audit_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-08-01 21:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0056_audit_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='audit', 15 | name='name', 16 | field=models.CharField(blank=True, max_length=256), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0002_rename_mesure_conformity_measure.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-26 12:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0001_squashed_0014_mesure_is_parent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='conformity', 15 | old_name='mesure', 16 | new_name='measure', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0055_finding_archived.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-07-27 10:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0054_remove_finding_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='finding', 15 | name='archived', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0022_action_status_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-07 04:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0021_alter_action_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='action', 15 | name='status_comment', 16 | field=models.CharField(blank=True, max_length=4096), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0047_alter_attachment_create_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-12 04:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0046_alter_attachment_mime_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='attachment', 15 | name='create_date', 16 | field=models.DateTimeField(auto_now_add=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0039_control_control.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-03-14 01:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0038_action_reference_control_level'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='control', 15 | name='control', 16 | field=models.ManyToManyField(blank=True, to='conformity.control'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0018_rename_plan_end_sate_action_plan_end_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 23:35 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0017_alter_action_create_date_alter_action_update_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='action', 15 | old_name='plan_end_sate', 16 | new_name='plan_end_date', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0037_alter_controlpoint_control_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 23:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0036_alter_controlpoint_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='controlpoint', 15 | name='control_date', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0040_alter_control_level.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-03-18 05:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0039_control_control'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='control', 15 | name='level', 16 | field=models.IntegerField(choices=[(1, '1st level'), (2, '2nd level')], default=1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0034_alter_controlpoint_control_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 21:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0033_alter_controlpoint_control_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='controlpoint', 15 | name='control_date', 16 | field=models.DateField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0046_alter_attachment_mime_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-12 04:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0045_alter_attachment_create_date_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='attachment', 15 | name='mime_type', 16 | field=models.CharField(blank=True, max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0033_alter_controlpoint_control_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 21:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0032_remove_control_owner_control_organization_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='controlpoint', 15 | name='control_date', 16 | field=models.DateField(blank=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0025_alter_conformity_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-12 23:15 2 | 3 | from django.db import migrations 4 | import tinymce.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0024_action_active_alter_action_status'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='conformity', 16 | name='comment', 17 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /conformity/migrations/0035_action_associated_controlpoints.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 21:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0034_alter_controlpoint_control_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='action', 15 | name='associated_controlPoints', 16 | field=models.ManyToManyField(blank=True, to='conformity.controlpoint'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0065_alter_control_conformity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.7 on 2025-10-12 08:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0064_indicator_indicatorpoint'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='control', 15 | name='conformity', 16 | field=models.ManyToManyField(blank=True, related_name='controls', to='conformity.conformity'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0027_alter_organization_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-13 01:08 2 | 3 | from django.db import migrations 4 | import tinymce.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0026_alter_action_control_comment_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='organization', 16 | name='description', 17 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /conformity/migrations/0031_alter_control_frequency.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 05:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0030_control_controlpoint'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='control', 15 | name='frequency', 16 | field=models.IntegerField(choices=[(1, 'Yearly'), (2, 'Half-Yearly'), (4, 'Quarterly'), (6, 'Bimonthly'), (12, 'Monthly')], default=1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/templates/conformity/control_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of a control

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/migrations/0050_conformity_status_origin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-07 10:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0049_remove_action_attachment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='conformity', 15 | name='status_origin', 16 | field=models.CharField(choices=[('MAN', 'From expert statement'), ('CTRL', 'From a control'), ('IND', 'From indicator')], default='MAN', max_length=5), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/templates/conformity/indicator_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of a indicator

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/migrations/0023_alter_action_organization.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-07 04:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0022_action_status_comment'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='action', 16 | name='organization', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.organization'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /conformity/templates/conformity/finding_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of a finding record

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/templates/conformity/indicatorpoint_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of a indicator point

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/migrations/0006_alter_measure_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-26 21:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0005_alter_measure_name_alter_organization_name_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='measure', 16 | name='parent', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='conformity.measure'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /env-exemple: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SECRET_KEY='django-insecure-oy-h!94w($b%mb2l-jv&g!1-7iqa9(jc!c=guv=%31_z%yyghr' 3 | LANGUAGE_CODE = 'en-us' 4 | TIME_ZONE = 'UTC' 5 | USE_I18N = True 6 | USE_TZ = True 7 | 8 | STATIC_URL = 'static/' 9 | STATIC_ROOT = 'static' 10 | DB_NAME = 'db.sqlite3' 11 | 12 | SESSION_COOKIE_SECURE = True 13 | SESSION_COOKIE_HTTPONLY = True 14 | SESSION_COOKIE_SAMESITE = 'Strict' 15 | SESSION_COOKIE_NAME = '__Host-sessionid' 16 | 17 | CSRF_USE_SESSIONS = True 18 | 19 | SECURE_SSL_REDIRECT = True 20 | SECURE_HSTS_SECONDS = '15768000' 21 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 22 | SECURE_HSTS_PRELOAD = True 23 | 24 | ALLOWED_HOSTS = '127.0.0.1' 25 | -------------------------------------------------------------------------------- /conformity/migrations/0021_alter_action_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-06 22:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0020_alter_action_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='action', 15 | name='status', 16 | field=models.CharField(choices=[('1', 'Analysing'), ('2', 'Planning'), ('3', 'Implementing'), ('4', 'Controlling'), ('7', 'Frozen'), ('8', 'Closed'), ('9', 'Canceled')], default='1', max_length=5), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0060_pre_MPTT_migration.py: -------------------------------------------------------------------------------- 1 | # migration 0060_pre_mptt.py 2 | from django.db import migrations 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ('conformity', '0059_alter_finding_cvss_alter_finding_cvss_descriptor_and_more'), 7 | ] 8 | operations = [ 9 | migrations.RenameField( 10 | model_name='requirement', 11 | old_name='level', 12 | new_name='legacy_level', 13 | ), 14 | migrations.RenameField( 15 | model_name='requirement', 16 | old_name='is_parent', 17 | new_name='legacy_is_parent', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /conformity/migrations/0036_alter_controlpoint_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 22:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0035_action_associated_controlpoints'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='controlpoint', 15 | name='status', 16 | field=models.CharField(choices=[('SCHD', 'Scheduled'), ('TOBE', 'To evaluate'), ('OK', 'Compliant'), ('NOK', 'Non-Compliant'), ('MISS', 'Missed')], default='SCHD', max_length=4), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0020_alter_action_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-05 23:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0019_alter_conformity_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='action', 15 | name='status', 16 | field=models.CharField(choices=[('A', 'Analysing'), ('P', 'Planning'), ('I', 'Implementing'), ('C', 'Controlling'), ('E', 'Close successfully'), ('F', 'Frozen'), ('X', 'Canceled')], default='A', max_length=5), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/templates/conformity/audit_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of an audit record

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/templates/conformity/organization_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Organization update

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_form form %} 14 | {% bootstrap_button button_type="submit" content="Save" %} 15 |
16 |
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /conformity/migrations/0019_alter_conformity_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-05 21:48 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0018_rename_plan_end_sate_action_plan_end_date'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='conformity', 16 | name='status', 17 | field=models.IntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /conformity/migrations/0053_finding_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-07-26 23:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0052_framework_language'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='finding', 15 | name='status', 16 | field=models.CharField(choices=[('DFT', 'Draft, not validated yet'), ('NEW', 'New and under review'), ('ACT', 'Active, actions identified and planned'), ('REJ', 'Incorrect or inadequate finding'), ('ARC', 'Archived, actions closes')], default='NEW', max_length=3), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/migrations/0004_conformity_comment_alter_measure_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-26 12:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0003_rename_mesure_measure'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='conformity', 15 | name='comment', 16 | field=models.TextField(blank=True, max_length=4096), 17 | ), 18 | migrations.AlterField( 19 | model_name='measure', 20 | name='description', 21 | field=models.TextField(blank=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxomium.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /conformity/migrations/0038_action_reference_control_level.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-03-11 22:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0037_alter_controlpoint_control_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='action', 15 | name='reference', 16 | field=models.URLField(blank=True), 17 | ), 18 | migrations.AddField( 19 | model_name='control', 20 | name='level', 21 | field=models.IntegerField(choices=[(1, '1st level control'), (2, '2nd level control')], default=1), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /conformity/migrations/0014_alter_finding_description_alter_finding_reference.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 02:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0013_alter_audit_end_date_alter_audit_report_date_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='description', 16 | field=models.CharField(blank=True, max_length=4096), 17 | ), 18 | migrations.AlterField( 19 | model_name='finding', 20 | name='reference', 21 | field=models.CharField(blank=True, max_length=4096), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /conformity/migrations/0045_alter_attachment_create_date_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-12 04:28 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0044_action_attachment_audit_attachment_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='attachment', 16 | name='create_date', 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | ), 19 | migrations.AlterField( 20 | model_name='attachment', 21 | name='mime_type', 22 | field=models.CharField(max_length=255), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | max-parallel: 4 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - uses: actions/checkout@v5 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v6 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install Dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | - name: Run Tests 31 | run: | 32 | python manage.py test 33 | -------------------------------------------------------------------------------- /conformity/migrations/0017_alter_action_create_date_alter_action_update_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 23:34 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0016_alter_action_create_date_alter_action_update_date'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='action', 16 | name='create_date', 17 | field=models.DateField(default=django.utils.timezone.now), 18 | ), 19 | migrations.AlterField( 20 | model_name='action', 21 | name='update_date', 22 | field=models.DateField(default=django.utils.timezone.now), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /conformity/migrations/0024_action_active_alter_action_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-07 05:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0023_alter_action_organization'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='action', 15 | name='active', 16 | field=models.BooleanField(default=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='action', 20 | name='status', 21 | field=models.CharField(choices=[('1', 'Analysing'), ('2', 'Planning'), ('3', 'Implementing'), ('4', 'Controlling'), ('5', 'Closed'), ('7', 'Frozen'), ('9', 'Canceled')], default='1', max_length=5), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /conformity/migrations/0028_alter_conformity_measure_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-14 03:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0027_alter_organization_description'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='conformity', 16 | name='measure', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.measure'), 18 | ), 19 | migrations.AlterField( 20 | model_name='conformity', 21 | name='organization', 22 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.organization'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /conformity/migrations/0016_alter_action_create_date_alter_action_update_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 23:33 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0015_alter_finding_severity_alter_policy_type_action'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='action', 16 | name='create_date', 17 | field=models.DateField(default=datetime.datetime(2023, 2, 3, 23, 33, 22, 666918, tzinfo=datetime.timezone.utc)), 18 | ), 19 | migrations.AlterField( 20 | model_name='action', 21 | name='update_date', 22 | field=models.DateField(default=datetime.datetime(2023, 2, 3, 23, 33, 22, 666936, tzinfo=datetime.timezone.utc)), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /conformity/migrations/0012_alter_audit_end_date_alter_audit_report_date_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 00:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0011_alter_audit_conclusion_alter_audit_description_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='audit', 15 | name='end_date', 16 | field=models.DateField(null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='audit', 20 | name='report_date', 21 | field=models.DateField(null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='audit', 25 | name='start_date', 26 | field=models.DateField(null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | permissions: 3 | contents: read 4 | 5 | on: [push] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.11", "3.12"] 13 | steps: 14 | - uses: actions/checkout@v5 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install project dependencies 20 | run: | 21 | pip install -r requirements.txt 22 | - name: Install pylint 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install pylint-django 26 | - name: Analysing the code with pylint 27 | run: | 28 | pylint -E --load-plugins pylint_django --django-settings-module=oxomium --ignore-paths='.*/migrations/' --ignore=__init__.py,manage.py $(git ls-files '*.py') 29 | -------------------------------------------------------------------------------- /conformity/migrations/0013_alter_audit_end_date_alter_audit_report_date_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 00:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0012_alter_audit_end_date_alter_audit_report_date_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='audit', 15 | name='end_date', 16 | field=models.DateField(blank=True, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='audit', 20 | name='report_date', 21 | field=models.DateField(blank=True, null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='audit', 25 | name='start_date', 26 | field=models.DateField(blank=True, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /oxomium/urls.py: -------------------------------------------------------------------------------- 1 | """oxomium URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path('', include('conformity.urls')), 21 | path('accounts/', include('django.contrib.auth.urls')), 22 | path('django-backend/', admin.site.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /conformity/migrations/0043_attachment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-12 02:57 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0042_rename_measure_requirement_alter_conformity_options_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Attachment', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('file', models.FileField(upload_to='attachments/')), 19 | ('comment', models.TextField(blank=True, max_length=4096)), 20 | ('mime_type', models.CharField(blank=True, max_length=255)), 21 | ('create_date', models.DateField(default=django.utils.timezone.now)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /conformity/templates/includes/filter_row.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 | 17 |
18 | 19 |
20 | 21 | {{ object_list | length }} items 22 | 23 |
-------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v5 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /conformity/migrations/0063_conformity_status_justification_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.4 on 2025-08-24 06:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0062_alter_conformity_options_alter_requirement_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='conformity', 15 | name='status_justification', 16 | field=models.CharField(blank=True, choices=[('EXPT', 'From expert statement'), ('CTRL', 'From successful control'), ('ACT', 'From completed action'), ('FIN', 'From an audit finding'), ('CONF', 'From conformity aggregation')], default='EXPT', max_length=4), 17 | ), 18 | migrations.AddField( 19 | model_name='conformity', 20 | name='status_last_update', 21 | field=models.DateTimeField(blank=True, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /conformity/migrations/0032_remove_control_owner_control_organization_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-25 20:18 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('conformity', '0031_alter_control_frequency'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='control', 17 | name='owner', 18 | ), 19 | migrations.AddField( 20 | model_name='control', 21 | name='organization', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.organization'), 23 | ), 24 | migrations.AlterField( 25 | model_name='controlpoint', 26 | name='control_date', 27 | field=models.DateField(default=datetime.date(2023, 2, 25)), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-08-25 02:39 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0040_alter_control_level'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameModel( 14 | old_name='Policy', 15 | new_name='Framework', 16 | ), 17 | migrations.AlterModelOptions( 18 | name='framework', 19 | options={'ordering': ['name'], 'verbose_name': 'Framework', 'verbose_name_plural': 'Frameworks'}, 20 | ), 21 | migrations.RenameField( 22 | model_name='measure', 23 | old_name='policy', 24 | new_name='framework', 25 | ), 26 | migrations.RenameField( 27 | model_name='audit', 28 | old_name='audited_policies', 29 | new_name='audited_frameworks', 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /.github/workflows/SonarCloud.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | jobs: 11 | sonarcloud: 12 | name: SonarCloud 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v5 16 | with: 17 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 18 | - name: Install Dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | pip install coverage 23 | - name: Run coverage test 24 | run: | 25 | coverage run manage.py test 26 | coverage xml 27 | - name: SonarCloud Scan 28 | uses: SonarSource/sonarcloud-github-action@master 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 31 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 32 | -------------------------------------------------------------------------------- /conformity/migrations/0042_rename_measure_requirement_alter_conformity_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-08-25 04:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0041_rename_policy_framework_alter_framework_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameModel( 14 | old_name='Measure', 15 | new_name='Requirement', 16 | ), 17 | migrations.AlterModelOptions( 18 | name='conformity', 19 | options={'ordering': ['organization', 'requirement'], 'verbose_name': 'Conformity', 'verbose_name_plural': 'Conformities'}, 20 | ), 21 | migrations.RenameField( 22 | model_name='conformity', 23 | old_name='measure', 24 | new_name='requirement', 25 | ), 26 | migrations.AlterUniqueTogether( 27 | name='conformity', 28 | unique_together={('organization', 'requirement')}, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /conformity/migrations/0005_alter_measure_name_alter_organization_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-26 21:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0004_conformity_comment_alter_measure_description'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='measure', 15 | name='name', 16 | field=models.CharField(blank=True, max_length=50, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='organization', 20 | name='name', 21 | field=models.CharField(max_length=256, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='policy', 25 | name='name', 26 | field=models.CharField(max_length=256, unique=True), 27 | ), 28 | migrations.AlterUniqueTogether( 29 | name='conformity', 30 | unique_together={('organization', 'measure')}, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /conformity/migrations/0059_alter_finding_cvss_alter_finding_cvss_descriptor_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.4 on 2025-08-02 14:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0058_finding_cvss_finding_cvss_descriptor_finding_name_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='cvss', 16 | field=models.FloatField(blank=True, default=None, null=True, verbose_name='CVSS'), 17 | ), 18 | migrations.AlterField( 19 | model_name='finding', 20 | name='cvss_descriptor', 21 | field=models.CharField(blank=True, max_length=256, verbose_name='CVSS Vector'), 22 | ), 23 | migrations.AlterField( 24 | model_name='finding', 25 | name='severity', 26 | field=models.CharField(choices=[('CRT', 'Critical non-conformity'), ('MAJ', 'Major non-conformity'), ('MIN', 'Minor non-conformity'), ('OBS', 'Opportunity For Improvement'), ('POS', 'Positive finding'), ('OTHER', 'Other comment')], default='OBS', max_length=5), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /conformity/migrations/0007_alter_conformity_options_alter_measure_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-06-05 17:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0006_alter_measure_parent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='conformity', 15 | options={'ordering': ['organization', 'measure'], 'verbose_name': 'Conformity', 'verbose_name_plural': 'Conformities'}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='measure', 19 | options={'ordering': ['name']}, 20 | ), 21 | migrations.AlterModelOptions( 22 | name='organization', 23 | options={'ordering': ['name']}, 24 | ), 25 | migrations.AlterModelOptions( 26 | name='policy', 27 | options={'ordering': ['name'], 'verbose_name': 'Policy', 'verbose_name_plural': 'Policies'}, 28 | ), 29 | migrations.AddField( 30 | model_name='conformity', 31 | name='applicable', 32 | field=models.BooleanField(default=True), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /conformity/templates/conformity/organization_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Organizations {{ organization }}

5 | {{ organization.administrative_id | default:'ø' }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

{{ organization.description | linebreaksbr }}

10 | 11 |

Applicable policy :

12 | {% for item in organization.get_frameworks %} 13 | 14 |
15 | {{ item }} 16 |
17 |
18 |
19 | {% endfor %} 20 | 21 |
22 | 23 |
24 |

Attachments

25 | 32 |
33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /misc/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl http2; 3 | server_name demo.oxomium.org; 4 | 5 | root /var/www/Oxomium; 6 | index index.php; 7 | 8 | error_log /var/log/nginx/oxomium-error.log; 9 | access_log /var/log/nginx/oxomium-access.log; 10 | 11 | ssl_certificate /etc/letsencrypt/live/demo.oxomium.org/fullchain.pem; 12 | ssl_certificate_key /etc/letsencrypt/live/demo.oxomium.org/privkey.pem; 13 | ssl_trusted_certificate /etc/letsencrypt/live/demo.oxomium.org/chain.pem; 14 | 15 | add_header Content-Security-Policy "default-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';object-src 'none';img-src 'self' data:;"; 16 | 17 | location /static/ { 18 | autoindex on; 19 | alias /var/www/Oxomium/static/; 20 | } 21 | 22 | location / { 23 | proxy_pass http://unix:/run/oxomium.sock; 24 | proxy_set_header Host $http_host; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | proxy_redirect off; 27 | } 28 | } 29 | 30 | server { 31 | # Redirect HTTP traffic to HTTPS 32 | listen 80 ; 33 | server_name demo.oxomium.org; 34 | return 301 https://$host$request_uri; 35 | 36 | if ($host = demo.oxomium.org) { 37 | return 301 https://$host$request_uri; 38 | } 39 | } -------------------------------------------------------------------------------- /conformity/migrations/0058_finding_cvss_finding_cvss_descriptor_finding_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-08-01 21:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0057_alter_audit_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='finding', 15 | name='cvss', 16 | field=models.FloatField(blank=True, default=None, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='finding', 20 | name='cvss_descriptor', 21 | field=models.CharField(blank=True, max_length=256), 22 | ), 23 | migrations.AddField( 24 | model_name='finding', 25 | name='name', 26 | field=models.CharField(blank=True, max_length=256), 27 | ), 28 | migrations.AddField( 29 | model_name='finding', 30 | name='observation', 31 | field=models.TextField(blank=True, max_length=4096), 32 | ), 33 | migrations.AddField( 34 | model_name='finding', 35 | name='recommendation', 36 | field=models.TextField(blank=True, max_length=4096), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /conformity/migrations/0051_alter_action_options_alter_attachment_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-07-06 07:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0050_conformity_status_origin'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='action', 15 | options={'ordering': ['status', '-update_date']}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='attachment', 19 | options={'ordering': ['-create_date', 'file']}, 20 | ), 21 | migrations.AlterModelOptions( 22 | name='audit', 23 | options={'ordering': ['-report_date', '-start_date']}, 24 | ), 25 | migrations.AlterModelOptions( 26 | name='control', 27 | options={'ordering': ['level', 'frequency', 'title']}, 28 | ), 29 | migrations.AlterModelOptions( 30 | name='controlpoint', 31 | options={'ordering': ['period_end_date']}, 32 | ), 33 | migrations.AlterModelOptions( 34 | name='finding', 35 | options={'ordering': ['severity']}, 36 | ), 37 | migrations.RemoveField( 38 | model_name='conformity', 39 | name='status_origin', 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /conformity/static/conformity/main.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | z-index: 100; 7 | padding: 48px 0 0; 8 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); 9 | } 10 | 11 | 12 | .sidebar-sticky { 13 | position: relative; 14 | top: 0; 15 | height: calc(100vh - 48px); 16 | padding-top: .5rem; 17 | overflow-x: hidden; 18 | overflow-y: auto; 19 | } 20 | 21 | .sidebar .nav-link { 22 | font-weight: 500; 23 | color: #333; 24 | } 25 | 26 | .sidebar .nav-link.active { 27 | color: #2470dc; 28 | } 29 | 30 | .sidebar-heading { 31 | font-size: .75rem; 32 | text-transform: uppercase; 33 | } 34 | 35 | .navbar-brand { 36 | padding-top: .75rem; 37 | padding-bottom: .75rem; 38 | font-size: 1rem; 39 | background-color: rgba(0, 0, 0, .25); 40 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); 41 | } 42 | 43 | .navbar .navbar-toggler { 44 | top: .25rem; 45 | right: 1rem; 46 | } 47 | 48 | .navbar .form-control { 49 | padding: .75rem 1rem; 50 | border-width: 0; 51 | border-radius: 0; 52 | } 53 | 54 | .form-control-dark { 55 | color: #fff; 56 | background-color: rgba(255, 255, 255, .1); 57 | border-color: rgba(255, 255, 255, .1); 58 | } 59 | 60 | .form-control-dark:focus { 61 | border-color: transparent; 62 | box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); 63 | } -------------------------------------------------------------------------------- /conformity/migrations/0062_alter_conformity_options_alter_requirement_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.4 on 2025-08-23 19:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0061_remove_requirement_legacy_is_parent_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='conformity', 15 | options={'ordering': ['organization', 'requirement__framework', 'requirement__tree_id', 'requirement__lft'], 'verbose_name': 'Conformity', 'verbose_name_plural': 'Conformities'}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='requirement', 19 | options={}, 20 | ), 21 | migrations.AlterField( 22 | model_name='action', 23 | name='associated_conformity', 24 | field=models.ManyToManyField(blank=True, related_name='actions', to='conformity.conformity'), 25 | ), 26 | migrations.AlterField( 27 | model_name='action', 28 | name='associated_controlPoints', 29 | field=models.ManyToManyField(blank=True, related_name='actions', to='conformity.controlpoint'), 30 | ), 31 | migrations.AlterField( 32 | model_name='action', 33 | name='associated_findings', 34 | field=models.ManyToManyField(blank=True, related_name='actions', to='conformity.finding'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /conformity/migrations/0011_alter_audit_conclusion_alter_audit_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 00:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0010_rename_type_finding_severity'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='audit', 15 | name='conclusion', 16 | field=models.CharField(blank=True, max_length=4096), 17 | ), 18 | migrations.AlterField( 19 | model_name='audit', 20 | name='description', 21 | field=models.CharField(blank=True, max_length=4096), 22 | ), 23 | migrations.AlterField( 24 | model_name='audit', 25 | name='end_date', 26 | field=models.DateField(blank=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='audit', 30 | name='report_date', 31 | field=models.DateField(blank=True), 32 | ), 33 | migrations.AlterField( 34 | model_name='audit', 35 | name='start_date', 36 | field=models.DateField(blank=True), 37 | ), 38 | migrations.AlterField( 39 | model_name='finding', 40 | name='severity', 41 | field=models.CharField(choices=[('CRIT', 'Critical non-conformity'), ('MAJ', 'Major non-conformity'), ('MIN', 'Minor non-conformity'), ('OBS', 'Observation or Opportunity For Improvement'), ('POS', 'Positive element'), ('OTHER', 'Other remark')], default='OBS', max_length=5), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /conformity/migrations/0044_action_attachment_audit_attachment_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-10-12 03:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0043_attachment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='action', 15 | name='attachment', 16 | field=models.ManyToManyField(blank=True, related_name='actions', to='conformity.attachment'), 17 | ), 18 | migrations.AddField( 19 | model_name='audit', 20 | name='attachment', 21 | field=models.ManyToManyField(blank=True, related_name='audits', to='conformity.attachment'), 22 | ), 23 | migrations.AddField( 24 | model_name='controlpoint', 25 | name='attachment', 26 | field=models.ManyToManyField(blank=True, related_name='ControlPoint', to='conformity.attachment'), 27 | ), 28 | migrations.AddField( 29 | model_name='finding', 30 | name='attachment', 31 | field=models.ManyToManyField(blank=True, related_name='findings', to='conformity.attachment'), 32 | ), 33 | migrations.AddField( 34 | model_name='framework', 35 | name='attachment', 36 | field=models.ManyToManyField(blank=True, related_name='frameworks', to='conformity.attachment'), 37 | ), 38 | migrations.AddField( 39 | model_name='organization', 40 | name='attachment', 41 | field=models.ManyToManyField(blank=True, related_name='organizations', to='conformity.attachment'), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /conformity/static/registration/login.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | display: -ms-flexbox; 7 | display: -webkit-box; 8 | display: flex; 9 | -ms-flex-align: center; 10 | -ms-flex-pack: center; 11 | -webkit-box-align: center; 12 | align-items: center; 13 | -webkit-box-pack: center; 14 | justify-content: center; 15 | padding-top: 40px; 16 | padding-bottom: 40px; 17 | background-color: #f5f5f5; 18 | } 19 | 20 | .form-signin { 21 | width: 100%; 22 | max-width: 330px; 23 | padding: 15px; 24 | margin: 0 auto; 25 | } 26 | .form-signin .form-control { 27 | position: relative; 28 | box-sizing: border-box; 29 | height: auto; 30 | padding: 10px; 31 | font-size: 16px; 32 | } 33 | .form-signin .form-control:focus { 34 | z-index: 2; 35 | } 36 | .form-signin input[type="email"] { 37 | margin-bottom: -1px; 38 | border-bottom-right-radius: 0; 39 | border-bottom-left-radius: 0; 40 | } 41 | .form-signin input[type="password"] { 42 | margin-bottom: 10px; 43 | border-top-left-radius: 0; 44 | border-top-right-radius: 0; 45 | } 46 | 47 | .animated { 48 | animation-duration: 2.2s; 49 | animation-fill-mode: both; 50 | } 51 | 52 | @keyframes fadeIn1 { 53 | 0% {opacity: 0;} 54 | 33% {opacity: 1;} 55 | 100% {opacity: 1;} 56 | } 57 | 58 | @keyframes fadeIn2 { 59 | 0% {opacity: 0;} 60 | 33% {opacity: 0;} 61 | 66% {opacity: 1;} 62 | 100% {opacity: 1;} 63 | } 64 | 65 | @keyframes fadeIn3 { 66 | 0% {opacity: 0;} 67 | 66% {opacity: 0;} 68 | 100% {opacity: 1;} 69 | } 70 | 71 | .fadeIn1 { 72 | animation-name: fadeIn1; 73 | } 74 | .fadeIn2 { 75 | animation-name: fadeIn2; 76 | } 77 | .fadeIn3 { 78 | animation-name: fadeIn3; 79 | } -------------------------------------------------------------------------------- /misc/oxomium.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Oxomium WSGI Service 3 | Documentation=https://www.oxomium.org 4 | After=network-online.target 5 | Requires=oxomium.socket 6 | 7 | [Service] 8 | Type=simple 9 | 10 | User=www-data 11 | Group=www-data 12 | DynamicUser=yes 13 | 14 | PrivateDevices=yes 15 | PrivateUsers=yes 16 | PrivateDevices=yes 17 | PrivateTmp=yes 18 | PrivateTmp=true 19 | PrivateNetwork=yes 20 | 21 | ProtectProc=invisible 22 | ProtectControlGroups=yes 23 | ProtectHome=yes 24 | ProtectHostname=yes 25 | ProtectKernelLogs=yes 26 | ProtectKernelModules=yes 27 | ProtectKernelTunables=yes 28 | ProtectSystem=strict 29 | ProtectClock=yes 30 | 31 | RestrictAddressFamilies=AF_UNIX 32 | RestrictNamespaces=yes 33 | RestrictRealtime=yes 34 | RestrictSUIDSGID=yes 35 | 36 | CapabilityBoundingSet=~CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_FOWNER CAP_IPC_OWNER CAP_CHECKPOINT_RESTORECAP_NET_ADMIN CAP_SYS_MODULE CAP_SYS_RAWIO CAP_SYS_TIME CAP_KILL CAP_MKNOD CAP_SYSLOG CAP_SYS_NICE CAP_SYS_RESOURCE CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_SYS_BOOT CAP_LINUX_IMMUTABLE CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_PACCT CAP_SYS_TTY_CONFIG CAP_WAKE_ALARM CAP_MAC_OVERRIDE CAP_MAC_ADMIN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH 37 | RestrictNamespaces=~CLONE_NEWUSER 38 | SystemCallFilter=~@clock @debug @module @mount @reboot @swap @resources @raw-io @cpu-emulation @obsolete 39 | 40 | LockPersonality=yes 41 | MemoryDenyWriteExecute=yes 42 | NoNewPrivileges=yes 43 | ProcSubset=pid 44 | 45 | Restart=always 46 | RestartSec=0 47 | RuntimeDirectory=oxomium 48 | WorkingDirectory=/srv/web/Oxomium 49 | ReadWritePaths=/srv/web/Oxomium 50 | ExecStart=/srv/web/Oxomium/.venv/bin/gunicorn oxomium.wsgi:application 51 | ExecReload=/bin/kill -s HUP $MAINPID 52 | KillMode=mixed 53 | TimeoutStopSec=5 54 | UMask=550 55 | 56 | [Install] 57 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /conformity/fixture/Template-Policy-Test.yaml: -------------------------------------------------------------------------------- 1 | - model: conformity.policy 2 | fields: 3 | name: Test policy 4 | version: 1 5 | publish_by: Oxomium 6 | type: OTHER 7 | - model: conformity.measure 8 | fields: 9 | code: TEST 10 | name: TEST 11 | level: 0 12 | order: 1 13 | policy: 14 | - Test policy 15 | parent: null 16 | title: Test policy 17 | description: '' 18 | is_parent: true 19 | - model: conformity.measure 20 | fields: 21 | code: A 22 | name: NIST-A 23 | level: 1 24 | order: 1 25 | policy: 26 | - Test policy 27 | parent: 28 | - TEST 29 | title: Titre A 30 | description: '' 31 | is_parent: true 32 | - model: conformity.measure 33 | fields: 34 | code: 1 35 | name: NIST-A-1 36 | level: 2 37 | order: 1 38 | policy: 39 | - Test policy 40 | parent: 41 | - TEST-A 42 | title: Measure A-1 43 | description: This is the measure 1 of section A of Test policy. 44 | is_parent: false 45 | - model: conformity.measure 46 | fields: 47 | code: 2 48 | name: NIST-A-2 49 | level: 2 50 | order: 2 51 | policy: 52 | - Test policy 53 | parent: 54 | - TEST-A 55 | title: Measure A-2 56 | description: This is the measure 2 of section A of Test policy. 57 | is_parent: false 58 | - model: conformity.measure 59 | fields: 60 | code: B 61 | name: NIST-B 62 | level: 1 63 | order: 2 64 | policy: 65 | - Test policy 66 | parent: 67 | - TEST 68 | title: Measure B 69 | description: This is the measure B of Test policy. 70 | is_parent: false 71 | - model: conformity.measure 72 | fields: 73 | code: C 74 | name: NIST-C 75 | level: 1 76 | order: 3 77 | policy: 78 | - Test policy 79 | parent: 80 | - TEST 81 | title: Measure C 82 | description: This is the measure C of Test policy. 83 | is_parent: false -------------------------------------------------------------------------------- /conformity/templates/conformity/framework_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Frameworks

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% include "includes/filter_row.html" %} 10 | 11 | {% if framework_list %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for framework in framework_list %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | {% endfor %} 39 | 40 |
List of frameworks
FrameworkVersionLanguagePublished byTypeRequirements
{{ framework.name }}{{ framework.version | default:'-'}}{{ framework.get_language_display }}{{ framework.publish_by }}{{ framework.get_type }} 33 | 34 | 35 | 36 |
41 | {% else %} 42 | 45 | {% endif %} 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /conformity/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import importlib 3 | from django.test import TestCase 4 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin 5 | 6 | 7 | class SecurityCoverageTests(TestCase): 8 | """ 9 | Security coverage tests: 10 | - All CBV defined in `conformity.views` must be protected by an auth-related mixin. 11 | """ 12 | 13 | ALLOW_PUBLIC_VIEWS = set() 14 | 15 | def test_all_cbv_have_auth_mixin(self): 16 | """ 17 | Every class-based view (CBV) declared in `conformity.views` must inherit from 18 | one of: LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin. 19 | This catches accidental public exposure of internal views. 20 | """ 21 | try: 22 | views_mod = importlib.import_module("conformity.views") 23 | except ModuleNotFoundError: 24 | self.skipTest("Module 'conformity.views' not found; adjust module path if your app is named differently.") 25 | 26 | from django.views import View 27 | 28 | protected_mixins = (LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin) 29 | missing = [] 30 | 31 | for name, obj in inspect.getmembers(views_mod, inspect.isclass): 32 | # Only classes defined in this module (ignore imports) 33 | if obj.__module__ != views_mod.__name__: 34 | continue 35 | if not issubclass(obj, View): 36 | continue 37 | if name in self.ALLOW_PUBLIC_VIEWS: 38 | continue 39 | if not any(issubclass(obj, mixin) for mixin in protected_mixins): 40 | missing.append(name) 41 | 42 | self.assertFalse( 43 | missing, 44 | msg=( 45 | "The following CBV lack an auth/permission mixin " 46 | f"(LoginRequiredMixin/PermissionRequiredMixin/UserPassesTestMixin): {missing}" 47 | ), 48 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Django # 2 | *.log 3 | *.pot 4 | *.pyc 5 | __pycache__ 6 | *.sqlite3 7 | media 8 | attachments/ 9 | 10 | # Backup files # 11 | *.bak 12 | 13 | # If you are using PyCharm # 14 | .idea 15 | 16 | # File-based project format 17 | *.iws 18 | 19 | # IntelliJ 20 | out/ 21 | 22 | # JIRA plugin 23 | atlassian-ide-plugin.xml 24 | 25 | # Python # 26 | *.py[cod] 27 | *$py.class 28 | 29 | # Distribution / packaging 30 | .Python build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | static/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | .pytest_cache/ 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | 65 | # Jupyter Notebook 66 | .ipynb_checkpoints 67 | 68 | # pyenv 69 | .python-version 70 | 71 | # celery 72 | celerybeat-schedule.* 73 | 74 | # SageMath parsed files 75 | *.sage.py 76 | 77 | # Environments 78 | .env 79 | .venv 80 | env/ 81 | venv/ 82 | ENV/ 83 | env.bak/ 84 | venv.bak/ 85 | 86 | # mkdocs documentation 87 | /site 88 | 89 | # mypy 90 | .mypy_cache/ 91 | 92 | # Sublime Text # 93 | *.tmlanguage.cache 94 | *.tmPreferences.cache 95 | *.stTheme.cache 96 | *.sublime-workspace 97 | *.sublime-project 98 | 99 | # sftp configuration file 100 | sftp-config.json 101 | 102 | # Package control specific files Package 103 | Control.last-run 104 | Control.ca-list 105 | Control.ca-bundle 106 | Control.system-ca-bundle 107 | GitHub.sublime-settings 108 | 109 | # Visual Studio Code # 110 | .vscode/* 111 | !.vscode/settings.json 112 | !.vscode/tasks.json 113 | !.vscode/launch.json 114 | !.vscode/extensions.json 115 | .history 116 | .sonarlint/connectedMode.json 117 | -------------------------------------------------------------------------------- /conformity/templates/conformity/conformity_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Conformities

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% include "includes/filter_row.html" %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for con in conformity_list %} 25 | 26 | 29 | 32 | 35 | 38 | 43 | 47 | 48 | {% empty %} 49 | 52 | 53 | {% endfor %} 54 |
Overview of the organizations conformity to there Frameworks
OrganizationFrameworkRequirementCompletenessConformityActions
27 | {{ con.organization }} 28 | 30 | {{ con.requirement.framework.name }} 31 | 33 | {{ con.get_leaf | length }} 34 | 36 | {{ con.get_completeness }} % 37 | 39 |
40 |
{{ con.status }}%
41 |
42 |
44 | 46 |
No data to display
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Pylint Error](https://github.com/pep-un/Oxomium/actions/workflows/pylint.yml/badge.svg)](https://github.com/pep-un/Oxomium/actions/workflows/pylint.yml) 3 | [![Django CI](https://github.com/pep-un/Oxomium/actions/workflows/django.yml/badge.svg)](https://github.com/pep-un/Oxomium/actions/workflows/django.yml) 4 | [![Dependency Review](https://github.com/pep-un/Oxomium/actions/workflows/dependency-review.yml/badge.svg?branch=main)](https://github.com/pep-un/Oxomium/actions/workflows/dependency-review.yml) 5 | 6 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 7 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 8 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 9 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 10 | [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 11 | [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 12 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=pep-un_Oxomium&metric=coverage)](https://sonarcloud.io/summary/new_code?id=pep-un_Oxomium) 13 | 14 | 15 | # Oxomium Project 16 | 17 | Oxomium is an opensource project build to help company to manage the cybersecurity compliance of organisations. 18 | 19 | It provides help to CISO or other security people to follow conformity to a Framework. 20 | 21 | More information on [Oxomium Website](https://www.oxomium.org). 22 | 23 | An online demonstration in available with user `demo` and password `6NLYm6F4PBBQBjc`: [Oxomium Demo](https://demo.oxomium.org) 24 | 25 | A wiki page detail the process of [installation](https://github.com/pep-un/Oxomium/wiki/Instalation). 26 | -------------------------------------------------------------------------------- /conformity/templates/conformity/framework_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Framework: {{ framework.name }}

5 |

6 | Version : {{ framework.version | default:'-' }} 7 | Language : {{ framework.get_language_display | default:'-' }} 8 | Published by : {{ framework.publish_by | default:'-' }} 9 | Type : {{ framework.get_type | default:'-' }} 10 |

11 | {% endblock %} 12 | 13 | {% block content %} 14 | {% for requirement in framework.get_requirements %} 15 | {% if requirement.level == 1 %} 16 |

{{ requirement.title }}

17 |

{{ requirement.description | linebreaksbr }}

18 | {% elif requirement.level == 2 %} 19 |

{{ requirement.title }}

20 |

{{ requirement.description | linebreaksbr }}

21 | {% else %} 22 |

{{ requirement.title }}

23 |

{{ requirement.description | linebreaksbr }}

24 | {% endif %} 25 | 26 | {% empty %} 27 | 30 | {% endfor %} 31 | 32 |
33 | 34 |
35 |

Attachments

36 | 43 |
44 | 45 | {% endblock %} -------------------------------------------------------------------------------- /conformity/migrations/0026_alter_action_control_comment_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-13 00:52 2 | 3 | from django.db import migrations 4 | import tinymce.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0025_alter_conformity_comment'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='action', 16 | name='control_comment', 17 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 18 | ), 19 | migrations.AlterField( 20 | model_name='action', 21 | name='description', 22 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 23 | ), 24 | migrations.AlterField( 25 | model_name='action', 26 | name='implement_comment', 27 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 28 | ), 29 | migrations.AlterField( 30 | model_name='action', 31 | name='plan_comment', 32 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 33 | ), 34 | migrations.AlterField( 35 | model_name='action', 36 | name='status_comment', 37 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 38 | ), 39 | migrations.AlterField( 40 | model_name='audit', 41 | name='conclusion', 42 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 43 | ), 44 | migrations.AlterField( 45 | model_name='audit', 46 | name='description', 47 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 48 | ), 49 | migrations.AlterField( 50 | model_name='finding', 51 | name='description', 52 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 53 | ), 54 | migrations.AlterField( 55 | model_name='finding', 56 | name='reference', 57 | field=tinymce.models.HTMLField(blank=True, max_length=4096), 58 | ), 59 | migrations.AlterField( 60 | model_name='measure', 61 | name='description', 62 | field=tinymce.models.HTMLField(blank=True), 63 | ), 64 | ] 65 | -------------------------------------------------------------------------------- /conformity/migrations/0030_control_controlpoint.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-24 23:22 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('conformity', '0001_squashed_0029_alter_action_control_comment_and_more'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Control', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=256)), 22 | ('description', models.TextField(blank=True, max_length=4096)), 23 | ('frequency', models.IntegerField(choices=[('1', 'Yearly'), ('2', 'Half-Yearly'), ('4', 'Quarterly'), ('6', 'Bimonthly'), ('12', 'Monthly')], default='1')), 24 | ('conformity', models.ManyToManyField(blank=True, to='conformity.conformity')), 25 | ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='ControlPoint', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('control_date', models.DateField(default=django.utils.timezone.now)), 33 | ('period_start_date', models.DateField()), 34 | ('period_end_date', models.DateField()), 35 | ('status', models.CharField(choices=[('SCHD', 'Scheduled'), ('TOBE', 'To be evaluated'), ('OK', 'Compliant'), ('NOK', 'Non-Compliant'), ('MISS', 'Missed')], default='SCHD', max_length=4)), 36 | ('comment', models.TextField(blank=True, max_length=4096)), 37 | ('control', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.control')), 38 | ('control_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /conformity/migrations/0008_audit_finding.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-07-09 15:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('conformity', '0007_alter_conformity_options_alter_measure_options_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Audit', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('description', models.CharField(max_length=4096)), 19 | ('conclusion', models.CharField(max_length=4096)), 20 | ('auditors', models.CharField(max_length=256)), 21 | ('start_date', models.DateField()), 22 | ('end_date', models.DateField()), 23 | ('report_date', models.DateField()), 24 | ('type', models.CharField(choices=[('INT', 'Internal Audit'), ('CUS', 'Customer Audit'), ('NAT', 'National Authority'), ('AUD', '3rd party auditor'), ('OTHER', 'Other')], default='OTHER', max_length=5)), 25 | ('audited_policies', models.ManyToManyField(blank=True, to='conformity.policy')), 26 | ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.organization')), 27 | ], 28 | options={ 29 | 'ordering': ['report_date'], 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='Finding', 34 | fields=[ 35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('short_description', models.CharField(max_length=256)), 37 | ('description', models.CharField(max_length=4096)), 38 | ('reference', models.CharField(max_length=4096)), 39 | ('type', models.CharField(choices=[('POS', 'Positive element'), ('OBS', 'Observation or Opportunity For Improvement'), ('MIN', 'Minor non-conformity'), ('MAJ', 'Major non-conformity'), ('CRIT', 'Critical non-conformity'), ('OTHER', 'Other remark')], default='OBS', max_length=5)), 40 | ('audit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.audit')), 41 | ], 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /conformity/migrations/0061_remove_requirement_legacy_is_parent_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.4 on 2025-08-22 21:25 2 | 3 | import django.core.validators 4 | import django.db.models.deletion 5 | import mptt.fields 6 | from django.db import migrations, models 7 | 8 | def rebuild_mptt(apps, schema_editor): 9 | Requirement = apps.get_model('conformity', 'Requirement') 10 | from mptt import register 11 | try: 12 | register(Requirement) 13 | except Exception: 14 | pass 15 | Requirement._tree_manager.rebuild() 16 | 17 | class Migration(migrations.Migration): 18 | 19 | dependencies = [ 20 | ('conformity', '0060_pre_MPTT_migration'), 21 | ] 22 | 23 | operations = [ 24 | migrations.RemoveField( 25 | model_name='requirement', 26 | name='legacy_is_parent', 27 | ), 28 | migrations.RemoveField( 29 | model_name='requirement', 30 | name='legacy_level', 31 | ), 32 | migrations.AddField( 33 | model_name='requirement', 34 | name='level', 35 | field=models.PositiveIntegerField(default=0, editable=False), 36 | preserve_default=False, 37 | ), 38 | migrations.AddField( 39 | model_name='requirement', 40 | name='lft', 41 | field=models.PositiveIntegerField(default=0, editable=False), 42 | preserve_default=False, 43 | ), 44 | migrations.AddField( 45 | model_name='requirement', 46 | name='rght', 47 | field=models.PositiveIntegerField(default=0, editable=False), 48 | preserve_default=False, 49 | ), 50 | migrations.AddField( 51 | model_name='requirement', 52 | name='tree_id', 53 | field=models.PositiveIntegerField(db_index=True, default=0, editable=False), 54 | preserve_default=False, 55 | ), 56 | migrations.AlterField( 57 | model_name='conformity', 58 | name='status', 59 | field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]), 60 | ), 61 | migrations.AlterField( 62 | model_name='requirement', 63 | name='parent', 64 | field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='conformity.requirement'), 65 | ), 66 | migrations.RunPython(rebuild_mptt, migrations.RunPython.noop), 67 | ] 68 | -------------------------------------------------------------------------------- /conformity/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conformity/migrations/0029_alter_action_control_comment_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-17 09:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0028_alter_conformity_measure_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='action', 15 | name='control_comment', 16 | field=models.TextField(blank=True, max_length=4096), 17 | ), 18 | migrations.AlterField( 19 | model_name='action', 20 | name='description', 21 | field=models.TextField(blank=True, max_length=4096), 22 | ), 23 | migrations.AlterField( 24 | model_name='action', 25 | name='implement_comment', 26 | field=models.TextField(blank=True, max_length=4096), 27 | ), 28 | migrations.AlterField( 29 | model_name='action', 30 | name='plan_comment', 31 | field=models.TextField(blank=True, max_length=4096), 32 | ), 33 | migrations.AlterField( 34 | model_name='action', 35 | name='status_comment', 36 | field=models.TextField(blank=True, max_length=4096), 37 | ), 38 | migrations.AlterField( 39 | model_name='audit', 40 | name='conclusion', 41 | field=models.TextField(blank=True, max_length=4096), 42 | ), 43 | migrations.AlterField( 44 | model_name='audit', 45 | name='description', 46 | field=models.TextField(blank=True, max_length=4096), 47 | ), 48 | migrations.AlterField( 49 | model_name='conformity', 50 | name='comment', 51 | field=models.TextField(blank=True, max_length=4096), 52 | ), 53 | migrations.AlterField( 54 | model_name='finding', 55 | name='description', 56 | field=models.TextField(blank=True, max_length=4096), 57 | ), 58 | migrations.AlterField( 59 | model_name='finding', 60 | name='reference', 61 | field=models.TextField(blank=True, max_length=4096), 62 | ), 63 | migrations.AlterField( 64 | model_name='measure', 65 | name='description', 66 | field=models.TextField(blank=True), 67 | ), 68 | migrations.AlterField( 69 | model_name='organization', 70 | name='description', 71 | field=models.TextField(blank=True, max_length=4096), 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /conformity/templates/conformity/controlpoint_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of a control point

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 |

{{ controlpoint.control.title }}

14 |

{{ controlpoint.control.description }}

15 |
    16 |
  • Organization: {{ controlpoint.control.organization }}
  • 17 |
  • Control frequency: {{ controlpoint.control.get_frequency_display }}
  • 18 |
  • Control period: From {{controlpoint.period_start_date}} to {{controlpoint.period_end_date}}
  • 19 |
20 |
21 | Attachments 22 |
    23 | {% for attachment in controlpoint.attachment.all %} 24 |
  • {{ attachment }}
  • 25 | {% empty %} 26 | No attachment 27 | {% endfor %} 28 |
29 |
30 |
31 | {% bootstrap_form form %} 32 | 33 |

Associated actions

34 |
35 | {% for action in controlpoint.get_action %} 36 | 37 | {{action}} {{ action.get_status_display }} 38 | 39 | {% empty %} 40 |

No action associated

41 | {% endfor %} 42 | 43 | Register a corrective action 44 | 45 |
46 |
47 |
48 | {% bootstrap_button button_type="submit" content="Save" %} 49 |
50 |
51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /conformity/middleware.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.signals import user_logged_in 2 | from django.dispatch import receiver 3 | from datetime import datetime 4 | from .models import ControlPoint, IndicatorPoint 5 | 6 | 7 | class SanityCheckMiddleware: 8 | last_checked = datetime.today() # Class variable to store the last check date 9 | 10 | def __init__(self, get_response): 11 | self.get_response = get_response 12 | 13 | def __call__(self, request): 14 | response = self.get_response(request) 15 | self.run_daily_checks() 16 | return response 17 | 18 | def run_daily_checks(self): 19 | """Performs the daily integrity checks.""" 20 | today = datetime.today().date() 21 | 22 | if SanityCheckMiddleware.last_checked != today: 23 | self.check_control_points(today) 24 | self.check_indicator_points(today) 25 | SanityCheckMiddleware.last_checked = today 26 | 27 | @staticmethod 28 | def check_control_points(today): 29 | """Checks and updates the status of ControlPoint.""" 30 | 31 | """Update SCHD to TOBE when period start""" 32 | scheduled_controls = ControlPoint.objects.filter(period_start_date__lte=today, 33 | period_end_date__gte=today, 34 | status="SCHD") 35 | scheduled_controls.update(status='TOBE') 36 | 37 | """Update expired TOBE to MISS """ 38 | missed_controls = ControlPoint.objects.filter(period_start_date__lt=today, 39 | period_end_date__lt=today, 40 | status__in=["TOBE","SCHD"]) 41 | missed_controls.update(status='MISS') 42 | 43 | @staticmethod 44 | def check_indicator_points(today): 45 | """Checks and updates the status of IndicatorPoint.""" 46 | 47 | """Update SCHD to TOBE when period start""" 48 | scheduled_indicators = IndicatorPoint.objects.filter(period_start_date__lte=today, 49 | period_end_date__gte=today, 50 | status="SCHD") 51 | scheduled_indicators.update(status='TOBE') 52 | 53 | """Update expired TOBE to MISS """ 54 | missed_indicators = IndicatorPoint.objects.filter(period_start_date__lt=today, 55 | period_end_date__lt=today, 56 | status__in=["TOBE","SCHD"]) 57 | missed_indicators.update(status='MISS') 58 | 59 | # Connect the user login signal 60 | @receiver(user_logged_in) 61 | def update_on_login(sender, user, request, **kwargs): 62 | middleware = SanityCheckMiddleware(None) 63 | middleware.run_daily_checks() -------------------------------------------------------------------------------- /conformity/static/conformity/conformity.js: -------------------------------------------------------------------------------- 1 | if (document.getElementById('accordion')){ 2 | // Dirty fix for Date form 3 | // https://github.com/zostera/django-bootstrap5/issues/445 4 | // TODO: contribute upstream to django-bootstrap 5 | document.getElementById('id_control_date').type = 'Date' 6 | document.getElementById('id_implement_start_date').type = 'Date' 7 | document.getElementById('id_implement_end_date').type = 'Date' 8 | document.getElementById('id_plan_start_date').type = 'Date' 9 | document.getElementById('id_plan_end_date').type = 'Date' 10 | 11 | // Open the good accordion at page load 12 | if (document.getElementById('id_status').value == '1') { 13 | document.getElementById('id_status_comment').parentNode.classList.add("d-none"); 14 | document.getElementById('buttonAnalyse').classList.remove("collapsed"); 15 | document.getElementById('collapseAnalyse').classList.add("show"); 16 | } else if (document.getElementById('id_status').value == '2') { 17 | document.getElementById('id_status_comment').parentNode.classList.add("d-none"); 18 | document.getElementById('buttonPlan').classList.remove("collapsed"); 19 | document.getElementById('collapsePlan').classList.add("show"); 20 | } else if (document.getElementById('id_status').value == '3') { 21 | document.getElementById('id_status_comment').parentNode.classList.add("d-none"); 22 | document.getElementById('buttonImplement').classList.remove("collapsed"); 23 | document.getElementById('collapseImplement').classList.add("show"); 24 | } else if (document.getElementById('id_status').value == '4') { 25 | document.getElementById('id_status_comment').parentNode.classList.add("d-none"); 26 | document.getElementById('buttonControl').classList.remove("collapsed"); 27 | document.getElementById('collapseControl').classList.add("show"); 28 | } 29 | 30 | // Display the field id_status_comment for MISC status 31 | document.getElementById('id_status').onchange = function(){ 32 | if (document.getElementById('id_status').value > '5') { 33 | document.getElementById('id_status_comment').parentNode.classList.remove("d-none"); 34 | document.getElementById('id_status_comment').required = true; 35 | } else { 36 | document.getElementById('id_status_comment').parentNode.classList.add("d-none"); 37 | document.getElementById('id_status_comment').required = false; 38 | } 39 | }; 40 | } 41 | 42 | if (document.getElementById('id_start_date')){ 43 | // Dirty fix for Date form 44 | // https://github.com/zostera/django-bootstrap5/issues/445 45 | // TODO: contribute upstream to django-bootstrap 46 | document.getElementById('id_start_date').type = 'Date' 47 | document.getElementById('id_end_date').type = 'Date' 48 | document.getElementById('id_report_date').type = 'Date' 49 | } 50 | -------------------------------------------------------------------------------- /conformity/templates/conformity/organization_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Organizations

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% include "includes/filter_row.html" %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for org in organization_list %} 23 | 24 | 27 | 35 | 45 | 53 | 54 | {% empty %} 55 | 58 | {% endfor %} 59 | 65 | 66 |
List of the organizations
OrganizationDescriptionApplicable frameworksActions
25 | {{ org.name }} 26 | 28 |

{{ org.description | linebreaksbr }}

29 | {% if org.administrative_id %} 30 |

Company identifier : {{ org.administrative_id }}

31 | {% else %} 32 |

No identifier

33 | {%endif %} 34 |
36 | {% for item in org.get_frameworks %} 37 | 38 |
39 | {{ item }} 40 |
41 |
42 |
43 | {% endfor %} 44 |
46 |
47 | 49 | 51 |
52 |
60 |
61 | 62 | 63 | 64 |
67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /conformity/templates/conformity/conformity_detail_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

5 | {{ conformity_list.0.organization }} conformity to {{ conformity_list.0.requirement.framework }}: 6 |

7 | {% endblock %} 8 | 9 | {% block content %} 10 |

11 |
12 | {{ conformity_list.0.get_leaf | length }} Requirements 13 | Completeness: {{ conformity_list.0.get_completeness }} % 14 | Conformity: {{ conformity_list.0.status }} % 15 | 16 | 28 |
29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% for con in conformity_list.0.get_children %} 45 | {% include 'conformity/conformity_detail_list_item.html' with con=con style="primary" %} 46 | {% empty %} 47 | 50 | 51 | {% endfor %} 52 |
List and status of framework requirements for the organisation.
RequirementStatusOwnerControlActionCommentEdit
No data to display
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /conformity/templates/conformity/attachment_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Attachment library

5 | {{ object_list | length }} Attachments 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for attachment in attachment_list %} 21 | 22 | 31 | 34 | 37 | 51 | 52 | {% empty %} 53 | 56 | {% endfor %} 57 | 58 |
Attachment library
AttachmentMIME typeCreation dateReferences
23 | 24 | 25 | 27 | {{ attachment | truncatechars:50 }} 28 | 29 | 30 | 32 | {{ attachment.mime_type }} 33 | 35 | {{ attachment.create_date }} 36 | 38 | {% for org in attachment.organizations.all %} 39 | {{ org }} 40 | {% endfor %} 41 | {% for framework in attachment.frameworks.all %} 42 | {{ framework }} 43 | {% endfor %} 44 | {% for cp in attachment.ControlPoint.all %} 45 | {{ cp }} 46 | {% endfor %} 47 | {% for audit in attachment.audits.all %} 48 | {{ audit }} 49 | {% endfor %} 50 |
59 | {% endblock %} -------------------------------------------------------------------------------- /conformity/migrations/0064_indicator_indicatorpoint.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2.4 on 2025-09-03 07:55 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('conformity', '0063_conformity_status_justification_and_more'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Indicator', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=256)), 21 | ('goal', models.TextField(blank=True, max_length=4096)), 22 | ('source', models.TextField(blank=True, max_length=4096)), 23 | ('formula', models.TextField(blank=True, max_length=4096)), 24 | ('worst', models.IntegerField(default=0)), 25 | ('best', models.IntegerField(default=100)), 26 | ('warning', models.IntegerField(default=80)), 27 | ('critical', models.IntegerField(default=90)), 28 | ('frequency', models.IntegerField(choices=[(1, 'Yearly'), (2, 'Half-Yearly'), (4, 'Quarterly'), (6, 'Bimonthly'), (12, 'Monthly')], default=4)), 29 | ('conformity', models.ManyToManyField(blank=True, to='conformity.conformity')), 30 | ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.organization')), 31 | ('responsible', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name='IndicatorPoint', 36 | fields=[ 37 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('control_date', models.DateTimeField(blank=True, null=True)), 39 | ('period_start_date', models.DateField()), 40 | ('period_end_date', models.DateField()), 41 | ('status', models.CharField(choices=[('SCHD', 'Scheduled'), ('TOBE', 'To evaluate'), ('OK', 'Compliant'), ('WARN', 'Warning'), ('CRIT', 'Critical'), ('MISS', 'Missed')], default='SCHD', max_length=4)), 42 | ('comment', models.TextField(blank=True, max_length=4096)), 43 | ('value', models.IntegerField(null=True)), 44 | ('attachment', models.ManyToManyField(blank=True, related_name='IndicatorPoint', to='conformity.attachment')), 45 | ('control_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 46 | ('indicator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.indicator')), 47 | ], 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /conformity/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% load django_bootstrap5 %} 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Oxomium - Log in 12 | 13 | 16 | 19 | 21 | 23 | 24 | 25 |
26 | {% if form.errors %} 27 | 28 | {% endif %} 29 | {% if next and not form.errors and next != "/" %} 30 | {% if user.is_authenticated %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | {% endif %} 36 | 37 |

38 | Oxomium
39 | 40 | 41 | 42 |

43 | {% csrf_token %} 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /conformity/templates/conformity/indicator_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

5 | Indicator "{{ indicator.name }}" 6 |

7 | {% endblock %} 8 | 9 | {% block content %} 10 |

Indicator description

11 |
12 |
13 | 22 |
23 | 24 |
25 |
26 |
27 | Thresholds 28 |
29 |
    30 |
  • Critical: from {{ indicator.worst }} to {{ indicator.critical }}
  • 31 |
  • Warning: from {{ indicator.critical }} to {{ indicator.warning }}
  • 32 |
  • Compliant: from {{ indicator.warning }} to {{ indicator.best }}
  • 33 |
34 |
35 |
36 |
37 | 38 |

Indicator values

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for point in indicator_point_list %} 52 | 53 | 54 | 55 | 56 | 66 | 67 | 68 | {% empty %} 69 | 72 | {% endfor %} 73 | 74 |
List of the indicator point
Period startPeriod endValueStatusComment
{{ point.period_start_date }}{{ point.period_end_date }}{{ point.value|default:"–" }} 57 | {% if point.status == "TOBE" %} 58 | 60 | {{ point.get_status_display }} 61 | 62 | {% else %} 63 | {{ point.get_status_display }} 64 | {% endif %} 65 | {{ point.comment}}
75 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /conformity/migrations/0001_squashed_0014_mesure_is_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-19 21:18 2 | 3 | from django.conf import settings 4 | import django.core.validators 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Policy', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('name', models.CharField(max_length=256)), 23 | ('version', models.IntegerField(default=0)), 24 | ('publish_by', models.CharField(max_length=256)), 25 | ('type', models.CharField(choices=[('INT', 'International Standard'), ('NAT', 'National Standard'), ('TECH', 'Technical Standard'), ('RECO', 'Technical Recomandation'), ('POL', 'Internal Policy'), ('OTHER', 'Other')], default='OTHER', max_length=5)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Mesure', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('code', models.CharField(blank=True, max_length=5)), 33 | ('title', models.CharField(blank=True, max_length=256)), 34 | ('description', models.CharField(blank=True, max_length=4096)), 35 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='conformity.mesure')), 36 | ('policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.policy')), 37 | ('name', models.CharField(blank=True, max_length=50)), 38 | ('level', models.IntegerField(default=0)), 39 | ('order', models.IntegerField(default=1)), 40 | ('is_parent', models.BooleanField(default=False)), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='Organization', 45 | fields=[ 46 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('name', models.CharField(max_length=256)), 48 | ('administrative_id', models.CharField(blank=True, max_length=256)), 49 | ('description', models.CharField(blank=True, max_length=256)), 50 | ('applicable_frameworks', models.ManyToManyField(blank=True, to='conformity.policy')), 51 | ], 52 | ), 53 | migrations.CreateModel( 54 | name='Conformity', 55 | fields=[ 56 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 57 | ('status', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), 58 | ('mesure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.mesure')), 59 | ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.organization')), 60 | ('responsible', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 61 | ], 62 | ), 63 | ] 64 | -------------------------------------------------------------------------------- /conformity/migrations/0015_alter_finding_severity_alter_policy_type_action.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-03 03:43 2 | 3 | from django.conf import settings 4 | import django.core.validators 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('conformity', '0014_alter_finding_description_alter_finding_reference'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='finding', 19 | name='severity', 20 | field=models.CharField(choices=[('CRT', 'Critical non-conformity'), ('MAJ', 'Major non-conformity'), ('MIN', 'Minor non-conformity'), ('OBS', 'Opportunity For Improvement'), ('POS', 'Positive finding'), ('OTHER', 'Other remark')], default='OBS', max_length=5), 21 | ), 22 | migrations.AlterField( 23 | model_name='policy', 24 | name='type', 25 | field=models.CharField(choices=[('INT', 'International Standard'), ('NAT', 'National Standard'), ('TECH', 'Technical Standard'), ('RECO', 'Technical Recommendation'), ('POL', 'Internal Policy'), ('OTHER', 'Other')], default='OTHER', max_length=5), 26 | ), 27 | migrations.CreateModel( 28 | name='Action', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('title', models.CharField(max_length=256)), 32 | ('description', models.CharField(blank=True, max_length=4096)), 33 | ('create_date', models.DateField(auto_now_add=True)), 34 | ('update_date', models.DateField(auto_now=True)), 35 | ('status', models.CharField(choices=[('A', 'Action under analyse'), ('P', 'Action analysed, waiting to be plan'), ('Q', 'Action planned, waiting for implementation'), ('I', 'Implementation in progress'), ('C', 'Implemented, to bo controlled'), ('S', 'Control successfully'), ('U', 'control unsuccessfully'), ('F', 'Frozen'), ('X', 'Canceled')], default='A', max_length=5)), 36 | ('plan_start_date', models.DateField(blank=True, null=True)), 37 | ('plan_end_sate', models.DateField(blank=True, null=True)), 38 | ('plan_comment', models.CharField(blank=True, max_length=4096)), 39 | ('implement_start_date', models.DateField(blank=True, null=True)), 40 | ('implement_end_date', models.DateField(blank=True, null=True)), 41 | ('implement_status', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), 42 | ('implement_comment', models.CharField(blank=True, max_length=4096)), 43 | ('control_date', models.DateField(blank=True, null=True)), 44 | ('control_comment', models.CharField(blank=True, max_length=4096)), 45 | ('associated_conformity', models.ManyToManyField(blank=True, to='conformity.conformity')), 46 | ('associated_findings', models.ManyToManyField(blank=True, to='conformity.finding')), 47 | ('control_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='controller', to=settings.AUTH_USER_MODEL)), 48 | ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conformity.organization')), 49 | ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /conformity/templates/conformity/control_detail_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load render_table from django_tables2 %} 3 | 4 | {% block header %} 5 |

6 | Control detail 7 |

8 |

9 | {% if control.level == 1 %} 10 | {% elif control.level == 2 %} 11 | {%endif %} {{ control.get_level_display }} 12 | {{ control.get_frequency_display }} 13 | {{ control.organization }} 14 |

15 | {% endblock %} 16 | 17 | {% block content %} 18 |

Titre : {{ control.title }}

19 |

Description : {{ control.description }}

20 |

Control history :

21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for cp in control.controlpoint %} 38 | 39 | 42 | 45 | 48 | 66 | 75 | 76 | {% empty %} 77 | 80 | 81 | {% endfor %} 82 | 88 |
List of all controls
Start DateEnd DateOwnerStatusEdit
40 | {{ cp.period_start_date | date:'d-M-Y'}} 41 | 43 | {{ cp.period_end_date | date:'d-M-Y'}} 44 | 46 | {{ cp.control_user| default_if_none:"" }} 47 | 49 | {% if cp.status == "SCHD" %} 50 | 51 | {% endif %} 52 | {% if cp.status == "TOBE" %} 53 | 54 | {% endif %} 55 | {% if cp.status == "NOK" %} 56 | 57 | {% endif %} 58 | {% if cp.status == "OK" %} 59 | 60 | {% endif %} 61 | {% if cp.status == "MISS" %} 62 | 63 | {% endif %} 64 | {{ cp.get_status_display }} 65 | 67 | 68 | {% if cp.status == "TOBE" %} 69 | 70 | {% else %} 71 | 72 | {% endif %} 73 | 74 |
No data to display
83 |
84 | 85 | 86 | 87 |
89 |
90 |
91 |
92 | {% endblock %} 93 | -------------------------------------------------------------------------------- /conformity/filterset.py: -------------------------------------------------------------------------------- 1 | from cProfile import label 2 | from random import choices 3 | 4 | from django_filters import FilterSet, CharFilter, DateFromToRangeFilter, ModelChoiceFilter, ChoiceFilter, NumberFilter 5 | from .models import Action, Control, ControlPoint, Conformity, Finding, Requirement, Framework, Organization, Audit, \ 6 | Indicator 7 | 8 | 9 | class ActionFilter(FilterSet): 10 | title = CharFilter(lookup_expr='icontains', label="Title") 11 | associated_conformity = ModelChoiceFilter(queryset=Conformity.objects.all(), label='Conformity') 12 | associated_findings = ModelChoiceFilter(queryset=Finding.objects.all(), label='Finding') 13 | associated_controlPoints = ModelChoiceFilter(queryset=ControlPoint.objects.all(), label='Control Point') 14 | 15 | class Meta: 16 | model = Action 17 | fields = ['title', 'owner', 'status', 'organization', 18 | 'associated_conformity', 'associated_findings', 'associated_controlPoints'] 19 | 20 | 21 | class ControlFilter(FilterSet): 22 | title = CharFilter(lookup_expr='icontains', label="Title") 23 | conformity = ModelChoiceFilter(queryset=Conformity.objects.all(), label='Conformity') 24 | 25 | class Meta: 26 | model = Control 27 | fields = ['title', 'level', 'frequency', 'organization', 28 | 'conformity'] 29 | 30 | 31 | class ControlPointFilter(FilterSet): 32 | control = ModelChoiceFilter(queryset=Control.objects.all(), label='Control') 33 | class Meta: 34 | model = ControlPoint 35 | fields = [ 'status', 36 | 'control', 'control__frequency'] 37 | 38 | class FrameworkFilter(FilterSet): 39 | name = CharFilter(lookup_expr='icontains', label="Name") 40 | publish_by = CharFilter(lookup_expr='icontains', label="Publish by") 41 | type = ChoiceFilter(choices=Framework.Type.choices, label='Type') 42 | language = ChoiceFilter(choices=Framework.Language.choices, label='Language') 43 | 44 | class Meta: 45 | model = Framework 46 | fields = [ 'name', 'version', 'language', 'publish_by', 'type' ] 47 | 48 | class OrganizationFilter(FilterSet): 49 | name = CharFilter(lookup_expr='icontains', label="Name") 50 | description = CharFilter(lookup_expr='icontains', label="Description") 51 | administrative_id = CharFilter(lookup_expr='icontains', label="Administrative identifier") 52 | 53 | class Meta: 54 | model = Organization 55 | fields = [ 'name', 'description', 'administrative_id' ] 56 | 57 | class ConformityFilter(FilterSet): 58 | organization = ModelChoiceFilter(queryset=Organization.objects.all(), label="Organization") 59 | requirement__framework = ModelChoiceFilter(queryset=Framework.objects.all(), label="Framework") 60 | 61 | class Meta: 62 | model = Conformity 63 | fields = [ 'organization', 'requirement__framework' ] 64 | 65 | class AuditFilter(FilterSet): 66 | name = CharFilter(lookup_expr='icontains', label='Name') 67 | auditor = CharFilter(lookup_expr='icontains', label='Auditor') 68 | type = ChoiceFilter(choices=Audit.Type.choices, label='Type') 69 | 70 | class Meta: 71 | model = Audit 72 | fields = [ 'name', 'auditor', 'type' ] 73 | 74 | class FindingFilter(FilterSet): 75 | name = CharFilter(lookup_expr='icontains', label='Name') 76 | short_description = CharFilter(lookup_expr='icontains', label='Short Description') 77 | cvss = CharFilter(lookup_expr='icontains', label='CVSS') 78 | 79 | class Meta: 80 | model = Finding 81 | fields = [ 'name', 'short_description', 'cvss'] 82 | 83 | class IndicatorFilter(FilterSet): 84 | name = CharFilter(lookup_expr='icontains', label='Name') 85 | goal = CharFilter(lookup_expr='icontains', label='Goal') 86 | 87 | class Meta: 88 | model = Indicator 89 | fields = [ 'name', 'goal' ] -------------------------------------------------------------------------------- /conformity/templates/auditlog/logentry_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Audit Log

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% include "includes/pagination.html" with page_obj=page_obj %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for logentry in logentry_list %} 24 | {% if logentry.object_id is not Null %} 25 | 26 | 29 | 33 | 47 | 51 | 66 | 67 | {% endif %} 68 | {% empty %} 69 | 72 | 73 | {% endfor %} 74 | 75 |
Audit trail of the actions on Oxomium
DateUserActionRessourceChange
27 | {{ logentry.timestamp | date:'d-M-Y H:i T' }} 28 | 30 | {{ logentry.actor | default_if_none:"Oxomium" }} 31 | {{ logentry.remote_addr | default_if_none:"" }} 32 | 34 | {% if logentry.action is 0 %} 35 | CREATE 36 | {% endif %} 37 | {% if logentry.action is 1 %} 38 | UPDATE 39 | {% endif %} 40 | {% if logentry.action is 2 %} 41 | DELETE 42 | {% endif %} 43 | {% if logentry.action is 3 %} 44 | ACCESS 45 | {% endif %} 46 | 48 | {{ logentry.object_repr | truncatechars:30 }} 49 | {{ logentry.content_type}} 50 | 52 |
    53 | {% for key, value in logentry.changes_display_dict.items %} 54 | {% if logentry.action is 0 %} 55 |
  • {{ key }}: {{ value.1 |truncatechars:40 }}
  • 56 | {% endif %} 57 | {% if logentry.action is 1 %} 58 |
  • {{ key }}: {{ value.0 | truncatechars:20 }}{{ value.1 |truncatechars:20 }}
  • 59 | {% endif %} 60 | {% if logentry.action is 2 %} 61 |
  • {{ key }}: {{ value.0 | truncatechars:40 }}
  • 62 | {% endif %} 63 | {% endfor %} 64 |
65 |
No data to display
76 | 77 | {% include "includes/pagination.html" with page_obj=page_obj %} 78 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /conformity/templates/conformity/finding_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

5 | Finding "{{ finding.short_description }}" 6 | {% if finding.severity == "CRT" %} 7 | Critical ({{ finding.cvss }}) 8 | {% endif %} 9 | {% if finding.severity == "MAJ" %} 10 | Major ({{ finding.cvss }}) 11 | {% endif %} 12 | {% if finding.severity == "MIN" %} 13 | Minor ({{ finding.cvss }}) 14 | {% endif %} 15 | {% if finding.severity == "OBS" %} 16 | Opportunity ({{ finding.cvss }}) 17 | {% endif %} 18 | {% if finding.severity == "OTHER" %} 19 | Remark ({{ finding.cvss }}) 20 | {% endif %} 21 | {% if finding.severity == "POS" %} 22 | Positive ({{ finding.cvss }}) 23 | {% endif %} 24 | 25 |

26 | {% endblock %} 27 | 28 | {% block content %} 29 | {% if finding.archived %} 30 | 33 | {% endif %} 34 |

35 | Audit of {{ audit.organization }} realised by {{ audit.auditor }} ({{ audit.get_type }}). 36 | See audit 37 |

38 | 39 |

Description

40 |

{{ finding.description | default:"No detail provided" | linebreaksbr }}

41 | 42 |

Observation

43 |

{{ finding.observation | default:"No detail provided" | linebreaksbr }}

44 | 45 |

Scoring

46 |

47 | CVSS Score: {{finding.cvss}} 48 |
49 | CVSS Vector: 50 | {% if finding.cvss_descriptor %} 51 |
52 | 53 | {{ finding.cvss_descriptor }} 54 | 55 | 56 | {%endif%} 57 |

58 | 59 |

Recommendation

60 |

{{ finding.recommendation | linebreaksbr }}

61 | 62 |

Associated action

63 |
64 | {% for action in finding.get_action %} 65 | 66 | {{action}} {{ action.get_status_display }} 67 | 68 | {% empty %} 69 |

No action associated

70 | {% endfor %} 71 | {% if not finding.archived %} 72 | 73 | Register a corrective action 74 | 75 | {% endif %} 76 |
77 | 78 | {% if finding.reference %} 79 |

External Reference

80 |

{{ finding.reference }}

81 | {% endif %} 82 | 83 | {% if not finding.archived %} 84 | 85 | 86 | 87 | {% endif %} 88 | {% endblock %} 89 | -------------------------------------------------------------------------------- /conformity/migrations/0052_framework_language.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-07-20 00:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('conformity', '0051_alter_action_options_alter_attachment_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='framework', 15 | name='language', 16 | field=models.CharField(choices=[('aa', 'Afar'), ('ab', 'Abkhazian'), ('af', 'Afrikaans'), ('ak', 'Akan'), ('am', 'Amharic'), ('ar', 'Arabic'), ('an', 'Aragonese'), ('as', 'Assamese'), ('av', 'Avaric'), ('ae', 'Avestan'), ('ay', 'Aymara'), ('az', 'Azerbaijani'), ('ba', 'Bashkir'), ('bm', 'Bambara'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('bi', 'Bislama'), ('bo', 'Tibetan'), ('bs', 'Bosnian'), ('br', 'Breton'), ('bg', 'Bulgarian'), ('ca', 'Catalan'), ('cs', 'Czech'), ('ch', 'Chamorro'), ('ce', 'Chechen'), ('cu', 'Church Slavic'), ('cv', 'Chuvash'), ('kw', 'Cornish'), ('co', 'Corsican'), ('cr', 'Cree'), ('cy', 'Welsh'), ('da', 'Danish'), ('de', 'German'), ('dv', 'Dhivehi'), ('dz', 'Dzongkha'), ('el', 'Modern Greek (1453-)'), ('en', 'English'), ('eo', 'Esperanto'), ('et', 'Estonian'), ('eu', 'Basque'), ('ee', 'Ewe'), ('fo', 'Faroese'), ('fa', 'Persian'), ('fj', 'Fijian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Western Frisian'), ('ff', 'Fulah'), ('gd', 'Scottish Gaelic'), ('ga', 'Irish'), ('gl', 'Galician'), ('gv', 'Manx'), ('gn', 'Guarani'), ('gu', 'Gujarati'), ('ht', 'Haitian'), ('ha', 'Hausa'), ('sh', 'Serbo-Croatian'), ('he', 'Hebrew'), ('hz', 'Herero'), ('hi', 'Hindi'), ('ho', 'Hiri Motu'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('hy', 'Armenian'), ('ig', 'Igbo'), ('io', 'Ido'), ('ii', 'Sichuan Yi'), ('iu', 'Inuktitut'), ('ie', 'Interlingue'), ('ia', 'Interlingua (International Auxiliary Language Association)'), ('id', 'Indonesian'), ('ik', 'Inupiaq'), ('is', 'Icelandic'), ('it', 'Italian'), ('jv', 'Javanese'), ('ja', 'Japanese'), ('kl', 'Kalaallisut'), ('kn', 'Kannada'), ('ks', 'Kashmiri'), ('ka', 'Georgian'), ('kr', 'Kanuri'), ('kk', 'Kazakh'), ('km', 'Khmer'), ('ki', 'Kikuyu'), ('rw', 'Kinyarwanda'), ('ky', 'Kirghiz'), ('kv', 'Komi'), ('kg', 'Kongo'), ('ko', 'Korean'), ('kj', 'Kuanyama'), ('ku', 'Kurdish'), ('lo', 'Lao'), ('la', 'Latin'), ('lv', 'Latvian'), ('li', 'Limburgan'), ('ln', 'Lingala'), ('lt', 'Lithuanian'), ('lb', 'Luxembourgish'), ('lu', 'Luba-Katanga'), ('lg', 'Ganda'), ('mh', 'Marshallese'), ('ml', 'Malayalam'), ('mr', 'Marathi'), ('mk', 'Macedonian'), ('mg', 'Malagasy'), ('mt', 'Maltese'), ('mn', 'Mongolian'), ('mi', 'Maori'), ('ms', 'Malay (macrolanguage)'), ('my', 'Burmese'), ('na', 'Nauru'), ('nv', 'Navajo'), ('nr', 'South Ndebele'), ('nd', 'North Ndebele'), ('ng', 'Ndonga'), ('ne', 'Nepali (macrolanguage)'), ('nl', 'Dutch'), ('nn', 'Norwegian Nynorsk'), ('nb', 'Norwegian Bokmål'), ('no', 'Norwegian'), ('ny', 'Nyanja'), ('oc', 'Occitan (post 1500)'), ('oj', 'Ojibwa'), ('or', 'Oriya (macrolanguage)'), ('om', 'Oromo'), ('os', 'Ossetian'), ('pa', 'Panjabi'), ('pi', 'Pali'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('ps', 'Pushto'), ('qu', 'Quechua'), ('rm', 'Romansh'), ('ro', 'Romanian'), ('rn', 'Rundi'), ('ru', 'Russian'), ('sg', 'Sango'), ('sa', 'Sanskrit'), ('si', 'Sinhala'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('se', 'Northern Sami'), ('sm', 'Samoan'), ('sn', 'Shona'), ('sd', 'Sindhi'), ('so', 'Somali'), ('st', 'Southern Sotho'), ('es', 'Spanish'), ('sq', 'Albanian'), ('sc', 'Sardinian'), ('sr', 'Serbian'), ('ss', 'Swati'), ('su', 'Sundanese'), ('sw', 'Swahili (macrolanguage)'), ('sv', 'Swedish'), ('ty', 'Tahitian'), ('ta', 'Tamil'), ('tt', 'Tatar'), ('te', 'Telugu'), ('tg', 'Tajik'), ('tl', 'Tagalog'), ('th', 'Thai'), ('ti', 'Tigrinya'), ('to', 'Tonga (Tonga Islands)'), ('tn', 'Tswana'), ('ts', 'Tsonga'), ('tk', 'Turkmen'), ('tr', 'Turkish'), ('tw', 'Twi'), ('ug', 'Uighur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('ve', 'Venda'), ('vi', 'Vietnamese'), ('vo', 'Volapük'), ('wa', 'Walloon'), ('wo', 'Wolof'), ('xh', 'Xhosa'), ('yi', 'Yiddish'), ('yo', 'Yoruba'), ('za', 'Zhuang'), ('zh', 'Chinese'), ('zu', 'Zulu')], default='en', max_length=2), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /conformity/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Customize Django Admin Site to manage my Models instances 3 | """ 4 | 5 | from django.contrib import admin 6 | from import_export import resources 7 | from import_export.admin import ImportExportModelAdmin 8 | from .models import Organization, Framework, Requirement, Conformity, Audit, Finding, Action, Control, ControlPoint, \ 9 | Attachment, Indicator, IndicatorPoint 10 | 11 | 12 | class OrganizationResources(resources.ModelResource): 13 | class Meta: 14 | model = Organization 15 | 16 | 17 | class OrganizationAdmin(ImportExportModelAdmin): 18 | ressource_class = Organization 19 | list_select_related = ['applicable_frameworks'] 20 | 21 | 22 | class FrameworkResources(resources.ModelResource): 23 | class Meta: 24 | model = Framework 25 | 26 | 27 | class FrameworkAdmin(ImportExportModelAdmin): 28 | ressource_class = Framework 29 | list_select_related = ['attachment'] 30 | 31 | 32 | class RequirementResources(resources.ModelResource): 33 | class Meta: 34 | model = Requirement 35 | 36 | 37 | class RequirementAdmin(ImportExportModelAdmin): 38 | ressource_class = Requirement 39 | list_select_related = ['framework', 'parent'] 40 | 41 | 42 | class ConformityResources(resources.ModelResource): 43 | class Meta: 44 | model = Conformity 45 | 46 | 47 | class ConformityAdmin(ImportExportModelAdmin): 48 | ressource_class = Conformity 49 | list_select_related = ['organization', 'requirement'] 50 | 51 | 52 | class ActionResources(resources.ModelResource): 53 | class Meta: 54 | model = Action 55 | 56 | 57 | class ActionAdmin(ImportExportModelAdmin): 58 | ressource_class = Action 59 | list_select_related = ['organization', 'responsible'] 60 | 61 | 62 | class ControlResources(resources.ModelResource): 63 | class Meta: 64 | model = Control 65 | 66 | 67 | class ControlAdmin(ImportExportModelAdmin): 68 | ressource_class = Control 69 | list_select_related = ['organization'] 70 | 71 | 72 | class ControlPointResources(resources.ModelResource): 73 | class Meta: 74 | model = ControlPoint 75 | 76 | 77 | class ControlPointAdmin(ImportExportModelAdmin): 78 | ressource_class = ControlPoint 79 | list_select_related = ['control', 'control_user'] 80 | 81 | 82 | class FindingResources(resources.ModelResource): 83 | class Meta: 84 | model = Finding 85 | 86 | 87 | class FindingAdmin(ImportExportModelAdmin): 88 | ressource_class = Finding 89 | list_select_related = ['audit'] 90 | 91 | 92 | class AuditResources(resources.ModelResource): 93 | class Meta: 94 | model = Audit 95 | 96 | 97 | class AuditAdmin(ImportExportModelAdmin): 98 | ressource_class = Audit 99 | list_select_related = ['organization', 'auditor'] 100 | 101 | 102 | class AttachmentResources(resources.ModelResource): 103 | class Meta: 104 | model = Attachment 105 | 106 | class AttachmentAdmin(ImportExportModelAdmin): 107 | ressource_class = Attachment 108 | 109 | class IndicatorResources(resources.ModelResource): 110 | class Meta: 111 | model = Indicator 112 | 113 | class IndicatorAdmin(ImportExportModelAdmin): 114 | ressource_class = Indicator 115 | 116 | class IndicatorPointResources(resources.ModelResource): 117 | class Meta: 118 | model = IndicatorPoint 119 | 120 | class IndicatorPointAdmin(ImportExportModelAdmin): 121 | ressource_class = IndicatorPoint 122 | 123 | 124 | # Registration 125 | admin.site.register(Action, ActionAdmin) 126 | admin.site.register(Attachment, AttachmentAdmin) 127 | admin.site.register(Audit, AuditAdmin) 128 | admin.site.register(Conformity, ConformityAdmin) 129 | admin.site.register(Control, ControlAdmin) 130 | admin.site.register(ControlPoint, ControlPointAdmin) 131 | admin.site.register(Finding, FindingAdmin) 132 | admin.site.register(Framework, FrameworkAdmin) 133 | admin.site.register(Organization, OrganizationAdmin) 134 | admin.site.register(Requirement, RequirementAdmin) 135 | admin.site.register(Indicator, IndicatorAdmin) 136 | admin.site.register(IndicatorPoint, IndicatorPointAdmin) 137 | -------------------------------------------------------------------------------- /conformity/templates/conformity/audit_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Audits

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 |
11 | 12 |
13 | 16 | 26 |
27 | 28 |
29 | 31 | Export 32 | 35 | 39 |
40 | 41 |
42 | 43 | {{ object_list | length }} items 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {% for audit in audit_list %} 62 | 63 | 66 | 69 | 72 | 75 | 78 | 81 | 89 | 90 | {% empty %} 91 | 94 | 95 | {% endfor %} 96 | 102 |
List of all Audits
NameAuditorTypeStartEndFindingActions
64 | {{ audit.name }} 65 | 67 | {{ audit.auditor }} 68 | 70 | {{ audit.get_type }} 71 | 73 | {{ audit.start_date }} 74 | 76 | {{ audit.end_date }} 77 | 79 | {{ audit.get_findings_number }} 80 | 82 |
83 | 85 | 87 |
88 |
No data to display
97 |
98 | 99 | 100 | 101 |
103 | {% endblock %} 104 | -------------------------------------------------------------------------------- /conformity/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conformity module URL router 3 | """ 4 | from django.urls import path 5 | from django.views.generic import TemplateView 6 | 7 | from . import views 8 | 9 | app_name = 'conformity' 10 | urlpatterns = [ 11 | path('', views.HomeView.as_view(), name='home'), 12 | 13 | path('audit/', views.AuditIndexView.as_view(), name='audit_index'), 14 | path('audit/', views.AuditDetailView.as_view(), name='audit_detail'), 15 | path('audit/create', views.AuditCreateView.as_view(), name='audit_create'), 16 | path('audit/update/', views.AuditUpdateView.as_view(), name='audit_form'), 17 | path('audit/export/', views.AuditExportView.as_view(), name='audit_export'), 18 | 19 | path('conformity/', views.ConformityIndexView.as_view(), name='conformity_index'), 20 | path('conformity/update/', views.ConformityUpdateView.as_view(), name='conformity_form'), 21 | path('conformity/organization//framework//', views.ConformityDetailIndexView.as_view(), 22 | name='conformity_detail_index'), 23 | path('conformity/organization//framework//export/', views.ConformityExportView.as_view(), 24 | name='conformity_export'), 25 | 26 | path('finding/', views.FindingIndexView.as_view(), name='finding_index'), 27 | path('finding/', views.FindingDetailView.as_view(), name='finding_detail'), 28 | path('finding/create', views.FindingCreateView.as_view(), name='finding_create'), 29 | path('finding/update/', views.FindingUpdateView.as_view(), name='finding_form'), 30 | path('finding/export', views.FindingExportView.as_view(), name='finding_export'), 31 | 32 | path('organization/', views.OrganizationIndexView.as_view(), name='organization_index'), 33 | path('organization/', views.OrganizationDetailView.as_view(), name='organization_detail'), 34 | path('organization/create', views.OrganizationCreateView.as_view(), name='organization_create'), 35 | path('organization/update/', views.OrganizationUpdateView.as_view(), name='organization_form'), 36 | 37 | path('framework/', views.FrameworkIndexView.as_view(), name='framework_index'), 38 | path('framework//', views.FrameworkDetailView.as_view(), name='framework_detail'), 39 | 40 | path('action/', views.ActionIndexView.as_view(), name='action_index'), 41 | path('action/create', views.ActionCreateView.as_view(), name='action_create'), 42 | path('action/update/', views.ActionUpdateView.as_view(), name='action_form'), 43 | path('action/export', views.ActionExportView.as_view(), name='action_export'), 44 | 45 | path('control/', views.ControlIndexView.as_view(), name='control_index'), 46 | path('control/create', views.ControlCreateView.as_view(), name='control_create'), 47 | path('control/update/', views.ControlUpdateView.as_view(), name='control_form'), 48 | path('control/', views.ControlDetailView.as_view(), name='control_detail'), 49 | path('control/export', views.ControlExportView.as_view(), name='control_export'), 50 | 51 | path('controlpoint/', views.ControlPointIndexView.as_view(), name='controlpoint_index'), 52 | path('controlpoint/update/', views.ControlPointUpdateView.as_view(), name='controlpoint_form'), 53 | 54 | path('indicator/', views.IndicatorIndexView.as_view(), name='indicator_index'), 55 | path('indicator/create', views.IndicatorCreateView.as_view(), name='indicator_create'), 56 | path('indicator/update//', views.IndicatorUpdateView.as_view(), name='indicator_form'), 57 | path('indicator//', views.IndicatorDetailView.as_view(), name='indicator_detail'), 58 | path('indicator/export', views.IndicatorExportView.as_view(), name='indicator_export'), 59 | path('indicatorpoint//', views.IndicatorPointUpdateView.as_view(), name='indicatorpoint_form'), 60 | 61 | path('attachment/', views.AttachmentIndexView.as_view(), name='attachment_index'), 62 | path('attachment//', views.AttachmentDownloadView.as_view(), name='attachment_download'), 63 | 64 | path('help/', TemplateView.as_view(template_name='help.html'), name='help'), 65 | path('auditlog/', views.AuditLogDetailView.as_view(), name='auditlog_index'), 66 | ] 67 | -------------------------------------------------------------------------------- /conformity/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import m2m_changed, pre_save, post_save 2 | from django.dispatch import receiver 3 | from .models import Organization, Requirement, Control, ControlPoint, Attachment, Action, Finding, Conformity, \ 4 | Indicator, IndicatorPoint 5 | 6 | 7 | @receiver(post_save, sender=Control) 8 | def control_post_save_bootstrap(instance: Control, **kwargs): 9 | Control.controlpoint_bootstrap(instance) 10 | 11 | @receiver(pre_save, sender=ControlPoint) 12 | def controlpoint_pre_save_status(sender, instance: ControlPoint, **kwargs): 13 | ControlPoint.update_status(instance) 14 | 15 | @receiver(pre_save, sender=Attachment) 16 | def attachment_pre_autoset_mimetype(sender, instance: Attachment, **kwargs): 17 | Attachment.autoset_mimetype(instance) 18 | 19 | @receiver(pre_save, sender=Requirement) 20 | def requirement_pre_save_naming(instance, **kwargs): 21 | """This function keep hierarchy of the Requirement working on each Requirement instantiation""" 22 | if instance.parent: 23 | instance.name = f"{instance.parent.name}-{instance.code}" 24 | else: 25 | instance.name = instance.code 26 | 27 | @receiver(m2m_changed, sender=Organization.applicable_frameworks.through) 28 | def change_framework(instance, action, pk_set, **kwargs): 29 | if action == "post_add": 30 | for pk in pk_set: 31 | instance.add_conformity(pk) 32 | 33 | if action == "post_remove": 34 | for pk in pk_set: 35 | instance.remove_conformity(pk) 36 | 37 | @receiver(post_save, sender=Action) 38 | def action_post_save_sync_findings(instance: Action, **kwargs): 39 | """ 40 | When an Action is saved (status/active may have changed), 41 | re-evaluate the archive state of all linked Findings. 42 | """ 43 | for f in instance.associated_findings.all(): 44 | f.update_archived() 45 | 46 | @receiver(m2m_changed, sender=Action.associated_findings.through) 47 | def action_finding_sync_on_m2m(instance, action, reverse, pk_set, **kwargs): 48 | """ 49 | Keep Finding.archived consistent when Action<->Finding links change. 50 | 51 | - reverse=False: `instance` is an Action; `pk_set` are Finding IDs added/removed. 52 | - reverse=True: `instance` is a Finding; re-evaluate that single Finding. 53 | """ 54 | if action not in {'post_add', 'post_remove', 'post_clear'}: 55 | return 56 | 57 | if reverse: 58 | findings = [instance] 59 | else: 60 | findings = Finding.objects.filter(pk__in=pk_set) if pk_set else instance.associated_findings.all() 61 | 62 | for f in findings: 63 | f.update_archived() 64 | 65 | @receiver(post_save, sender=ControlPoint) 66 | def controlpoint_post_save_sync(instance: ControlPoint, **kwargs): 67 | """ 68 | Transactional rule: 69 | - NONCOMPLIANT (current period) -> conformity = 0 (CTRL) 70 | - COMPLIANT (current period) -> try conformity = 100 (CTRL) if no negatives remain 71 | """ 72 | if instance.is_current_period() and instance.is_final_status(): 73 | for conf in instance.control.conformity.all(): 74 | if instance.status == ControlPoint.Status.NONCOMPLIANT: 75 | conf.set_status_from(0, Conformity.StatusJustification.CONTROL) 76 | elif instance.status == ControlPoint.Status.COMPLIANT: 77 | conf.set_status_from(100, Conformity.StatusJustification.CONTROL) 78 | 79 | @receiver(post_save, sender=Action) 80 | def action_post_save_sync(instance: Action, **kwargs): 81 | """ 82 | Transactional rule: 83 | - in progress -> conformity = 0 (ACT) 84 | - ended -> try conformity = 100 (ACT) if no negatives remain 85 | """ 86 | for conf in instance.associated_conformity.all(): 87 | if instance.is_in_progress(): 88 | conf.set_status_from(0, Conformity.StatusJustification.ACTION) 89 | elif instance.is_completed(): 90 | conf.set_status_from(100, Conformity.StatusJustification.ACTION) 91 | 92 | @receiver(post_save, sender=Indicator) 93 | def indicator_post_save_bootstrap(instance: Indicator, **kwargs): 94 | instance.indicator_point_init() 95 | 96 | @receiver(pre_save, sender=IndicatorPoint) 97 | def indicatorpoint_pre_save_ctrl(instance: IndicatorPoint, **kwargs): 98 | instance.status_update() -------------------------------------------------------------------------------- /conformity/templates/conformity/conformity_detail_list_item.html: -------------------------------------------------------------------------------- 1 | {% if con.requirement.is_parent and con.requirement.level < 3 %} 2 | 3 | {% else %} 4 | 5 | {% endif %} 6 | 7 | {% if con.requirement.is_leaf_node %} 8 | 9 | {% if con.requirement.level == 1 %} 10 | {{ con.requirement.name }} 11 | {% elif con.requirement.level == 2 %} 12 | {{ con.requirement.name }} 13 | {% else %} 14 | {{ con.requirement.name }} 15 | {% endif %} 16 | {{ con.requirement.title }} 17 | {% else %} 18 | 19 | {% if con.requirement.level == 1 %} 20 | {{ con.requirement.name }} 21 | {% elif con.requirement.level == 2 %} 22 | {{ con.requirement.name }} 23 | {% else %} 24 | {{ con.requirement.name }} 25 | {% endif %} 26 | {{ con.requirement.title }} 27 | {% endif %} 28 | 29 | 30 | 31 |
32 | {% if not con.applicable %} 33 |
Not applicable
35 | {% else %} 36 | {% if con.status is Null %} 37 |
Not Evaluated
39 | {% elif con.status > 79 %} 40 |
{{ con.status }} %
42 | {% elif con.status > 49 %} 43 |
{{ con.status }} %
45 | {% else %} 46 |
{{ con.status }} %
48 | {% endif %} 49 | {% endif %} 50 |
51 | 52 | 53 | 54 | {% if con.applicable and con.responsible%} 55 | {{ con.responsible|capfirst }} 56 | {% endif %} 57 | 58 | 59 | 60 | {% if con.get_control %} 61 | 62 | 66 | 67 | {% endif %} 68 | 69 | 70 | 71 | {% if con.get_action %} 72 | 73 | 77 | 78 | {% endif %} 79 | 80 | 81 | 82 | {% if con.comment %} 83 | 84 | {% endif %} 85 | 86 | 87 | 88 |
89 | 91 |
92 | 93 | 94 | 95 | {% for con in con.get_children %} 96 | {% if con.requirement.level == 1 %} 97 | {% include 'conformity/conformity_detail_list_item.html' with con=con style="primary" %} 98 | {% elif con.requirement.level == 2 %} 99 | {% include 'conformity/conformity_detail_list_item.html' with con=con style="info" %} 100 | {% else %} 101 | {% include 'conformity/conformity_detail_list_item.html' with con=con style="secondary" %} 102 | {% endif %} 103 | {% endfor %} 104 | -------------------------------------------------------------------------------- /conformity/templates/conformity/action_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Actions

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | 11 |
12 | 15 | 25 |
26 | 27 | 39 | 40 |
41 | 42 | {{ object_list | length }} items 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for action in object_list %} 60 | 61 | 64 | 67 | 91 | 94 | 101 | 109 | 110 | {% empty %} 111 | 114 | 115 | {% endfor %} 116 | 123 |
List of all actions
TitreOwnerStatusLast UpdateAssociationActions
62 | {{ action.title }} 63 | 65 | {{ action.owner | default_if_none:"" }} 66 | 68 | {% if action.status == "1" %} 69 | 70 | {% endif %} 71 | {% if action.status == "2" %} 72 | 73 | {% endif %} 74 | {% if action.status == "3" %} 75 | 76 | {% endif %} 77 | {% if action.status == "4" %} 78 | 79 | {% endif %} 80 | {% if action.status == "5" %} 81 | 82 | {% endif %} 83 | {% if action.status == "7" %} 84 | 85 | {% endif %} 86 | {% if action.status == "9" %} 87 | 88 | {% endif %} 89 | {{ action.get_status_display }} 90 | 92 | {{ action.update_date | date:'d-M-Y'}} 93 | 95 | {% if action.get_associated %} 96 | 99 | {% endif %} 100 | 102 |
103 | 105 | 107 |
108 |
No data to display
117 |
118 | 120 | Register a new action 121 | 122 |
124 | {% endblock %} 125 | -------------------------------------------------------------------------------- /conformity/templates/conformity/controlpoint_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load render_table from django_tables2 %} 3 | 4 | {% block header %} 5 |

6 | Control Point 7 |

8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 | {% if filter.form.data.control %} 13 |
14 | 15 |
16 |
17 |

Control

18 |

{{ controlpoint_list.0.control.title }}

19 |
20 |
21 | 22 |

{{ controlpoint_list.0.control.description | linebreaksbr }}

23 | 24 |
    25 |
  • Level: {{ controlpoint_list.0.control.get_level_display }}
  • 26 |
  • Frequency: {{ controlpoint_list.0.control.get_frequency_display }}
  • 27 |
  • Organization: {{ controlpoint_list.0.control.organization }}
  • 28 |
29 | 30 | Associated conformity: 31 |
    32 | {% for conformity in controlpoint_list.0.control.conformity.all %} 33 |
  • {{ conformity.requirement.name }}: {{ conformity.requirement.title }}
  • 34 | {% endfor %} 35 |
36 |
37 |
38 |
39 | {% endif %} 40 | 41 |
42 | {% if filter.form.data.control %} 43 |

Control Point List

44 | {% else %} 45 | {% include "includes/filter_row.html" %} 46 | {% endif %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% for cp in controlpoint_list %} 61 | 62 | 65 | 68 | 71 | 89 | 98 | 99 | {% empty %} 100 | 103 | 104 | {% endfor %} 105 | 108 | 109 |
List of all control points
Start DateEnd DateOwnerStatusEdit
63 | {{ cp.period_start_date | date:'d-M-Y'}} 64 | 66 | {{ cp.period_end_date | date:'d-M-Y'}} 67 | 69 | {{ cp.control_user| default_if_none:"" }} 70 | 72 | {% if cp.status == "SCHD" %} 73 | 74 | {% endif %} 75 | {% if cp.status == "TOBE" %} 76 | 77 | {% endif %} 78 | {% if cp.status == "NOK" %} 79 | 80 | {% endif %} 81 | {% if cp.status == "OK" %} 82 | 83 | {% endif %} 84 | {% if cp.status == "MISS" %} 85 | 86 | {% endif %} 87 | {{ cp.get_status_display }} 88 | 90 | 91 | {% if cp.status == "TOBE" %} 92 | 93 | {% else %} 94 | 95 | {% endif %} 96 | 97 |
No data to display
106 |
107 |
110 |
111 |
112 | 113 |
114 | 120 |
121 | {% endblock %} 122 | -------------------------------------------------------------------------------- /conformity/templates/conformity/finding_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | 3 | {% block header %} 4 |

Active findings

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | 11 |
12 | 15 | 25 |
26 | 27 | 39 | 40 |
41 | 42 | {{ object_list | length }} items 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for finding in finding_list %} 60 | 61 | 64 | 67 | 89 | 96 | 105 | 113 | 114 | {% empty %} 115 | 118 | 119 | {% endfor %} 120 |
List of Critical, Major, Minor finding and Opportunity for improvement.
FindingDescriptionCVSSAudit campaignAssociated actionsAction
62 | {{ finding.name | default:"Unnamed finding" }} 63 | 65 | {{ finding.short_description }} 66 | 68 | {% if finding.severity == "CRT" %} 69 | 70 | {% endif %} 71 | {% if finding.severity == "MAJ" %} 72 | 73 | {% endif %} 74 | {% if finding.severity == "MIN" %} 75 | 76 | {% endif %} 77 | {% if finding.severity == "OBS" %} 78 | 79 | {% endif %} 80 | {% if finding.severity == "OTHER" %} 81 | 82 | {% endif %} 83 | {% if finding.severity == "POS" %} 84 | 85 | {% endif %} 86 | {{ finding.cvss | default:'-'}} 87 | 88 | 90 | 91 | 94 | 95 | 97 | {% if finding.get_action %} 98 | 99 | 102 | 103 | {% endif %} 104 | 106 |
107 | 109 | 111 |
112 |
No data to display
121 | {% endblock %} 122 | -------------------------------------------------------------------------------- /conformity/templates/conformity/indicator_list.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load render_table from django_tables2 %} 3 | 4 | {% block header %} 5 |

Indicators

6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 | 12 |
13 | 16 | 26 |
27 | 28 | 40 | 41 |
42 | 43 | {{ object_list | length }} items 44 | 45 |
46 | 47 | 48 |
49 | {% for indicator in indicator_list %} 50 |
51 |
52 |
53 | {{indicator.name}} 54 |
55 | 57 | 59 |
60 |
61 |
62 | {% if indicator.get_current_point.status == "CRIT" %} 63 |
64 | {{ indicator.get_current_point.value }} 65 | {% elif indicator.get_current_point.status == "WARN" %} 66 |
67 | {{ indicator.get_current_point.value }} 68 | {% elif indicator.get_current_point.status == "OK" %} 69 |
70 | {{ indicator.get_current_point.value }} 71 | {% else %} 72 |
73 | 75 | Add a value 76 | 77 | {% endif %} 78 | 79 | {% if indicator.get_current_point.value %} 80 | {% if indicator.best == 100 and indicator.worst == 0 %} % 81 | {% elif indicator.best == 0 and indicator.worst == 100 %} % 82 | {% else %} / {{indicator.best}} {% endif %} 83 | {% endif %} 84 |
85 |
86 |
87 |

88 | {{indicator.goal }} 89 |

90 |
91 |
    92 |
  • Critical: from {{ indicator.worst }} to {{ indicator.critical }}
  • 93 |
  • Warning: from {{ indicator.critical }} to {{ indicator.warning }}
  • 94 |
  • Compliant: from {{ indicator.warning }} to {{ indicator.best }}
  • 95 |
96 | 102 |
103 |
104 | 105 | {% empty %} 106 | 109 | 110 | {% endfor %} 111 |
112 | 113 | 118 | 119 | {% endblock %} -------------------------------------------------------------------------------- /conformity/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Forms for front-end editing of Models instance 3 | """ 4 | 5 | from django.forms import ModelForm, FileField, ClearableFileInput 6 | from django.utils import timezone 7 | from .models import Conformity, Organization, Audit, Finding, Action, Control, ControlPoint, Indicator, IndicatorPoint 8 | 9 | 10 | class ConformityForm(ModelForm): 11 | class Meta: 12 | model = Conformity 13 | fields = ['applicable', 'responsible', 'status', 'comment'] 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(ConformityForm, self).__init__(*args, **kwargs) 17 | if self.instance.get_descendants().exists(): 18 | self.fields['status'].disabled = True 19 | 20 | 21 | class OrganizationForm(ModelForm): 22 | attachments = FileField(required=False, widget=ClearableFileInput()) 23 | class Meta: 24 | model = Organization 25 | fields = ['name', 'administrative_id', 'description', 'applicable_frameworks'] 26 | 27 | 28 | class AuditForm(ModelForm): 29 | attachments = FileField(required=False, widget=ClearableFileInput()) 30 | class Meta: 31 | model = Audit 32 | fields = ['name', 'organization', 'description', 'conclusion', 'auditor', 'audited_frameworks', 'start_date', 33 | 'end_date', 'report_date', 'type', 'attachments'] 34 | 35 | 36 | class FindingForm(ModelForm): 37 | class Meta: 38 | model = Finding 39 | fields = ['name', 'audit', 'severity', 'short_description', 'description', 'observation', 'recommendation', 'reference', 'cvss', 'cvss_descriptor', 'archived'] 40 | # TODO add a preselection and a disable selector for 'audit' field when the form is open from an audit. 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(FindingForm, self).__init__(*args, **kwargs) 44 | 45 | if self.get_initial_for_field(self.fields['archived'], 'archived') : 46 | for key, value in self.fields.items(): 47 | self.fields[key].disabled = True 48 | 49 | 50 | class ActionForm(ModelForm): 51 | class Meta: 52 | model = Action 53 | fields = '__all__' 54 | 55 | def __init__(self, *args, **kwargs): 56 | super(ActionForm, self).__init__(*args, **kwargs) 57 | self.fields['create_date'].disabled = True 58 | self.fields['update_date'].disabled = True 59 | 60 | generic_fields = ['title', 'owner', 'status', 'status_comment', 'reference'] 61 | analyse_fields = ['organization', 'associated_conformity', 'associated_findings', 'associated_controlPoints', 'description'] 62 | plan_fields = ['plan_start_date', 'plan_end_date', 'plan_comment'] 63 | implement_fields = ['implement_start_date', 'implement_end_date', 'implement_status', 'implement_comment'] 64 | control_fields = ['control_date', 'control_comment', 'control_user'] 65 | fields_by_status = { 66 | Action.Status.ANALYSING.value: generic_fields + analyse_fields, 67 | Action.Status.PLANNING.value: generic_fields + plan_fields, 68 | Action.Status.IMPLEMENTING.value: generic_fields + implement_fields, 69 | Action.Status.CONTROLLING.value: generic_fields + control_fields, 70 | Action.Status.FROZEN.value: generic_fields, 71 | Action.Status.CANCELED.value: generic_fields, 72 | Action.Status.ENDED.value: generic_fields, 73 | } 74 | 75 | for key, value in self.fields.items(): 76 | if key not in fields_by_status[self.get_initial_for_field(self.fields['status'], 'status')]: 77 | self.fields[key].disabled = True 78 | 79 | 80 | class ControlForm(ModelForm): 81 | class Meta: 82 | model = Control 83 | fields = ['title', 'description', 'organization', 'conformity', 'control', 'frequency', 'level'] 84 | 85 | 86 | class ControlPointForm(ModelForm): 87 | attachments = FileField(required=False, widget=ClearableFileInput()) 88 | class Meta: 89 | model = ControlPoint 90 | fields = ['control_date', 'control_user', 'status', 'comment', 'attachments'] 91 | 92 | def __init__(self, *args, **kwargs): 93 | self.user = kwargs.pop('user', None) 94 | super(ControlPointForm, self).__init__(*args, **kwargs) 95 | 96 | # Set some value for all situation 97 | self.fields['control_date'].disabled = True 98 | self.fields['control_user'].disabled = True 99 | 100 | # Set some value if the ControlPoint has to be evaluated 101 | if self.get_initial_for_field(self.fields['status'], 'status') == ControlPoint.Status.TOBEEVALUATED.value: 102 | self.initial['control_date'] = timezone.now() 103 | self.initial['control_user'] = self.user 104 | self.fields['status'].widget.choices = [ 105 | (ControlPoint.Status.COMPLIANT, ControlPoint.Status.COMPLIANT.label), 106 | (ControlPoint.Status.NONCOMPLIANT, ControlPoint.Status.NONCOMPLIANT.label), 107 | ] 108 | # Switch to display mode if ControlPoint is not to be evaluated 109 | else: 110 | del self.fields['attachments'] 111 | for field in self.fields: 112 | self.fields[field].disabled = True 113 | 114 | 115 | class IndicatorForm(ModelForm): 116 | class Meta: 117 | model = Indicator 118 | fields = '__all__' 119 | 120 | 121 | class IndicatorPointForm(ModelForm): 122 | class Meta: 123 | model = IndicatorPoint 124 | fields = ['value', 'comment', 'attachment'] -------------------------------------------------------------------------------- /conformity/templates/conformity/action_form.html: -------------------------------------------------------------------------------- 1 | {% extends "conformity/main.html" %} 2 | {% load django_bootstrap5 %} 3 | 4 | {% block header %} 5 |

Edit of an action record

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% csrf_token %} 11 |
12 |
13 | {% bootstrap_field form.title layout='floating' %} 14 | {% bootstrap_field form.owner layout='floating' %} 15 | {% bootstrap_field form.status layout='floating' %} 16 | {% bootstrap_field form.reference layout='floating' %} 17 | {% bootstrap_field form.status_comment layout='floating' %} 18 |

Created on {{ form.create_date.value }}, last update {{ form.update_date.value }}.

19 |
20 |
21 |

22 | 26 |

27 |
29 |
30 | {% bootstrap_field form.description layout='floating' %} 31 | {% bootstrap_field form.organization layout='floating' %} 32 | {% bootstrap_field form.associated_conformity layout='floating' %} 33 | {% bootstrap_field form.associated_findings layout='floating' %} 34 | {% bootstrap_field form.associated_controlPoints layout='floating' %} 35 |
36 |
37 |
38 |
39 |

40 | 44 |

45 |
47 |
48 | {% bootstrap_field form.plan_start_date layout='floating' %} 49 | {% bootstrap_field form.plan_end_date layout='floating' %} 50 | {% bootstrap_field form.plan_comment layout='floating' %} 51 |
52 |
53 |
54 |
55 |

56 | 60 |

61 |
63 |
64 | {% bootstrap_field form.implement_start_date layout='floating' %} 65 | {% bootstrap_field form.implement_end_date layout='floating' %} 66 | {% bootstrap_field form.implement_status layout='floating' %} 67 | {% bootstrap_field form.implement_comment layout='floating' %} 68 |
69 |
70 |
71 |
72 |

73 | 77 |

78 |
80 |
81 | {% bootstrap_field form.control_date layout='floating' %} 82 | {% bootstrap_field form.control_comment layout='floating' %} 83 | {% bootstrap_field form.control_user layout='floating' %} 84 |
85 |
86 |
87 |
88 |
89 | {% bootstrap_button button_type="submit" content="Save" %} 90 |
91 |
92 |
93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /oxomium/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for oxomium project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | from pathlib import Path 13 | from decouple import config, Csv 14 | from django.conf.locale.az.formats import DATETIME_FORMAT, SHORT_DATETIME_FORMAT 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = config('SECRET_KEY', default='django-insecure-oy-h!94w($b%mb2l-jv&g!1-7iqa9(jc!c=guv=%31_z%yyghr') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = config('DEBUG', default='False', cast=bool) 28 | 29 | # SECURITY WARNING: to be configured before production setup 30 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='[localhost,127.0.0.1]', cast=Csv()) 31 | 32 | # SECURITY WARNING: to be enabled of production usage 33 | SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=False, cast=bool) 34 | SESSION_COOKIE_HTTPONLY = config('SESSION_COOKIE_HTTPONLY', default=False, cast=bool) 35 | SESSION_COOKIE_SAMESITE = config('SESSION_COOKIE_SAMESITE', default='Lax') 36 | SESSION_COOKIE_NAME = config('SESSION_COOKIE_NAME', default='sessionid') 37 | CSRF_USE_SESSIONS = config('CSRF_USE_SESSIONS', default=False, cast=bool) 38 | SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool) 39 | SECURE_HSTS_SECONDS = config('SECURE_HSTS_SECONDS', default=0, cast=int) 40 | SECURE_HSTS_INCLUDE_SUBDOMAINS = config('SECURE_HSTS_INCLUDE_SUBDOMAINS', default=False, cast=bool) 41 | SECURE_HSTS_PRELOAD = config('SECURE_HSTS_PRELOAD', default=False, cast=bool) 42 | 43 | # Application definition 44 | 45 | INSTALLED_APPS = [ 46 | 'django_filters', 47 | 'auditlog', 48 | 'conformity.apps.ConformityConfig', 49 | 'import_export', 50 | 'django_bootstrap5', 51 | 'django_tables2', 52 | 'django.contrib.admin', 53 | 'django.contrib.auth', 54 | 'django.contrib.contenttypes', 55 | 'django.contrib.sessions', 56 | 'django.contrib.messages', 57 | 'django.contrib.staticfiles', 58 | 'mptt', 59 | 'constance', 60 | ] 61 | 62 | MIDDLEWARE = [ 63 | 'django.middleware.security.SecurityMiddleware', 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | 'auditlog.middleware.AuditlogMiddleware', 71 | 'conformity.middleware.SanityCheckMiddleware', 72 | ] 73 | 74 | ROOT_URLCONF = 'oxomium.urls' 75 | 76 | TEMPLATES = [ 77 | { 78 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 79 | 'DIRS': [], 80 | 'APP_DIRS': True, 81 | 'OPTIONS': { 82 | 'context_processors': [ 83 | 'constance.context_processors.config', 84 | 'django.template.context_processors.debug', 85 | 'django.template.context_processors.request', 86 | 'django.contrib.auth.context_processors.auth', 87 | 'django.contrib.messages.context_processors.messages', 88 | ], 89 | }, 90 | }, 91 | ] 92 | 93 | WSGI_APPLICATION = 'oxomium.wsgi.application' 94 | 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 98 | 99 | DATABASES = { 100 | 'default': { 101 | 'ENGINE': 'django.db.backends.sqlite3', 102 | 'NAME': BASE_DIR / config('DB_NAME', default='db.sqlite3'), 103 | } 104 | } 105 | 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [ 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 122 | }, 123 | ] 124 | 125 | 126 | # Internationalization 127 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 128 | 129 | LANGUAGE_CODE = config('LANGUAGE_CODE', default='en-us') 130 | TIME_ZONE = config('TIME_ZONE', default='UTC') 131 | USE_I18N = config('USE_I18N', default='True', cast=bool) 132 | USE_TZ = config('USE_TZ', default='True', cast=bool) 133 | 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 137 | 138 | STATIC_URL = config('STATIC_URL', default='static/') 139 | STATIC_ROOT = config('STATIC_ROOT', default='static') 140 | 141 | # Default primary key field type 142 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 143 | 144 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 145 | 146 | # Login / Logout configuration 147 | LOGIN_REDIRECT_URL = config('STATIC_ROOT', default='/') 148 | LOGOUT_REDIRECT_URL = config('STATIC_ROOT', default='/') 149 | 150 | # AuditLog configuration 151 | AUDITLOG_INCLUDE_ALL_MODELS = True 152 | AUDITLOG_INCLUDE_TRACKING_MODELS = ( 153 | "conformity.Organization", {"model": "conformity.Organization", "m2m_fields": ["applicable_frameworks"], }, 154 | "conformity.Action", {"model": "conformity.Action", "m2m_fields": ["associated_conformity", "associated_findings"], }, 155 | "conformity.Audit", {"model": "conformity.Audit", "m2m_fields": ["audited_frameworks"], } 156 | ) 157 | AUDITLOG_EXCLUDE_TRACKING_FIELDS = ( 158 | "created", 159 | "modified", 160 | "password", 161 | "last_login", 162 | "id", 163 | "ID", 164 | "create_date", 165 | "update_date" 166 | ) 167 | 168 | CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' 169 | CONSTANCE_CONFIG = { 170 | 'WELCOME_HEADER': ( 171 | "Bonjour !", 172 | "This text will be printed on the login page", 173 | str), 174 | } 175 | --------------------------------------------------------------------------------