feat: Phase 1 — socle backend FastAPI + frontend Next.js

Backend (FastAPI + SQLAlchemy):
- Modèles : User, Client, Audit, Cible, Vulnérabilité, Action
- Auth JWT (register/login/me) avec bcrypt
- Routes CRUD complets : clients, audits, cibles, vulnérabilités, actions
- Schémas Pydantic v2, migrations Alembic configurées
- Rate limiting (slowapi), CORS, structure scanners/reports pour phase 2

Frontend (Next.js 14 App Router):
- shadcn/ui : Button, Input, Card, Badge, Label
- Page login avec gestion token JWT
- Dashboard avec stats temps réel
- Pages Clients (grille) et Audits (liste) avec recherche
- Layout avec sidebar navigation + protection auth
- Dockerfiles multi-stage (backend + frontend standalone)

Infrastructure:
- docker-compose.yml : postgres, redis, backend, frontend
- docker-compose.prod.yml avec labels Traefik
- .env.example complet
- .gitignore mis à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 17:16:12 +01:00
parent 1ff3c15ea9
commit 0fe1a1b751
60 changed files with 2308 additions and 6 deletions

View File

@@ -0,0 +1,21 @@
from backend.models.base import Base
from backend.models.user import User
from backend.models.client import Client
from backend.models.audit import Audit, AuditStatut
from backend.models.target import Cible, CibleType
from backend.models.vulnerability import Vulnerabilite, Criticite
from backend.models.action import Action, ActionStatut
__all__ = [
"Base",
"User",
"Client",
"Audit",
"AuditStatut",
"Cible",
"CibleType",
"Vulnerabilite",
"Criticite",
"Action",
"ActionStatut",
]

22
backend/models/action.py Normal file
View File

@@ -0,0 +1,22 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Text, ForeignKey, Enum as SAEnum
import enum
from backend.models.base import Base, TimestampMixin
class ActionStatut(str, enum.Enum):
ouvert = "ouvert"
en_cours = "en_cours"
resolu = "resolu"
class Action(Base, TimestampMixin):
__tablename__ = "actions"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
vulnerabilite_id: Mapped[int] = mapped_column(ForeignKey("vulnerabilites.id"), nullable=False, index=True)
statut: Mapped[ActionStatut] = mapped_column(SAEnum(ActionStatut), default=ActionStatut.ouvert, nullable=False)
assigne_a: Mapped[str | None] = mapped_column(String(255), nullable=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
vulnerabilite: Mapped["Vulnerabilite"] = relationship("Vulnerabilite", back_populates="actions")

30
backend/models/audit.py Normal file
View File

@@ -0,0 +1,30 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Integer, Float, ForeignKey, DateTime, Enum as SAEnum
from datetime import datetime
import enum
from backend.models.base import Base, TimestampMixin
class AuditStatut(str, enum.Enum):
planifie = "planifie"
en_cours = "en_cours"
termine = "termine"
annule = "annule"
class Audit(Base, TimestampMixin):
__tablename__ = "audits"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id"), nullable=False, index=True)
nom: Mapped[str] = mapped_column(String(255), nullable=False)
statut: Mapped[AuditStatut] = mapped_column(SAEnum(AuditStatut), default=AuditStatut.planifie, nullable=False)
date_debut: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
date_fin: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
score_global: Mapped[float | None] = mapped_column(Float, nullable=True)
client: Mapped["Client"] = relationship("Client", back_populates="audits")
cibles: Mapped[list["Cible"]] = relationship("Cible", back_populates="audit", cascade="all, delete-orphan")
vulnerabilites: Mapped[list["Vulnerabilite"]] = relationship(
"Vulnerabilite", back_populates="audit", cascade="all, delete-orphan"
)

16
backend/models/base.py Normal file
View File

@@ -0,0 +1,16 @@
from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from sqlalchemy import DateTime, func
from datetime import datetime
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)

16
backend/models/client.py Normal file
View File

@@ -0,0 +1,16 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Text
from backend.models.base import Base, TimestampMixin
class Client(Base, TimestampMixin):
__tablename__ = "clients"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
nom: Mapped[str] = mapped_column(String(255), nullable=False)
contact: Mapped[str | None] = mapped_column(String(255), nullable=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
telephone: Mapped[str | None] = mapped_column(String(50), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
audits: Mapped[list["Audit"]] = relationship("Audit", back_populates="client", cascade="all, delete-orphan")

22
backend/models/target.py Normal file
View File

@@ -0,0 +1,22 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey, Enum as SAEnum
import enum
from backend.models.base import Base, TimestampMixin
class CibleType(str, enum.Enum):
ip = "ip"
domaine = "domaine"
subnet = "subnet"
class Cible(Base, TimestampMixin):
__tablename__ = "cibles"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
audit_id: Mapped[int] = mapped_column(ForeignKey("audits.id"), nullable=False, index=True)
type: Mapped[CibleType] = mapped_column(SAEnum(CibleType), nullable=False)
valeur: Mapped[str] = mapped_column(String(255), nullable=False)
validee: Mapped[bool] = mapped_column(default=False, nullable=False)
audit: Mapped["Audit"] = relationship("Audit", back_populates="cibles")

14
backend/models/user.py Normal file
View File

@@ -0,0 +1,14 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Boolean
from backend.models.base import Base, TimestampMixin
class User(Base, TimestampMixin):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

View File

@@ -0,0 +1,28 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Text, Float, ForeignKey, Enum as SAEnum
import enum
from backend.models.base import Base, TimestampMixin
class Criticite(str, enum.Enum):
critique = "critique" # CVSS 9-10
important = "important" # CVSS 7-8.9
modere = "modere" # CVSS 4-6.9
faible = "faible" # CVSS 0-3.9
class Vulnerabilite(Base, TimestampMixin):
__tablename__ = "vulnerabilites"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
audit_id: Mapped[int] = mapped_column(ForeignKey("audits.id"), nullable=False, index=True)
criticite: Mapped[Criticite] = mapped_column(SAEnum(Criticite), nullable=False)
titre: Mapped[str] = mapped_column(String(500), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
recommandation: Mapped[str] = mapped_column(Text, nullable=False)
cve: Mapped[str | None] = mapped_column(String(50), nullable=True)
cvss_score: Mapped[float | None] = mapped_column(Float, nullable=True)
cible: Mapped[str | None] = mapped_column(String(255), nullable=True)
audit: Mapped["Audit"] = relationship("Audit", back_populates="vulnerabilites")
actions: Mapped[list["Action"]] = relationship("Action", back_populates="vulnerabilite", cascade="all, delete-orphan")