Skip to content

JMousqueton/cyber-maturity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CyberMaturity Assessment Platform

A multilingual (EN/FR) regulatory maturity assessment web application. Token-based access with no user accounts required.

Author: Julien Mousqueton — Field CISO @ Cohesity Repository: github.com/JMousqueton/cyber-maturity


Features

  • Multiple regulatory frameworks out of the box (extensible via JSON):
    • NIS2 — 10 domains, 50 questions covering all Article 21 requirements (Art. 21.2.a–j)
    • DORA — 8 domains, 40 questions
    • Any custom framework — drop a JSON file in frameworks/
  • Maturity scoring on a 0–5 scale with five levels: Initial, Developing, Defined, Managed, Optimized
  • Sector benchmark — compares domain scores against anonymised peer organisations (requires ≥ 3 completed assessments)
  • Four export formats:
    • Full PDF report (detailed, all domains and recommendations)
    • Executive Summary PDF (1–2 pages, top 5 recommendations, radar chart)
    • Action Plan Excel (colour-coded, Owner / Target Date / Status columns)
    • CSV raw data export
  • Multilingual (English / French) — language follows the session
  • Anonymous mode — full assessment without email; user saves their access URL
  • AJAX auto-save — answers persisted immediately, no data lost on browser close
  • Admin dashboard at /admin:
    • Session-based login with two roles: admin (full access) and auditor (read-only)
    • Session statistics, filterable session table, session deletion
    • Session detail with radar chart and recommendations
    • Bulk CSV export and token email resend
    • Framework management: upload / download / delete / enable-disable JSON files
    • User management: create, edit, delete admin users
    • Audit log: every admin action is recorded with user, timestamp and IP

Prerequisites

Python

  • Python 3.11+
  • pip

System libraries (required for PDF export)

WeasyPrint needs native system libraries. Install them before pip install -r requirements.txt.

Ubuntu / Debian:

sudo apt-get install -y \
  python3-dev python3-pip python3-cffi \
  libcairo2 \
  libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 \
  libharfbuzz0b \
  libgdk-pixbuf2.0-0 \
  libffi-dev \
  shared-mime-info \
  fonts-liberation

macOS:

brew install pango libffi gdk-pixbuf libxml2 libxslt

Verify WeasyPrint can load:

python3 -c "from weasyprint import HTML; print('OK')"

If this prints OK, PDF export will work. If it raises an OSError about a missing .so, re-check the system packages above.


Installation

# 1. Clone the repository
git clone https://github.com/JMousqueton/cyber-maturity.git
cd cyber-maturity

# 2. Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate      # macOS/Linux
venv\Scripts\activate         # Windows

# 3. Install Python dependencies
pip install -r requirements.txt

# 4. Create your environment file
cp .env.example .env
# Edit .env with your settings (see Environment Variables below)

# 5. Apply database migrations
flask db upgrade

# 6. Start the application
python run.py

The application will be available at http://localhost:5000. The first admin user is created automatically from ADMIN_USERNAME / ADMIN_PASSWORD in .env.


Environment Variables

Variable Default Description
FLASK_ENV development development or production
SECRET_KEY (required) Flask session secret key
DATABASE_URL sqlite:///instance/maturity.db SQLAlchemy database URL
MAIL_SERVER localhost SMTP server hostname
MAIL_PORT 25 SMTP port
MAIL_USE_TLS False Enable TLS
MAIL_USERNAME SMTP username
MAIL_PASSWORD SMTP password
MAIL_DEFAULT_SENDER noreply@example.com From address for emails
ADMIN_USERNAME admin Initial admin username (seeded on first run)
ADMIN_PASSWORD change-me Initial admin password (seeded on first run)
TOKEN_EXPIRY_DAYS 30 Assessment token validity in days
BASE_URL http://localhost:5000 Public URL for tokenized links
DEFAULT_LANGUAGE en Default language (en or fr)
PORT 5000 Port to listen on

Database Migrations

This project uses Flask-Migrate (Alembic) for schema management.

# Apply all pending migrations (run after every git pull)
flask db upgrade

# After modifying a model — generate a new migration
flask db migrate -m "describe your change"
flask db upgrade

# Roll back one version
flask db downgrade

# Mark an existing database as current without running migrations
flask db stamp head

Admin Dashboard

Access at /admin/login.

The first user is seeded from .env on startup. Additional users can be managed in the Users tab (admin role only).

Role Permissions
admin Full access — manage sessions, frameworks, users
auditor Read-only — view sessions, frameworks, audit log

Resetting a forgotten password:

source venv/bin/activate
python3 - <<'EOF'
from run import app
from app import db
from app.models import AdminUser
with app.app_context():
    user = AdminUser.query.filter_by(username="admin").first()
    user.set_password("new-password")
    db.session.commit()
    print("Password reset.")
EOF

Scoring

Responses are collected on a 0–4 internal scale (five maturity levels per question). Domain and global scores are displayed on a 0–5 scale (× 1.25).

Score range Maturity level
0.0 – 1.24 Initial
1.25 – 2.49 Developing
2.50 – 3.74 Defined
3.75 – 4.99 Managed
5.0 Optimized

Recommendations are generated for every question scoring below 3 (0–5 scale), sorted by priority.


Sector Benchmark

When at least 3 completed assessments exist for the same framework and sector, the results page shows:

  • An orange dashed overlay on the radar chart
  • A "Sector Average" column in the domain scores table
  • Benchmark data included in the Executive Summary PDF

If fewer than 3 peer sessions exist, a note is displayed explaining that data is insufficient.


Adding a New Framework

Add frameworks via the admin UI (Frameworks tab → Upload), or manually by placing a .json file in frameworks/. No restart required.

JSON schema

{
  "id": "my_framework",
  "version": "1.0",
  "enabled": true,
  "translations": {
    "en": { "name": "My Framework", "description": "..." },
    "fr": { "name": "Mon référentiel", "description": "..." }
  },
  "domains": [
    {
      "id": "domain_id",
      "weight": 0.20,
      "translations": {
        "en": { "label": "Domain Label", "description": "..." },
        "fr": { "label": "Libellé du domaine", "description": "..." }
      },
      "questions": [
        {
          "id": "q_001",
          "article_ref": "Art. X.Y",
          "translations": {
            "en": { "text": "Question text?", "help": "Help text." },
            "fr": { "text": "Texte de la question ?", "help": "Texte d'aide." }
          },
          "maturity_levels": {
            "0": { "en": "None", "fr": "Inexistant" },
            "1": { "en": "Initial", "fr": "Initial" },
            "2": { "en": "Developing", "fr": "En développement" },
            "3": { "en": "Defined", "fr": "Défini" },
            "4": { "en": "Optimized", "fr": "Optimisé" }
          },
          "recommendation": {
            "en": "Recommendation text. Ref: Art. X.Y",
            "fr": "Texte de la recommandation. Réf : Art. X.Y"
          },
          "shared_with": []
        }
      ]
    }
  ]
}

Rules:

  • Domain weights must sum to 1.0
  • enabled: true is required — set to false to hide from the survey without deleting
  • shared_with accepts other framework IDs (e.g. ["dora"]) to display cross-framework badges

A minimal one-question-per-domain example is available in frameworks/sample.json.


Project Structure

cyber-maturity/
├── app/
│   ├── __init__.py              # Flask app factory, Jinja2 filters
│   ├── models.py                # SQLAlchemy models (sessions, results, admin users, audit log)
│   ├── routes/
│   │   ├── assessment.py        # User-facing routes + PDF/CSV/Excel exports
│   │   ├── admin.py             # Admin dashboard + framework + user management
│   │   └── auth.py              # Token, language, session start
│   ├── services/
│   │   ├── scoring.py           # Maturity scoring engine (0–5 scale)
│   │   ├── email.py             # Email service
│   │   ├── pdf.py               # PDF (WeasyPrint) + Excel (openpyxl) generation
│   │   ├── i18n.py              # Translation helper
│   │   └── framework.py         # JSON framework loader + validator
│   ├── templates/
│   │   ├── base.html            # Base layout (navbar, footer, about modal)
│   │   ├── pdf_report.html      # Full PDF template
│   │   ├── pdf_executive.html   # Executive summary PDF template
│   │   └── admin/               # Admin dashboard templates (login, dashboard, users, audit)
│   ├── translations/
│   │   ├── en.json              # English strings
│   │   └── fr.json              # French strings
│   └── static/                  # CSS, JS, favicons
├── frameworks/                  # Framework JSON files (gitignored: sample*.json, test*.json)
│   ├── nis2.json
│   └── dora.json
├── migrations/                  # Alembic migration versions
├── tests/
│   └── test_scoring.py
├── instance/
│   └── maturity.db              # SQLite database (auto-created, gitignored)
├── .env.example
├── config.py
├── requirements.txt
└── run.py

Architecture Notes

  • Token-based access — every session gets a UUID4 token; no user accounts needed.
  • Anonymous mode — full assessment without email; the user must manually save the URL.
  • AJAX auto-save — each answer saved immediately via POST /assessment/<token>/answer.
  • Framework extensibility — drop a .json file in frameworks/, toggle enabled/disabled from the admin UI.
  • Scoring — weighted average per domain → weighted global score on a 0–5 scale.
  • Benchmark — sector peer comparison using anonymised completed sessions (minimum 3).
  • Cross-framework badges — questions with shared_with display a pill badge on the assessment page.
  • Admin auth — session-based login form; two roles (admin / auditor); all actions recorded in audit log.
  • Migrations — Flask-Migrate (Alembic); run flask db upgrade after every deployment.

Production Deployment

# Generate a strong secret key
export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")

# Set environment
export FLASK_ENV=production

# Apply migrations
flask db upgrade

# Start with gunicorn
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 "run:app"

Use a reverse proxy (nginx/caddy) in front of gunicorn. SESSION_COOKIE_SECURE=True is enabled automatically in ProductionConfig.


Running Tests

pytest tests/ -v

# With coverage
pip install pytest-cov
pytest tests/ --cov=app/services/scoring

About

Cyber maturity assessment platform supporting NIS2, DORA & ISO 27k frameworks, with scoring, benchmarking, PDF/Excel exports and a full admin interface

Topics

Resources

Stars

Watchers

Forks

Used by

Contributors

Languages