diff --git a/Makefile b/Makefile index 83cd829..a6a646d 100644 --- a/Makefile +++ b/Makefile @@ -62,12 +62,14 @@ $(TEST_TREE_BUILDER): \ $(TEST_PROJECT): \ tests/test_investigation_project.c \ src/core/investigation_project.c \ - src/database/database.c + src/database/database.c \ + src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(TEST_DATABASE): \ tests/test_database.c \ - src/database/database.c + src/database/database.c \ + src/database/schema.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 test: \ diff --git a/database/init.sql b/database/init.sql deleted file mode 100644 index e69de29..0000000 diff --git a/database/schema.sql b/database/schema.sql deleted file mode 100644 index e69de29..0000000 diff --git a/database/schema_v1.sql b/database/schema_v1.sql new file mode 100644 index 0000000..010e433 --- /dev/null +++ b/database/schema_v1.sql @@ -0,0 +1,1231 @@ +/****************************************************************************** + * Labfy Investigation + * + * Schéma SQLite officiel V1 + ******************************************************************************/ + +PRAGMA foreign_keys = ON; + +CREATE TABLE metadata +( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE TABLE investigation +( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + root_path TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE categories +( + id TEXT PRIMARY KEY, + + nom TEXT NOT NULL COLLATE NOCASE, + description TEXT, + + icone TEXT, + couleur TEXT, + + system INTEGER NOT NULL DEFAULT 0, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (nom), + + CHECK ( + length(trim(nom)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + icone IS NULL + OR length(trim(icone)) > 0 + ), + + CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) + ), + + CHECK ( + system IN (0, 1) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_categories_status +ON categories(status); + +CREATE TABLE tags +( + id TEXT PRIMARY KEY, + + nom TEXT NOT NULL COLLATE NOCASE, + description TEXT, + couleur TEXT, + + system INTEGER NOT NULL DEFAULT 0, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (nom), + + CHECK ( + length(trim(nom)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) + ), + + CHECK ( + system IN (0, 1) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_tags_status +ON tags(status); + +CREATE TABLE types_preuve +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +INSERT INTO types_preuve +(id, code, label) +VALUES +(1, 'screenshot', 'Capture d''écran'), +(2, 'photo', 'Photographie'), +(3, 'video', 'Vidéo'), +(4, 'document', 'Document'), +(5, 'email', 'Courrier électronique'), +(6, 'archive', 'Archive'), +(7, 'audio', 'Audio'), +(8, 'text', 'Texte'), +(9, 'other', 'Autre'); + +CREATE TABLE types_entite +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +CREATE TABLE tag_preuves +( + tag_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, preuve_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_preuves_preuve_id +ON tag_preuves(preuve_id); + +CREATE TABLE tag_recherches +( + tag_id TEXT NOT NULL, + recherche_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, recherche_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_recherches_recherche_id +ON tag_recherches(recherche_id); + +CREATE TABLE tag_entites +( + tag_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, entite_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_entites_entite_id +ON tag_entites(entite_id); + +CREATE TABLE tag_relations +( + tag_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, relation_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_relations_relation_id +ON tag_relations(relation_id); + +CREATE TABLE tag_hypotheses +( + tag_id TEXT NOT NULL, + hypothese_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, hypothese_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_hypotheses_hypothese_id +ON tag_hypotheses(hypothese_id); + +CREATE TABLE tag_chronologie +( + tag_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY (tag_id, chronologie_id), + + FOREIGN KEY (tag_id) + REFERENCES tags(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_tag_chronologie_chronologie_id +ON tag_chronologie(chronologie_id); + +INSERT INTO types_entite +(id, code, label) +VALUES +(1, 'email_address', 'Adresse email'), +(2, 'bank_account', 'Compte bancaire'), +(3, 'facebook_account', 'Compte Facebook'), +(4, 'instagram_account', 'Compte Instagram'), +(5, 'identity_document', 'Document d''identité'), +(6, 'iban', 'IBAN'), +(7, 'person', 'Personne'), +(8, 'pseudonym', 'Pseudonyme'), +(9, 'phone_number', 'Numéro de téléphone'), +(10, 'website', 'Site web'), +(11, 'domain_name', 'Nom de domaine'), +(12, 'ip_address', 'Adresse IP'), +(13, 'organization', 'Organisation'), +(14, 'other', 'Autre'); + +CREATE TABLE types_source +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +INSERT INTO types_source +(id, code, label) +VALUES +(1, 'website', 'Site web'), +(2, 'social_network', 'Réseau social'), +(3, 'email', 'Courrier électronique'), +(4, 'document', 'Document'), +(5, 'testimony', 'Témoignage'), +(6, 'phone_export', 'Export de téléphone'), +(7, 'disk_export', 'Export de disque'), +(8, 'osint_tool', 'Outil OSINT'), +(9, 'manual_entry', 'Saisie manuelle'), +(10, 'other', 'Autre'); + +CREATE TABLE sources +( + id TEXT PRIMARY KEY, + + type_id INTEGER NOT NULL, + + nom TEXT NOT NULL, + reference TEXT, + description TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + FOREIGN KEY (type_id) + REFERENCES types_source(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (length(trim(nom)) > 0), + + CHECK ( + reference IS NULL + OR length(trim(reference)) > 0 + ) +); + +CREATE INDEX idx_sources_type_id +ON sources(type_id); + +CREATE INDEX idx_sources_reference +ON sources(reference); + +CREATE TABLE preuves +( + id TEXT PRIMARY KEY, + + name TEXT NOT NULL, + relative_path TEXT NOT NULL UNIQUE, + + type_id INTEGER NOT NULL, + + size_bytes INTEGER, + sha256 TEXT, + mime_type TEXT, + + description TEXT, + commentaire TEXT, + categorie_id TEXT, + + file_created_at TEXT, + imported_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + locked INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (type_id) + REFERENCES types_preuve(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL + + CHECK (length(trim(name)) > 0), + + CHECK (length(trim(relative_path)) > 0), + + CHECK ( + size_bytes IS NULL + OR size_bytes >= 0 + ), + + CHECK ( + sha256 IS NULL + OR ( + length(sha256) = 64 + AND sha256 = lower(sha256) + ) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ), + + CHECK ( + locked IN (0, 1) + ) +); + +CREATE INDEX idx_preuves_type_id +ON preuves(type_id); + +CREATE INDEX idx_preuves_status +ON preuves(status); + +CREATE INDEX idx_preuves_sha256 +ON preuves(sha256); + +CREATE INDEX idx_preuves_categorie_id +ON preuves(categorie_id); + +CREATE TABLE recherches +( + id TEXT PRIMARY KEY, + + source_id TEXT, + type_outil_id INTEGER NOT NULL, + + outil_nom TEXT NOT NULL, + requete TEXT, + resultat TEXT, + observations TEXT, + categorie_id TEXT, + + started_at TEXT NOT NULL, + completed_at TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'planned', + + FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL, + + FOREIGN KEY (type_outil_id) + REFERENCES types_outil(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL, + + CHECK (length(trim(outil_nom)) > 0), + + CHECK ( + requete IS NULL + OR length(trim(requete)) > 0 + ), + + CHECK ( + completed_at IS NULL + OR completed_at >= started_at + ), + + CHECK ( + status IN ( + 'planned', + 'running', + 'completed', + 'failed', + 'cancelled', + 'archived' + ) + ) +); + +CREATE INDEX idx_recherches_source_id +ON recherches(source_id); + +CREATE INDEX idx_recherches_type_outil_id +ON recherches(type_outil_id); + +CREATE INDEX idx_recherches_status +ON recherches(status); + +CREATE INDEX idx_recherches_started_at +ON recherches(started_at); + +CREATE INDEX idx_recherches_categorie_id +ON recherches(categorie_id); + +CREATE TABLE recherche_preuves +( + recherche_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + preuve_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'input', + 'output' + ) + ) +); + +CREATE INDEX idx_recherche_preuves_preuve_id +ON recherche_preuves(preuve_id); + +CREATE TABLE entites +( + id TEXT PRIMARY KEY, + + type_id INTEGER NOT NULL, + + valeur TEXT NOT NULL, + label TEXT, + description TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE (type_id, valeur), + + FOREIGN KEY (type_id) + REFERENCES types_entite(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (length(trim(valeur)) > 0), + + CHECK ( + label IS NULL + OR length(trim(label)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_entites_type_id +ON entites(type_id); + +CREATE INDEX idx_entites_valeur +ON entites(valeur); + +CREATE INDEX idx_entites_status +ON entites(status); + +CREATE INDEX idx_entites_confiance +ON entites(confiance); + +CREATE TABLE types_outil +( + id INTEGER PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT +); + +CREATE TABLE recherche_entites +( + recherche_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + entite_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'discovered', + 'enriched', + 'validated', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_entites_entite_id +ON recherche_entites(entite_id); + +CREATE TABLE preuve_entites +( + preuve_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY ( + preuve_id, + entite_id + ), + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_preuve_entites_entite_id +ON preuve_entites(entite_id); + +CREATE TABLE relations +( + id TEXT PRIMARY KEY, + + entite_source_id TEXT NOT NULL, + entite_cible_id TEXT NOT NULL, + + type_relation TEXT NOT NULL, + label TEXT, + justification TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + UNIQUE ( + entite_source_id, + entite_cible_id, + type_relation + ), + + FOREIGN KEY (entite_source_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (entite_cible_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + entite_source_id <> entite_cible_id + ), + + CHECK ( + length(trim(type_relation)) > 0 + ), + + CHECK ( + label IS NULL + OR length(trim(label)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted', + 'disputed' + ) + ) +); + +CREATE INDEX idx_relations_entite_source_id +ON relations(entite_source_id); + +CREATE INDEX idx_relations_entite_cible_id +ON relations(entite_cible_id); + +CREATE INDEX idx_relations_type_relation +ON relations(type_relation); + +CREATE INDEX idx_relations_status +ON relations(status); + +CREATE INDEX idx_relations_confiance +ON relations(confiance); + +CREATE TABLE relation_preuves +( + relation_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + PRIMARY KEY ( + relation_id, + preuve_id + ), + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_relation_preuves_preuve_id +ON relation_preuves(preuve_id); + +CREATE TABLE recherche_relations +( + recherche_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + relation_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'discovered', + 'confirmed', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_relations_relation_id +ON recherche_relations(relation_id); + +CREATE TABLE chronologie +( + id TEXT PRIMARY KEY, + + event_time TEXT NOT NULL, + + titre TEXT NOT NULL, + description TEXT, + + origine TEXT NOT NULL DEFAULT 'manual', + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'active', + + CHECK ( + length(trim(titre)) > 0 + ), + + CHECK ( + description IS NULL + OR length(trim(description)) > 0 + ), + + CHECK ( + origine IN ( + 'manual', + 'automatic', + 'imported' + ) + ), + + CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) + ) +); + +CREATE INDEX idx_chronologie_event_time +ON chronologie(event_time); + +CREATE INDEX idx_chronologie_origine +ON chronologie(origine); + +CREATE INDEX idx_chronologie_status +ON chronologie(status); + +CREATE TABLE recherche_chronologie +( + recherche_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + chronologie_id + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE preuve_chronologie +( + preuve_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + preuve_id, + chronologie_id + ), + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE entite_chronologie +( + entite_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + entite_id, + chronologie_id + ), + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE TABLE relation_chronologie +( + relation_id TEXT NOT NULL, + chronologie_id TEXT NOT NULL, + + PRIMARY KEY ( + relation_id, + chronologie_id + ), + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + FOREIGN KEY (chronologie_id) + REFERENCES chronologie(id) + ON UPDATE CASCADE + ON DELETE CASCADE +); + +CREATE INDEX idx_recherche_chronologie_chronologie_id +ON recherche_chronologie(chronologie_id); + +CREATE INDEX idx_preuve_chronologie_chronologie_id +ON preuve_chronologie(chronologie_id); + +CREATE INDEX idx_entite_chronologie_chronologie_id +ON entite_chronologie(chronologie_id); + +CREATE INDEX idx_relation_chronologie_chronologie_id +ON relation_chronologie(chronologie_id); + +CREATE TABLE journal +( + id TEXT PRIMARY KEY, + + event_time TEXT NOT NULL, + action TEXT NOT NULL, + + objet_type TEXT, + objet_id TEXT, + + resultat TEXT NOT NULL, + + details TEXT, + acteur TEXT, + + created_at TEXT NOT NULL, + + CHECK ( + length(trim(action)) > 0 + ), + + CHECK ( + objet_type IS NULL + OR length(trim(objet_type)) > 0 + ), + + CHECK ( + objet_id IS NULL + OR length(trim(objet_id)) > 0 + ), + + CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) + ), + + CHECK ( + resultat IN ( + 'success', + 'failure', + 'partial', + 'cancelled' + ) + ), + + CHECK ( + details IS NULL + OR length(trim(details)) > 0 + ), + + CHECK ( + acteur IS NULL + OR length(trim(acteur)) > 0 + ) +); + +CREATE INDEX idx_journal_event_time +ON journal(event_time); + +CREATE INDEX idx_journal_action +ON journal(action); + +CREATE INDEX idx_journal_objet +ON journal(objet_type, objet_id); + +CREATE INDEX idx_journal_resultat +ON journal(resultat); + +CREATE TABLE hypotheses +( + id TEXT PRIMARY KEY, + + titre TEXT NOT NULL, + description TEXT NOT NULL, + categorie_id TEXT, + + confiance INTEGER NOT NULL DEFAULT 50, + evaluation TEXT, + + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'proposed', + + FOREIGN KEY (categorie_id) + REFERENCES categories(id) + ON UPDATE CASCADE + ON DELETE SET NULL + + CHECK ( + length(trim(titre)) > 0 + ), + + CHECK ( + length(trim(description)) > 0 + ), + + CHECK ( + confiance BETWEEN 0 AND 100 + ), + + CHECK ( + evaluation IS NULL + OR length(trim(evaluation)) > 0 + ), + + CHECK ( + status IN ( + 'proposed', + 'under_review', + 'supported', + 'contradicted', + 'confirmed', + 'rejected', + 'archived' + ) + ) +); + +CREATE INDEX idx_hypotheses_status +ON hypotheses(status); + +CREATE INDEX idx_hypotheses_confiance +ON hypotheses(confiance); + +CREATE INDEX idx_hypotheses_updated_at +ON hypotheses(updated_at); + +CREATE INDEX idx_hypotheses_categorie_id +ON hypotheses(categorie_id); + +CREATE TABLE hypothese_preuves +( + hypothese_id TEXT NOT NULL, + preuve_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + preuve_id, + role + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (preuve_id) + REFERENCES preuves(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'supports', + 'contradicts', + 'confirms' + ) + ) +); + +CREATE INDEX idx_hypothese_preuves_preuve_id +ON hypothese_preuves(preuve_id); + +CREATE TABLE hypothese_entites +( + hypothese_id TEXT NOT NULL, + entite_id TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + entite_id + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entite_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +); + +CREATE INDEX idx_hypothese_entites_entite_id +ON hypothese_entites(entite_id); + +CREATE TABLE hypothese_relations +( + hypothese_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + hypothese_id, + relation_id, + role + ), + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'supports', + 'contradicts' + ) + ) +); + +CREATE INDEX idx_hypothese_relations_relation_id +ON hypothese_relations(relation_id); + +CREATE TABLE recherche_hypotheses +( + recherche_id TEXT NOT NULL, + hypothese_id TEXT NOT NULL, + + role TEXT NOT NULL, + + PRIMARY KEY ( + recherche_id, + hypothese_id, + role + ), + + FOREIGN KEY (recherche_id) + REFERENCES recherches(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (hypothese_id) + REFERENCES hypotheses(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK ( + role IN ( + 'created', + 'enriched', + 'confirmed', + 'contradicted' + ) + ) +); + +CREATE INDEX idx_recherche_hypotheses_hypothese_id +ON recherche_hypotheses(hypothese_id); + +INSERT INTO types_outil +(id, code, label) +VALUES +(1, 'web_browser', 'Navigateur web'), +(2, 'search_engine', 'Moteur de recherche'), +(3, 'dns_tool', 'Outil DNS'), +(4, 'whois_tool', 'Outil WHOIS'), +(5, 'social_network', 'Réseau social'), +(6, 'email_tool', 'Outil email'), +(7, 'metadata_tool', 'Outil de métadonnées'), +(8, 'hash_tool', 'Outil de calcul de hash'), +(9, 'ocr_tool', 'Outil OCR'), +(10, 'custom_script', 'Script personnalisé'), +(11, 'manual_process', 'Procédure manuelle'), +(12, 'other', 'Autre'); + diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md new file mode 100644 index 0000000..8ab7432 --- /dev/null +++ b/docs/CONVENTIONS.md @@ -0,0 +1,302 @@ +# Conventions de développement + +Version : 1.0 + +--- + +# Philosophie + +Labfy Investigation est développé comme un logiciel d'enquête numérique. + +Les choix d'architecture privilégient : + +- la lisibilité ; +- la maintenabilité ; +- la robustesse ; +- la traçabilité ; +- la simplicité. + +Une solution simple et cohérente est toujours préférée à une solution +complexe. + +--- + +# Langue + +Le domaine métier est écrit en français. + +Exemples : + +- preuve +- entite +- personne +- source +- chronologie +- journal +- hypothese +- recherche + +Les standards techniques conservent leur nom d'origine. + +Exemples : + +- uuid +- sha256 +- mime_type +- sqlite +- created_at +- updated_at +- relative_path + +--- + +# Architecture + +Le projet est organisé par domaine métier. + +Exemple : + +``` +preuve + ↓ +preuve.h +preuve.c + +entite + ↓ +entite.h +entite.c +``` + +Chaque module possède une responsabilité unique. + +--- + +# SQL + +Une table représente un objet métier. + +Une table importante possède : + +- une structure C ; +- un module C ; +- des tests. + +Les objets métier utilisent : + +``` +TEXT PRIMARY KEY +``` + +contenant un UUID. + +Les tables de référence utilisent : + +``` +INTEGER PRIMARY KEY +``` + +--- + +# UUID + +Tous les objets métier utilisent un UUID. + +Exemple : + +``` +preuve +entite +personne +source +chronologie +journal +``` + +Il n'existe pas simultanément : + +``` +id +uuid +``` + +La colonne : + +``` +id +``` + +contient directement l'UUID. + +--- + +# Dates + +Toutes les dates sont enregistrées en UTC. + +Format : + +``` +YYYY-MM-DDTHH:MM:SSZ +``` + +--- + +# Chemins + +Les chemins sont toujours relatifs à la racine de l'enquête. + +Autorisé : + +``` +01_Preuves_Originales/Documents/facture.pdf +``` + +Interdit : + +``` +/home/user/Documents/... +``` + +--- + +# Suppression + +Les objets métier importants ne sont jamais supprimés immédiatement. + +Une suppression est une modification d'état. + +Exemple : + +``` +active +archived +deleted +``` + +Une purge physique éventuelle devra être explicite. + +--- + +# Nommage + +Le SQL utilise : + +- snake_case +- minuscules +- aucun accent +- aucun espace + +--- + +# Code C + +Norme : + +``` +C17 +``` + +Variables : + +Toujours explicites. + +Autorisé : + +```c +relative_path +description +commentaire +created_at +``` + +Interdit : + +```c +path +desc +comm +crt +``` + +Les fonctions utilisent le nom du module. + +Exemple : + +```c +preuve_create() +preuve_load() +preuve_update() +preuve_delete() +``` + +--- + +# Structures + +Une structure représente un objet métier. + +Exemple : + +```c +typedef struct Preuve Preuve; +``` + +Les structures publiques sont opaques lorsque cela est possible. + +--- + +# Tests + +Toute nouvelle fonctionnalité importante possède un test. + +Le développement suit toujours l'ordre : + +``` +Conception + +↓ + +Implémentation + +↓ + +Tests + +↓ + +Commit +``` + +--- + +# Git + +Un ticket terminé correspond à un commit. + +Le message de commit est rédigé en anglais. + +Exemple : + +``` +feat(database): initialize investigation database +``` + +--- + +# Documentation + +Les décisions importantes sont documentées. + +Les changements de schéma SQLite sont précédés d'un audit. + +Aucune décision importante ne doit exister uniquement dans le code. + +--- + +# Objectif + +Le projet doit rester compréhensible plusieurs années après sa création. + +La priorité est donnée à la qualité du code plutôt qu'à la rapidité de +développement. diff --git a/docs/database/SCHEMA_AUDIT_V1.md b/docs/database/SCHEMA_AUDIT_V1.md new file mode 100644 index 0000000..5e3ba25 --- /dev/null +++ b/docs/database/SCHEMA_AUDIT_V1.md @@ -0,0 +1,2909 @@ +# Audit du schéma SQLite V1 + +Version : 1.0 + +Date : 2026-07-15 + +--- + +# Objectif + +Ce document analyse le schéma historique de Labfy Investigation afin de +définir le schéma métier officiel V1. + +Chaque table historique reçoit l'une des décisions suivantes : + +- `CONSERVER` +- `MODIFIER` +- `RENOMMER` +- `FUSIONNER` +- `SUPPRIMER` + +Le schéma historique sert de fondation. Les concepts utiles sont conservés, +mais les noms, contraintes et relations peuvent être corrigés. + +--- + +# Conventions générales + +## Identifiants métier + +Tous les objets métier utilisent un UUID stocké sous forme de texte : + +```sql +id TEXT PRIMARY KEY +``` + +L'UUID est généré par l'application avec : + +```c +g_uuid_string_random() +``` + +Les tables de référence utilisent des identifiants entiers. + +--- + +## Dates + +Les dates techniques sont stockées en UTC au format ISO 8601 : + +```text +YYYY-MM-DDTHH:MM:SSZ +``` + +--- + +## Chemins + +Tous les chemins enregistrés dans la base sont relatifs à la racine de +l'enquête. + +Exemple valide : + +```text +01_Preuves_Originales/Documents/facture.pdf +``` + +Exemple interdit : + +```text +/home/utilisateur/Enquetes/Test/01_Preuves_Originales/Documents/facture.pdf +``` + +--- + +## Nommage SQL + +Les noms utilisent : + +- des minuscules ; +- le `snake_case` ; +- aucun accent ; +- aucun espace. + +--- + +## Suppression logique + +Les objets importants ne sont pas supprimés immédiatement de la base. + +Un statut métier permet de conserver leur historique et leurs relations. + +Une purge physique éventuelle devra être une opération explicite et +journalisée. + +--- + +# Audit des tables + +## Table historique `preuves` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +preuves +``` + +### Justification + +Le concept est central et doit être conservé. + +La table historique doit cependant être renforcée afin de garantir : + +- l'identification unique des preuves ; +- la portabilité des chemins ; +- l'intégrité des fichiers ; +- la traçabilité de l'import ; +- la suppression logique ; +- la cohérence des types et statuts. + +--- + +## Définition métier + +Une preuve représente un élément collecté dans le cadre d'une enquête. + +Elle peut correspondre notamment à : + +- une capture d'écran ; +- une photographie ; +- une vidéo ; +- un document ; +- un email exporté ; +- une archive ; +- un fichier audio ; +- un journal technique ; +- un résultat produit par un outil. + +Le contenu d'une preuve originale ne doit jamais être modifié. + +Les annotations, conversions, extractions et autres transformations produisent +de nouveaux fichiers dans `02_Preuves_Traitees`. + +--- + +## Identifiant + +La colonne : + +```text +id +``` + +contient directement l'UUID de la preuve. + +Il n'est pas nécessaire d'avoir deux colonnes distinctes `id` et `uuid`. + +Exemple : + +```text +550e8400-e29b-41d4-a716-446655440000 +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la preuve | +| `name` | `TEXT` | oui | Nom visible du fichier ou de l'élément | +| `relative_path` | `TEXT` | oui | Chemin relatif dans l'enquête | +| `type_id` | `INTEGER` | oui | Référence vers `types_preuve` | +| `source_id` | `TEXT` | non | Référence vers la source principale | +| `size_bytes` | `INTEGER` | non | Taille du fichier en octets | +| `sha256` | `TEXT` | non | Empreinte SHA-256 | +| `mime_type` | `TEXT` | non | Type MIME détecté | +| `description` | `TEXT` | non | Description synthétique | +| `comment` | `TEXT` | non | Notes détaillées | +| `file_created_at` | `TEXT` | non | Date de création connue du fichier | +| `imported_at` | `TEXT` | oui | Date d'ajout dans l'enquête | +| `updated_at` | `TEXT` | oui | Dernière modification des métadonnées | +| `status` | `TEXT` | oui | État logique de la preuve | +| `locked` | `INTEGER` | oui | Verrouillage logique, `0` ou `1` | + +--- + +## Statuts autorisés + +Première version : + +```text +active +archived +deleted +``` + +Une preuve marquée : + +```text +deleted +``` + +reste présente dans la base afin de préserver : + +- le journal ; +- les relations ; +- les références dans les rapports ; +- la traçabilité. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (relative_path) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_preuve(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL +``` + +```sql +CHECK (length(trim(name)) > 0) +``` + +```sql +CHECK (length(trim(relative_path)) > 0) +``` + +```sql +CHECK (size_bytes IS NULL OR size_bytes >= 0) +``` + +```sql +CHECK ( + sha256 IS NULL + OR ( + length(sha256) = 64 + AND sha256 = lower(sha256) + ) +) +``` + +```sql +CHECK (status IN ('active', 'archived', 'deleted')) +``` + +```sql +CHECK (locked IN (0, 1)) +``` + +--- + +## Index prévus + +La contrainte `UNIQUE` crée déjà un index sur : + +```text +relative_path +``` + +Ajouter également : + +```sql +CREATE INDEX idx_preuves_type_id +ON preuves(type_id); +``` + +```sql +CREATE INDEX idx_preuves_source_id +ON preuves(source_id); +``` + +```sql +CREATE INDEX idx_preuves_status +ON preuves(status); +``` + +```sql +CREATE INDEX idx_preuves_sha256 +ON preuves(sha256); +``` + +L'index sur `sha256` permettra notamment de détecter rapidement les fichiers +déjà présents dans l'enquête. + +--- + +## Relation avec les sources + +Une preuve peut posséder une source principale via : + +```text +source_id +``` + +Ce champ ne remplace pas nécessairement toutes les relations possibles. + +Une future table de liaison pourra être ajoutée si une preuve doit être reliée +à plusieurs sources : + +```text +preuve_sources +``` + +Pour le schéma V1, une source principale est suffisante tant que le besoin +multi-source n'est pas confirmé. + +--- + +## Relation avec les tags + +La relation entre les preuves et les tags sera stockée dans : + +```text +preuve_tags +``` + +La table historique : + +```text +preuves_tag +``` + +sera donc renommée et consolidée. + +--- + +## Structure C cible + +La structure C envisagée est : + +```c +typedef struct InvestigationEvidence InvestigationEvidence; +``` + +Sa représentation privée contiendra conceptuellement : + +```c +struct InvestigationEvidence +{ + char *id; + char *name; + char *relative_path; + + int type_id; + char *source_id; + + guint64 size_bytes; + + char *sha256; + char *mime_type; + char *description; + char *comment; + + char *file_created_at; + char *imported_at; + char *updated_at; + + char *status; + gboolean locked; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +Elle sert uniquement à vérifier que le schéma SQL correspond bien au futur +modèle C. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `preuves` ; +- adoption d'un UUID comme clé primaire ; +- utilisation exclusive d'un chemin relatif ; +- ajout de contraintes `NOT NULL` ; +- ajout de la taille en octets ; +- ajout du hash SHA-256 ; +- ajout du type MIME ; +- séparation entre date du fichier et date d'import ; +- ajout d'une date de mise à jour ; +- ajout d'un statut de suppression logique ; +- ajout du verrouillage ; +- ajout d'une relation optionnelle vers `sources` ; +- ajout d'index métier ; +- renommage futur de `preuves_tag` en `preuve_tags`. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `preuves` +``` +--- + +## Table historique `sources` + +### Décision + +```text +MODIFIER +``` +--- + +### Nom final + +```text +sources +``` +--- + +### Justification + +Le concept doit être conservé. + +Une source représente l'origine d'une information ou d'une preuve. Elle ne +représente pas la preuve elle-même. + +Exemples : + +- site web ; +- réseau social ; +- témoin ; +- téléphone ; +- disque ; +- document ; +- email ; +- outil externe. + +La table historique doit être renforcée avec : + +- un UUID ; +- un type obligatoire ; +- une référence technique générique ; +- des dates techniques ; +- des contraintes explicites. + +--- + +### Colonnes finales proposées + + Colonnes finales proposées + +|Colonne|Type|Obligatoire|Description| +|-|-|-|-| +|id|TEXT|oui|UUID de la source| +|type_id|INTEGER|oui|Référence vers types_source| +|nom|TEXT|oui|Nom visible de la source| +|reference|TEXT|non|URL, identifiant, numéro de série, Message-ID ou autre référence technique| +|description|TEXT|non|Description libre| +|created_at|TEXT|oui|Date de création dans l'enquête| +|updated_at|TEXT|oui|Dernière modification des métadonnées| + +--- + +### Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_source(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + reference IS NULL + OR length(trim(reference)) > 0 +) +``` +--- + +### Relation avec les preuves + +Le modèle prévu est : + +``` +source + ↓ +recherche + ↓ +preuve +``` + +Une recherche documentera : + +- la source interrogée ; +- l'outil utilisé ; +- la requête ; +- les résultats ; +- les preuves produites. + +--- + +### Index prévus + +```sql +CREATE INDEX idx_sources_type_id +ON sources(type_id); +``` + +```sql +CREATE INDEX idx_sources_reference +ON sources(reference); +``` +--- + +### Structure C cible + +```C +typedef struct Source Source; +``` + +```C +struct Source +{ + char *id; + + int type_id; + + char *nom; + char *reference; + char *description; + + char *created_at; + char *updated_at; +}; +``` +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +### Modifications par rapport au schéma historique + +- conservation du concept sources ; +- UUID comme clé primaire ; +- type obligatoire ; +- remplacement des champs trop spécialisés par reference ; +- ajout des dates techniques ; +- ajout des contraintes ; +- ajout des index ; +- absence de relation directe avec preuves. + +--- + +### Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `sources` +``` + +## Table historique `recherche` + +### Décision + +```text +RENOMMER ET MODIFIER +``` + +### Nom final + +```text +recherches +``` + +### Justification + +Le concept doit être conservé, mais la table est renommée au pluriel afin de +respecter les conventions SQL du projet. + +Une recherche représente une opération documentée réalisée au cours d'une +enquête. + +Elle peut correspondre à : + +- une requête sur un moteur de recherche ; +- une consultation de profil social ; +- une requête DNS ou WHOIS ; +- une analyse de fichier ; +- une extraction de métadonnées ; +- une recherche dans une base publique ; +- une procédure manuelle ; +- l'exécution d'un script ou d'un outil OSINT. + +La recherche conserve le contexte de l'opération afin que son résultat puisse +être compris et reproduit. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la recherche | +| `source_id` | `TEXT` | non | Source principalement interrogée | +| `type_outil_id` | `INTEGER` | oui | Catégorie de l'outil utilisé | +| `outil_nom` | `TEXT` | oui | Nom précis de l'outil ou de la procédure | +| `requete` | `TEXT` | non | Requête, commande ou opération exécutée | +| `resultat` | `TEXT` | non | Résultat synthétique | +| `observations` | `TEXT` | non | Notes et interprétations | +| `started_at` | `TEXT` | oui | Début de l'opération en UTC | +| `completed_at` | `TEXT` | non | Fin de l'opération en UTC | +| `created_at` | `TEXT` | oui | Création de l'enregistrement | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de la recherche | + +--- + +## Statuts autorisés + +```text +planned +running +completed +failed +cancelled +archived +``` + +Une recherche échouée reste enregistrée. + +Un échec peut être important pour comprendre : + +- les méthodes déjà essayées ; +- les limitations rencontrées ; +- les changements d'une cible ; +- les erreurs techniques ; +- les étapes non reproductibles. + +--- + +## Relation avec les sources + +La colonne : + +```text +source_id +``` + +est facultative. + +Certaines recherches interrogent une source identifiable : + +```text +profil Instagram +site web +export téléphonique +document +``` + +D'autres opérations n'en possèdent pas directement : + +```text +calcul de hash +OCR local +analyse de métadonnées +script interne +``` + +La suppression logique ou physique d'une source ne doit pas supprimer +l'historique des recherches. + +La relation utilisera donc : + +```sql +ON DELETE SET NULL +``` + +--- + +## Relation avec les outils + +`types_outil` représente une catégorie technique : + +```text +dns_tool +whois_tool +web_browser +ocr_tool +custom_script +``` + +La colonne : + +```text +outil_nom +``` + +contient le nom concret utilisé : + +```text +dig +Firefox +ExifTool +Tesseract +labfy-whois +``` + +Cela évite de créer immédiatement une table `outils` alors que le besoin +d'un catalogue complet d'outils n'est pas encore établi. + +--- + +## Relation avec les preuves + +Une recherche peut : + +- utiliser une preuve comme donnée d'entrée ; +- produire une preuve comme résultat. + +Une preuve peut également être liée à plusieurs recherches. + +La relation est donc plusieurs-à-plusieurs et sera stockée dans : + +```text +recherche_preuves +``` + +Cette table comportera un rôle : + +```text +input +output +``` + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (source_id) + REFERENCES sources(id) + ON UPDATE CASCADE + ON DELETE SET NULL +``` + +```sql +FOREIGN KEY (type_outil_id) + REFERENCES types_outil(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(outil_nom)) > 0) +``` + +```sql +CHECK ( + requete IS NULL + OR length(trim(requete)) > 0 +) +``` + +```sql +CHECK ( + completed_at IS NULL + OR completed_at >= started_at +) +``` + +```sql +CHECK ( + status IN ( + 'planned', + 'running', + 'completed', + 'failed', + 'cancelled', + 'archived' + ) +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_recherches_source_id +ON recherches(source_id); +``` + +```sql +CREATE INDEX idx_recherches_type_outil_id +ON recherches(type_outil_id); +``` + +```sql +CREATE INDEX idx_recherches_status +ON recherches(status); +``` + +```sql +CREATE INDEX idx_recherches_started_at +ON recherches(started_at); +``` + +--- + +## Structure C cible + +```c +typedef struct Recherche Recherche; +``` + +Représentation privée envisagée : + +```c +struct Recherche +{ + char *id; + char *source_id; + + int type_outil_id; + char *outil_nom; + + char *requete; + char *resultat; + char *observations; + + char *started_at; + char *completed_at; + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure ne sera pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- renommage de `recherche` en `recherches` ; +- adoption d'un UUID comme clé primaire ; +- ajout d'une source optionnelle ; +- distinction entre catégorie et nom concret de l'outil ; +- ajout du contexte de la requête ; +- ajout des résultats et observations ; +- ajout des dates de début et de fin ; +- ajout d'un statut ; +- création d'une relation plusieurs-à-plusieurs avec les preuves. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +RENOMMER EN `recherches` +MODIFIER LE SCHÉMA +``` + +## Table historique `entites` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +entites +``` + +### Justification + +Le concept est central et doit être conservé. + +Une entité représente une information identifiable découverte au cours de +l'enquête. + +Elle peut être : + +- extraite d'une preuve ; +- produite par une recherche ; +- ajoutée manuellement ; +- enrichie progressivement ; +- reliée à d'autres entités. + +La table historique doit être renforcée avec : + +- un UUID ; +- un type obligatoire ; +- une valeur canonique ; +- une valeur d'affichage ; +- un statut ; +- des dates techniques ; +- des contraintes explicites. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'entité | +| `type_id` | `INTEGER` | oui | Référence vers `types_entite` | +| `valeur` | `TEXT` | oui | Valeur canonique utilisée pour les comparaisons | +| `label` | `TEXT` | non | Valeur lisible ou nom d'affichage | +| `description` | `TEXT` | non | Description libre | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `created_at` | `TEXT` | oui | Date de création dans l'enquête | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de l'entité | + +--- + +## Différence entre `valeur` et `label` + +La colonne : + +```text +valeur +``` + +contient la forme canonique utilisée pour les recherches et les doublons. + +Exemples : + +```text +contact@example.com ++33612345678 +arnaque_123 +example.com +192.0.2.10 +``` + +La colonne : + +```text +label +``` + +contient une forme d'affichage facultative. + +Exemples : + +```text +Adresse principale +Téléphone du vendeur +Compte Instagram suspect +Site de la société +``` + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +utilise une valeur entière comprise entre : + +```text +0 et 100 +``` + +Exemples : + +```text +100 = vérifié +80 = fortement probable +50 = à confirmer +20 = faible +0 = inconnu +``` + +La valeur par défaut est : + +```text +50 +``` + +--- + +## Statuts autorisés + +```text +active +archived +deleted +``` + +Une entité supprimée logiquement reste dans la base afin de préserver : + +- les recherches ; +- les relations ; +- les rapports ; +- la chronologie ; +- le journal. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (type_id) + REFERENCES types_entite(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (length(trim(valeur)) > 0) +``` + +```sql +CHECK ( + label IS NULL + OR length(trim(label)) > 0 +) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Unicité + +Une même valeur peut exister pour plusieurs types différents. + +Exemple : + +```text +valeur = "example.com" +``` + +pourrait être interprétée comme : + +- nom de domaine ; +- site web. + +L'unicité doit donc porter sur : + +```sql +UNIQUE (type_id, valeur) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_entites_type_id +ON entites(type_id); +``` + +```sql +CREATE INDEX idx_entites_valeur +ON entites(valeur); +``` + +```sql +CREATE INDEX idx_entites_status +ON entites(status); +``` + +```sql +CREATE INDEX idx_entites_confiance +ON entites(confiance); +``` + +--- + +## Relation avec les recherches + +Une recherche peut découvrir ou enrichir plusieurs entités. + +Une entité peut être liée à plusieurs recherches. + +La relation sera stockée dans : + +```text +recherche_entites +``` + +avec un rôle : + +```text +discovered +enriched +validated +contradicted +``` + +--- + +## Relation avec les preuves + +Une entité peut être extraite d'une ou plusieurs preuves. + +Une preuve peut contenir plusieurs entités. + +Cette relation sera stockée dans : + +```text +preuve_entites +``` + +--- + +## Structure C cible + +```c +typedef struct Entite Entite; +``` + +Représentation privée envisagée : + +```c +struct Entite +{ + char *id; + + int type_id; + + char *valeur; + char *label; + char *description; + + int confiance; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `entites` ; +- UUID comme clé primaire ; +- type obligatoire ; +- séparation entre valeur canonique et label d'affichage ; +- ajout d'un niveau de confiance ; +- ajout d'un statut logique ; +- ajout des dates techniques ; +- ajout d'une contrainte d'unicité ; +- ajout de relations avec les recherches et les preuves. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `entites` +``` + +## Table historique `associations` + +### Décision + +```text +RENOMMER ET MODIFIER +``` + +### Nom final + +```text +relations +``` + +### Justification + +Le concept de liaison entre objets métier doit être conservé, mais la table +historique `associations` est trop générique. + +Une relation représente un lien documenté entre deux entités. + +Exemples : + +- une personne utilise une adresse email ; +- un pseudonyme appartient probablement à une personne ; +- un compte social utilise un numéro de téléphone ; +- un IBAN est associé à une adresse email ; +- un domaine pointe vers une adresse IP ; +- une organisation possède un site web. + +Une relation n'est pas un simple raccord technique. Elle constitue une +information d'enquête à part entière. + +Elle doit donc posséder : + +- un UUID ; +- une entité source ; +- une entité cible ; +- un type de relation ; +- un niveau de confiance ; +- une justification ; +- des dates ; +- un statut. + +--- + +## Nom et orientation + +Une relation est orientée : + +```text +entite_source + │ + ▼ +entite_cible +``` + +Exemple : + +```text +Personne + │ utilise + ▼ +Adresse email +``` + +L'orientation doit être choisie de manière cohérente lors de la création. + +La relation inverse ne doit pas être créée automatiquement dans la base. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la relation | +| `entite_source_id` | `TEXT` | oui | Entité à l'origine de la relation | +| `entite_cible_id` | `TEXT` | oui | Entité ciblée par la relation | +| `type_relation` | `TEXT` | oui | Code stable décrivant la relation | +| `label` | `TEXT` | non | Libellé d'affichage facultatif | +| `justification` | `TEXT` | non | Explication du raisonnement | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de la relation | + +--- + +## Type de relation + +La colonne : + +```text +type_relation +``` + +contient un code technique stable. + +Exemples : + +```text +uses +owns +belongs_to +linked_to +communicates_with +resolves_to +registered_with +paid_to +same_as +possibly_same_as +``` + +Le libellé humain pourra être traduit plus tard. + +Pour la V1, aucune table de référence `types_relation` n'est imposée. + +Le type reste une chaîne contrôlée par l'application. + +Une table dédiée pourra être introduite si le catalogue devient important. + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +est comprise entre : + +```text +0 et 100 +``` + +Exemples : + +```text +100 = relation vérifiée +80 = fortement probable +50 = hypothèse raisonnable +20 = faible indice +0 = inconnu +``` + +--- + +## Statuts autorisés + +```text +active +archived +deleted +disputed +``` + +Le statut : + +```text +disputed +``` + +indique que la relation est contestée ou qu'un élément contradictoire a été +découvert. + +La relation reste conservée pour préserver l'historique du raisonnement. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +FOREIGN KEY (entite_source_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +FOREIGN KEY (entite_cible_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT +``` + +```sql +CHECK (entite_source_id <> entite_cible_id) +``` + +```sql +CHECK (length(trim(type_relation)) > 0) +``` + +```sql +CHECK ( + label IS NULL + OR length(trim(label)) > 0 +) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted', + 'disputed' + ) +) +``` + +--- + +## Unicité + +Un même couple d'entités peut posséder plusieurs types de relations. + +Exemple : + +```text +Personne A + owns +Compte X + +Personne A + uses +Compte X +``` + +L'unicité doit donc porter sur : + +```sql +UNIQUE ( + entite_source_id, + entite_cible_id, + type_relation +) +``` + +Cela empêche un doublon exact sans interdire plusieurs relations distinctes. + +--- + +## Relation avec les preuves + +Une relation peut être soutenue par plusieurs preuves. + +Une preuve peut soutenir plusieurs relations. + +La liaison sera stockée dans : + +```text +relation_preuves +``` + +--- + +## Relation avec les recherches + +Une relation peut être découverte, confirmée ou contredite par plusieurs +recherches. + +La liaison sera stockée dans : + +```text +recherche_relations +``` + +avec un rôle : + +```text +discovered +confirmed +contradicted +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_relations_entite_source_id +ON relations(entite_source_id); +``` + +```sql +CREATE INDEX idx_relations_entite_cible_id +ON relations(entite_cible_id); +``` + +```sql +CREATE INDEX idx_relations_type_relation +ON relations(type_relation); +``` + +```sql +CREATE INDEX idx_relations_status +ON relations(status); +``` + +```sql +CREATE INDEX idx_relations_confiance +ON relations(confiance); +``` + +--- + +## Structure C cible + +```c +typedef struct Relation Relation; +``` + +Représentation privée envisagée : + +```c +struct Relation +{ + char *id; + + char *entite_source_id; + char *entite_cible_id; + + char *type_relation; + char *label; + char *justification; + + int confiance; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- remplacement de `associations` par `relations` ; +- UUID comme clé primaire ; +- relation orientée entre deux entités ; +- ajout d'un type de relation ; +- ajout d'un niveau de confiance ; +- ajout d'une justification ; +- ajout d'un statut contestable ; +- ajout des dates techniques ; +- ajout de liaisons avec les preuves et les recherches ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +RENOMMER `associations` EN `relations` +MODIFIER PROFONDÉMENT LE SCHÉMA +``` + +## Table historique `chronologie` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +chronologie +``` + +### Justification + +Le concept doit être conservé. + +Une entrée de chronologie représente un événement important de l'enquête. + +Elle peut être : + +- saisie manuellement ; +- produite par une recherche ; +- associée à une preuve ; +- liée à une entité ; +- liée à une relation ; +- générée automatiquement par l'application. + +La chronologie ne doit pas être confondue avec le journal technique. + +La chronologie raconte les événements utiles à la compréhension de l'enquête. + +Le journal enregistre les actions réalisées dans le logiciel. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'événement | +| `event_time` | `TEXT` | oui | Date et heure de l'événement décrit | +| `titre` | `TEXT` | oui | Titre court | +| `description` | `TEXT` | non | Description détaillée | +| `origine` | `TEXT` | oui | Origine manuelle ou automatique | +| `created_at` | `TEXT` | oui | Date de création de l'enregistrement | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Origines autorisées + +```text +manual +automatic +imported +``` + +`manual` : + +- saisie directe par l'enquêteur. + +`automatic` : + +- génération par Labfy Investigation. + +`imported` : + +- événement importé depuis une source externe. + +--- + +## Statuts autorisés + +```text +active +archived +deleted +``` + +Une entrée supprimée logiquement reste conservée afin de préserver : + +- les rapports ; +- les références ; +- les liens avec les preuves ; +- l'historique de l'enquête. + +--- + +## Relation avec les objets métier + +Une entrée de chronologie peut être liée à plusieurs objets métier. + +Les relations sont stockées dans des tables spécialisées : + +```text +recherche_chronologie +preuve_chronologie +entite_chronologie +relation_chronologie +``` + +Cette solution est préférée à un couple polymorphe : + +```text +objet_type +objet_id +``` + +car elle permet de conserver de vraies clés étrangères SQLite. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(titre)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + origine IN ( + 'manual', + 'automatic', + 'imported' + ) +) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_chronologie_event_time +ON chronologie(event_time); +``` + +```sql +CREATE INDEX idx_chronologie_origine +ON chronologie(origine); +``` + +```sql +CREATE INDEX idx_chronologie_status +ON chronologie(status); +``` + +--- + +## Structure C cible + +```c +typedef struct EvenementChronologie EvenementChronologie; +``` + +Représentation privée envisagée : + +```c +struct EvenementChronologie +{ + char *id; + + char *event_time; + + char *titre; + char *description; + + char *origine; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `chronologie` ; +- UUID comme clé primaire ; +- séparation entre date de l'événement et dates techniques ; +- ajout d'un titre obligatoire ; +- ajout d'une origine ; +- ajout d'un statut logique ; +- ajout de relations explicites vers les objets métier ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `chronologie` +``` + +## Table historique `journal` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +journal +``` + +### Justification + +Le concept doit être conservé. + +Le journal constitue la trace d'audit des actions significatives réalisées +dans Labfy Investigation. + +Il ne décrit pas directement les événements de l'enquête. + +Il décrit les opérations effectuées dans le logiciel. + +Exemples : + +- création d'une enquête ; +- import d'une preuve ; +- modification d'une entité ; +- création d'une relation ; +- changement du niveau de confiance ; +- archivage d'un objet ; +- génération d'un rapport ; +- calcul ou vérification d'un hash. + +Le journal doit permettre de répondre aux questions suivantes : + +- quelle action a été effectuée ; +- quand elle a été effectuée ; +- sur quel objet ; +- avec quel résultat ; +- avec quels détails complémentaires. + +--- + +## Différence avec `chronologie` + +La table : + +```text +chronologie +``` + +raconte les événements utiles à la compréhension de l'enquête. + +La table : + +```text +journal +``` + +raconte les actions effectuées dans l'application. + +Exemple : + +```text +Chronologie : +Le compte Instagram suspect a été découvert à 14 h 32. + +Journal : +Une entité de type compte Instagram a été créée à 14 h 34. +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'entrée du journal | +| `event_time` | `TEXT` | oui | Date et heure de l'action | +| `action` | `TEXT` | oui | Code technique de l'action | +| `objet_type` | `TEXT` | non | Type de l'objet concerné | +| `objet_id` | `TEXT` | non | UUID de l'objet concerné | +| `resultat` | `TEXT` | oui | Résultat de l'action | +| `details` | `TEXT` | non | Informations complémentaires | +| `acteur` | `TEXT` | non | Utilisateur, processus ou composant ayant réalisé l'action | +| `created_at` | `TEXT` | oui | Date de création de l'enregistrement | + +--- + +## Actions prévues + +Les actions utilisent un code technique stable. + +Exemples : + +```text +create +update +archive +restore +delete +import +export +open +close +hash_compute +hash_verify +report_generate +relation_create +relation_dispute +``` + +La liste n'est pas limitée par une contrainte SQL afin de permettre +l'évolution du logiciel sans migration du schéma. + +L'application reste responsable de l'utilisation de codes cohérents. + +--- + +## Types d'objets possibles + +Exemples : + +```text +investigation +preuve +source +recherche +entite +relation +chronologie +hypothese +rapport +``` + +Une action peut ne concerner aucun objet précis. + +Exemple : + +```text +application_start +application_stop +``` + +Dans ce cas : + +```text +objet_type = NULL +objet_id = NULL +``` + +--- + +## Cohérence entre `objet_type` et `objet_id` + +Les deux colonnes doivent être : + +- toutes les deux nulles ; +- ou toutes les deux renseignées. + +Contrainte prévue : + +```sql +CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) +) +``` + +Le journal utilise volontairement une relation polymorphe. + +Aucune clé étrangère ne peut garantir automatiquement l'existence de +l'objet référencé. + +L'application doit vérifier la cohérence lors de l'écriture. + +--- + +## Résultats autorisés + +```text +success +failure +partial +cancelled +``` + +Une opération échouée doit rester enregistrée. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(action)) > 0) +``` + +```sql +CHECK ( + objet_type IS NULL + OR length(trim(objet_type)) > 0 +) +``` + +```sql +CHECK ( + objet_id IS NULL + OR length(trim(objet_id)) > 0 +) +``` + +```sql +CHECK ( + ( + objet_type IS NULL + AND objet_id IS NULL + ) + OR + ( + objet_type IS NOT NULL + AND objet_id IS NOT NULL + ) +) +``` + +```sql +CHECK ( + resultat IN ( + 'success', + 'failure', + 'partial', + 'cancelled' + ) +) +``` + +```sql +CHECK ( + details IS NULL + OR length(trim(details)) > 0 +) +``` + +```sql +CHECK ( + acteur IS NULL + OR length(trim(acteur)) > 0 +) +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_journal_event_time +ON journal(event_time); +``` + +```sql +CREATE INDEX idx_journal_action +ON journal(action); +``` + +```sql +CREATE INDEX idx_journal_objet +ON journal(objet_type, objet_id); +``` + +```sql +CREATE INDEX idx_journal_resultat +ON journal(resultat); +``` + +--- + +## Structure C cible + +```c +typedef struct EntreeJournal EntreeJournal; +``` + +Représentation privée envisagée : + +```c +struct EntreeJournal +{ + char *id; + + char *event_time; + char *action; + + char *objet_type; + char *objet_id; + + char *resultat; + char *details; + char *acteur; + + char *created_at; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Suppression + +Les entrées du journal ne doivent pas être modifiées ni supprimées par les +fonctions métier ordinaires. + +Une éventuelle purge devra être : + +- explicite ; +- réservée à une opération administrative ; +- documentée ; +- elle-même journalisée lorsque cela est possible. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `journal` ; +- UUID comme clé primaire ; +- séparation entre action, objet, résultat et détails ; +- ajout de l'acteur ; +- ajout de contraintes de cohérence ; +- ajout d'index ; +- affirmation du caractère append-only du journal. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `journal` +``` + +## Table historique `hypotheses` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +hypotheses +``` + +### Justification + +Le concept doit être conservé. + +Une hypothèse représente une proposition de travail formulée pendant +l'enquête. + +Elle ne doit pas être confondue avec : + +- une relation ; +- une preuve ; +- une note libre ; +- une conclusion définitive. + +Une hypothèse peut évoluer à mesure que de nouveaux éléments apparaissent. + +Elle doit pouvoir être : + +- proposée ; +- étudiée ; +- soutenue ; +- contredite ; +- confirmée ; +- rejetée ; +- archivée. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de l'hypothèse | +| `titre` | `TEXT` | oui | Titre court | +| `description` | `TEXT` | oui | Description détaillée | +| `confiance` | `INTEGER` | oui | Niveau de confiance de 0 à 100 | +| `evaluation` | `TEXT` | non | Justification du niveau de confiance | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique de l'hypothèse | + +--- + +## Niveau de confiance + +La colonne : + +```text +confiance +``` + +est comprise entre : + +```text +0 et 100 +``` + +Elle représente l'évaluation actuelle de l'enquêteur. + +Exemples : + +```text +0 = aucune confiance +25 = faible +50 = plausible +75 = fortement probable +100 = confirmé +``` + +La confiance ne doit pas être calculée automatiquement dans la V1. + +L'application pourra proposer des aides à l'évaluation plus tard, mais +l'enquêteur reste responsable de la valeur enregistrée. + +--- + +## Évaluation + +La colonne : + +```text +evaluation +``` + +explique pourquoi le niveau de confiance a été choisi. + +Exemple : + +```text +Deux preuves indépendantes, +un numéro de téléphone commun +et une relation confirmée. +``` + +Cette colonne est facultative, mais fortement recommandée lorsque la confiance +est modifiée. + +--- + +## Statuts autorisés + +```text +proposed +under_review +supported +contradicted +confirmed +rejected +archived +``` + +Signification : + +```text +proposed +``` + +Hypothèse nouvellement formulée. + +```text +under_review +``` + +Hypothèse en cours d'étude. + +```text +supported +``` + +Plusieurs éléments la soutiennent, sans confirmation définitive. + +```text +contradicted +``` + +Des éléments la contredisent. + +```text +confirmed +``` + +Hypothèse considérée comme confirmée. + +```text +rejected +``` + +Hypothèse écartée. + +```text +archived +``` + +Hypothèse conservée uniquement pour l'historique. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +CHECK (length(trim(titre)) > 0) +``` + +```sql +CHECK (length(trim(description)) > 0) +``` + +```sql +CHECK (confiance BETWEEN 0 AND 100) +``` + +```sql +CHECK ( + evaluation IS NULL + OR length(trim(evaluation)) > 0 +) +``` + +```sql +CHECK ( + status IN ( + 'proposed', + 'under_review', + 'supported', + 'contradicted', + 'confirmed', + 'rejected', + 'archived' + ) +) +``` + +--- + +## Relation avec les preuves + +Une hypothèse peut être : + +- soutenue ; +- contredite ; +- confirmée ; + +par plusieurs preuves. + +Une preuve peut intervenir dans plusieurs hypothèses. + +La relation est stockée dans : + +```text +hypothese_preuves +``` + +avec un rôle : + +```text +supports +contradicts +confirms +``` + +--- + +## Relation avec les entités + +Une hypothèse peut concerner plusieurs entités. + +La liaison est stockée dans : + +```text +hypothese_entites +``` + +--- + +## Relation avec les relations + +Une hypothèse peut s'appuyer sur des relations existantes ou les remettre en +cause. + +La liaison est stockée dans : + +```text +hypothese_relations +``` + +avec un rôle : + +```text +supports +contradicts +``` + +--- + +## Relation avec les recherches + +Une recherche peut : + +- produire une hypothèse ; +- l'enrichir ; +- la confirmer ; +- la contredire. + +La liaison est stockée dans : + +```text +recherche_hypotheses +``` + +avec un rôle : + +```text +created +enriched +confirmed +contradicted +``` + +--- + +## Index prévus + +```sql +CREATE INDEX idx_hypotheses_status +ON hypotheses(status); +``` + +```sql +CREATE INDEX idx_hypotheses_confiance +ON hypotheses(confiance); +``` + +```sql +CREATE INDEX idx_hypotheses_updated_at +ON hypotheses(updated_at); +``` + +--- + +## Structure C cible + +```c +typedef struct Hypothese Hypothese; +``` + +Représentation privée envisagée : + +```c +struct Hypothese +{ + char *id; + + char *titre; + char *description; + + int confiance; + char *evaluation; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +Cette structure n'est pas implémentée dans le ticket #023. + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept `hypotheses` ; +- UUID comme clé primaire ; +- ajout d'un titre obligatoire ; +- ajout d'une description obligatoire ; +- ajout d'un niveau de confiance ; +- ajout d'une évaluation explicative ; +- ajout d'un statut détaillé ; +- ajout des dates techniques ; +- ajout de relations vers les preuves, entités, relations et recherches ; +- ajout de contraintes et d'index. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `hypotheses` +``` + +## Nouvelle table `categories` + +### Décision + +```text +AJOUTER +``` + +### Nom final + +```text +categories +``` + +### Justification + +Le schéma historique ne distinguait pas clairement la classification +structurante des annotations libres. + +Une catégorie sert à classer un objet métier dans un domaine principal. + +Exemples : + +- OSINT ; +- finance ; +- réseaux sociaux ; +- téléphonie ; +- documents ; +- infrastructure. + +Une catégorie est stable, peu nombreuse et généralement administrée. + +Elle ne doit pas être confondue avec un tag, qui constitue une annotation +libre et potentiellement multiple. + +--- + +## Objets catégorisables dans la V1 + +Les catégories peuvent être attribuées à : + +```text +preuves +recherches +hypotheses +``` + +Chaque objet possède au maximum une catégorie. + +Les entités disposent déjà de `type_id`. + +Les relations disposent déjà de `type_relation`. + +Aucune catégorie supplémentaire n'est donc prévue pour elles dans la V1. + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID de la catégorie | +| `nom` | `TEXT` | oui | Nom visible | +| `description` | `TEXT` | non | Description libre | +| `icone` | `TEXT` | non | Nom d'icône du thème GTK | +| `couleur` | `TEXT` | non | Couleur hexadécimale | +| `system` | `INTEGER` | oui | Catégorie fournie par l'application | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Colonne `system` + +```text +system = 1 +``` + +indique une catégorie livrée avec Labfy Investigation. + +```text +system = 0 +``` + +indique une catégorie créée par l'utilisateur. + +L'application pourra empêcher la suppression ordinaire d'une catégorie +système. + +--- + +## Couleur + +La couleur facultative utilise le format : + +```text +#RRGGBB +``` + +Exemple : + +```text +#FF0000 +``` + +Le schéma vérifie uniquement la longueur et le préfixe. + +La validation complète des caractères hexadécimaux sera aussi effectuée dans +le code C. + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (nom COLLATE NOCASE) +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + icone IS NULL + OR length(trim(icone)) > 0 +) +``` + +```sql +CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) +) +``` + +```sql +CHECK (system IN (0, 1)) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Structure C cible + +```c +typedef struct Categorie Categorie; +``` + +Représentation privée envisagée : + +```c +struct Categorie +{ + char *id; + + char *nom; + char *description; + char *icone; + char *couleur; + + gboolean system; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +--- + +## Décision finale + +```text +AJOUTER LA TABLE `categories` +``` + +## Table historique `tags` + +### Décision + +```text +MODIFIER +``` + +### Nom final + +```text +tags +``` + +### Justification + +Le concept doit être conservé. + +Un tag est une annotation libre et transversale. + +Contrairement à une catégorie : + +- plusieurs tags peuvent être appliqués au même objet ; +- les utilisateurs peuvent en créer librement ; +- ils ne structurent pas le modèle métier principal. + +Exemples : + +- urgent ; +- à vérifier ; +- escroquerie ; +- Telegram ; +- fraude ; +- prioritaire. + +--- + +## Objets pouvant recevoir des tags + +Dans la V1 : + +```text +preuves +recherches +entites +relations +hypotheses +chronologie +``` + +--- + +## Colonnes finales proposées + +| Colonne | Type | Obligatoire | Description | +|---|---:|:---:|---| +| `id` | `TEXT` | oui | UUID du tag | +| `nom` | `TEXT` | oui | Nom visible | +| `description` | `TEXT` | non | Description libre | +| `couleur` | `TEXT` | non | Couleur hexadécimale | +| `system` | `INTEGER` | oui | Tag fourni par l'application | +| `created_at` | `TEXT` | oui | Date de création | +| `updated_at` | `TEXT` | oui | Dernière modification | +| `status` | `TEXT` | oui | État logique | + +--- + +## Contraintes prévues + +```sql +PRIMARY KEY (id) +``` + +```sql +UNIQUE (nom COLLATE NOCASE) +``` + +```sql +CHECK (length(trim(nom)) > 0) +``` + +```sql +CHECK ( + description IS NULL + OR length(trim(description)) > 0 +) +``` + +```sql +CHECK ( + couleur IS NULL + OR ( + length(couleur) = 7 + AND substr(couleur, 1, 1) = '#' + ) +) +``` + +```sql +CHECK (system IN (0, 1)) +``` + +```sql +CHECK ( + status IN ( + 'active', + 'archived', + 'deleted' + ) +) +``` + +--- + +## Structure C cible + +```c +typedef struct Tag Tag; +``` + +Représentation privée envisagée : + +```c +struct Tag +{ + char *id; + + char *nom; + char *description; + char *couleur; + + gboolean system; + + char *created_at; + char *updated_at; + + char *status; +}; +``` + +--- + +## Modifications par rapport au schéma historique + +- conservation du concept ; +- UUID comme clé primaire ; +- unicité du nom sans tenir compte de la casse ; +- ajout d'une description ; +- ajout d'une couleur ; +- distinction entre tags système et utilisateur ; +- ajout des dates techniques ; +- ajout de la suppression logique ; +- ajout de tables de liaison explicites. + +--- + +## Décision finale + +```text +CONSERVER LE CONCEPT +MODIFIER LE SCHÉMA +CONSERVER LE NOM `tags` +``` + diff --git a/docs/tickets/closed/TICKET-023.md b/docs/tickets/closed/TICKET-023.md new file mode 100644 index 0000000..377d2f9 --- /dev/null +++ b/docs/tickets/closed/TICKET-023.md @@ -0,0 +1,643 @@ +# Ticket #023 + +## Titre + +Consolider le schéma métier initial de l'enquête. + +--- + +## Objectif + +Transformer le schéma SQLite préparé lors des premières phases du projet en +un schéma métier officiel, cohérent, documenté et testable. + +La base existante sert de fondation. + +Le travail ne consiste pas à repartir de zéro, mais à : + +- conserver les concepts métier déjà définis ; +- corriger les incohérences ; +- renforcer les contraintes ; +- uniformiser les conventions ; +- intégrer le schéma au module `Database`. + +--- + +## Base de travail + +La base historique contient notamment les tables suivantes : + +```text +preuves +entites +personnes +chronologie +sources +recherche +hypotheses +journal +tags +types_preuve +types_entite +types_source +types_outils +associations +personnes_entites +entite_tags +preuves_tag +``` + +Les tables créées au ticket #022 restent également obligatoires : + +```text +metadata +investigation +``` + +Le schéma officiel doit donc préserver ces concepts tout en les consolidant. + +--- + +## Architecture + +```text +InvestigationProject + │ + ▼ +Database + │ + ▼ +Schéma SQLite versionné + │ + ├── Métadonnées + ├── Enquête + ├── Preuves + ├── Entités + ├── Personnes + ├── Sources + ├── Chronologie + ├── Recherches + ├── Hypothèses + ├── Journal + └── Tags et relations +``` + +Le module `Database` reste le seul autorisé à exécuter du SQL. + +--- + +## Décisions de conception à officialiser + +### Identifiants + +Les entités métier utiliseront des identifiants texte de type UUID. + +Exemple : + +```text +550e8400-e29b-41d4-a716-446655440000 +``` + +Les identifiants sont générés par l'application avec : + +```c +g_uuid_string_random() +``` + +Les tables de référence simples peuvent utiliser des identifiants entiers. + +--- + +### Dates + +Les dates techniques sont stockées en UTC au format ISO 8601 : + +```text +YYYY-MM-DDTHH:MM:SSZ +``` + +Exemple : + +```text +2026-07-15T08:42:17Z +``` + +--- + +### Nommage + +Les noms SQL utilisent uniquement : + +- des minuscules ; +- du `snake_case` ; +- aucun accent ; +- aucun espace. + +Les noms des tables de liaison sont uniformisés. + +Exemple : + +```text +preuve_tags +entite_tags +personne_entites +``` + +Aucune forme singulier/pluriel incohérente ne doit subsister. + +--- + +### Clés étrangères + +Toutes les relations métier doivent utiliser des clés étrangères explicites. + +Le module `Database` doit activer : + +```sql +PRAGMA foreign_keys = ON; +``` + +à chaque ouverture de connexion. + +Les comportements `ON DELETE` doivent être décidés pour chaque relation. + +--- + +### Tables de liaison + +Chaque table de liaison doit empêcher les doublons grâce à : + +- une clé primaire composite ; +- ou une contrainte `UNIQUE`. + +Exemple : + +```sql +PRIMARY KEY (preuve_id, tag_id) +``` + +--- + +## Tables métier à consolider + +### `preuves` + +Responsabilité : + +- représenter un élément de preuve ; +- mémoriser son nom ; +- son chemin relatif ; +- son type ; +- sa description ; +- ses dates ; +- son hash éventuel ; +- son statut. + +Contraintes minimales : + +- identifiant obligatoire ; +- nom obligatoire ; +- chemin relatif obligatoire ; +- type obligatoire ; +- date de création obligatoire ; +- chemin unique au sein d'une enquête. + +--- + +### `entites` + +Responsabilité : + +- représenter une entité identifiée pendant l'enquête. + +Exemples : + +- adresse email ; +- compte bancaire ; +- profil social ; +- pseudonyme ; +- téléphone ; +- document d'identité. + +Contraintes minimales : + +- identifiant obligatoire ; +- type obligatoire ; +- valeur obligatoire ; +- date de création obligatoire. + +--- + +### `personnes` + +Responsabilité : + +- représenter une personne physique ou un profil humain étudié. + +Le modèle ne doit pas obliger à connaître l'identité civile complète. + +Une personne peut n'avoir qu'un pseudonyme ou une désignation temporaire. + +--- + +### `sources` + +Responsabilité : + +- représenter l'origine d'une information ou d'une preuve. + +Exemples : + +- site web ; +- email ; +- réseau social ; +- document ; +- témoignage ; +- outil OSINT. + +--- + +### `chronologie` + +Responsabilité : + +- mémoriser les événements importants de l'enquête ; +- relier éventuellement un événement à une preuve, une entité ou une source. + +--- + +### `recherches` + +Responsabilité : + +- documenter une opération de recherche ; +- mémoriser la requête ; +- l'outil employé ; +- la date ; +- le résultat ; +- les observations. + +--- + +### `hypotheses` + +Responsabilité : + +- enregistrer une hypothèse de travail ; +- mémoriser son statut ; +- sa justification ; +- les éléments qui la soutiennent ou la contredisent. + +--- + +### `journal` + +Responsabilité : + +- conserver la trace des actions significatives réalisées dans l'enquête. + +Exemples : + +- création d'une preuve ; +- modification d'une entité ; +- calcul d'un hash ; +- génération d'un rapport ; +- import d'un fichier. + +--- + +### `tags` + +Responsabilité : + +- permettre une classification libre des preuves et des entités. + +--- + +## Tables de référence + +Les tables suivantes doivent être consolidées : + +```text +types_preuve +types_entite +types_source +types_outils +``` + +Elles doivent comporter au minimum : + +```sql +id INTEGER PRIMARY KEY +code TEXT NOT NULL UNIQUE +label TEXT NOT NULL +description TEXT +``` + +Les valeurs initiales sont insérées lors de la création de la base. + +--- + +## Tables de liaison + +Les relations suivantes doivent être prévues : + +```text +preuve_tags +entite_tags +personne_entites +``` + +Le schéma existant contient également une table générique : + +```text +associations +``` + +Cette table doit être auditée avant conservation. + +Elle ne doit être gardée que si son rôle est clairement défini et ne fait pas +doublon avec des relations spécialisées. + +--- + +## Audit obligatoire + +Avant d'écrire le schéma définitif, chaque table de la base historique doit +être classée dans l'une des catégories suivantes : + +```text +CONSERVER +MODIFIER +RENOMMER +FUSIONNER +SUPPRIMER +``` + +La décision doit être documentée dans : + +```text +docs/database/SCHEMA_AUDIT_V1.md +``` + +Le document doit contenir au minimum : + +```text +Nom historique +Décision +Nom final +Justification +Modifications prévues +``` + +--- + +## Organisation du module Database + +Le code SQL est organisé par domaine métier. + +``` +include/ +└── database/ + ├── database.h + ├── schema.h + ├── preuve.h + ├── entite.h + ├── personne.h + ├── source.h + ├── chronologie.h + ├── recherche.h + ├── hypothese.h + ├── journal.h + ├── tag.h + ├── type_preuve.h + ├── type_entite.h + ├── type_source.h + └── type_outil.h + +src/ +└── database/ + ├── database.c + ├── schema.c + ├── preuve.c + ├── entite.c + ├── personne.c + ├── source.c + ├── chronologie.c + ├── recherche.c + ├── hypothese.c + ├── journal.c + ├── tag.c + ├── type_preuve.c + ├── type_entite.c + ├── type_source.c + └── type_outil.c + +database/ +├── schema_v1.sql +├── indexes.sql +├── triggers.sql +└── reference_data.sql +``` + +--- + +### Responsabilités + +`database.c` + +- ouverture de SQLite ; +- fermeture ; +- transactions ; +- erreurs ; +- point d'entrée du module Database. + +`schema.c` + +- installation du schéma ; +- migrations ; +- exécution des scripts SQL. + +Chaque module métier est responsable exclusivement de son objet métier. + +Exemple : + +``` +preuve.c +``` + +gère uniquement : + +- création ; +- lecture ; +- modification ; +- suppression logique ; +- gestion des tags de la preuve. + +Les tables de liaison simples ne possèdent pas de module dédié. + +Exemples : + +``` +preuve_tags +personne_entites +entite_tags +``` + +Leur manipulation est réalisée par le module métier correspondant. + +--- + +## Intégration dans `Database` + +Le module `database_initialize()` doit exécuter le schéma officiel contenu +dans : + +```text +database/schema_v1.sql +``` + +Deux approches sont acceptables : + +1. intégrer le schéma au binaire via une ressource GLib ; +2. générer une chaîne C depuis le fichier SQL lors de la compilation. + +Le logiciel installé ne doit pas dépendre d'un chemin relatif fragile vers le +dépôt source. + +--- + +## Principe d'organisation + +Une table métier importante implique systématiquement : + +- une structure C ; +- un module C ; +- des tests ; +- une documentation. + +La correspondance est la suivante : + +| SQL | C | +|-----|---| +| preuves | preuve.c | +| entites | entite.c | +| personnes | personne.c | +| sources | source.c | +| chronologie | chronologie.c | +| journal | journal.c | +| recherches | recherche.c | +| hypotheses | hypothese.c | + +--- + +## Version du schéma + +Le numéro de version reste : + +```text +1 +``` + +tant que Labfy Investigation n'a pas produit de base officiellement publiée. + +Une fois le schéma V1 considéré comme stable et distribué, toute modification +incompatible nécessitera : + +- une nouvelle version ; +- une migration dédiée ; +- une mise à jour de `schema_version`. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- développer les fonctions CRUD ; +- créer les structures C `Preuve`, `Entite` ou `Personne` ; +- connecter les tables à GTK ; +- importer les anciennes données automatiquement ; +- créer les migrations V2 ; +- produire un rapport ; +- ajouter la recherche plein texte. + +--- + +## Contraintes techniques + +- C17 ; +- SQLite3 ; +- GLib ; +- aucune dépendance GTK ; +- toutes les tables utilisent des contraintes explicites ; +- toutes les clés étrangères sont documentées ; +- toutes les tables de liaison empêchent les doublons ; +- aucun SQL métier hors du module `Database` ; +- aucun identifiant généré par SQLite pour les objets métier en UUID ; +- compilation sans warning. + +--- + +## Tests + +Faire évoluer : + +```text +tests/test_database.c +``` + +Les tests doivent vérifier : + +- la présence de toutes les tables officielles ; +- la présence des tables de référence ; +- l'insertion des valeurs initiales ; +- l'activation des clés étrangères ; +- le refus d'une clé étrangère invalide ; +- le refus d'un doublon dans une table de liaison ; +- le refus des colonnes obligatoires manquantes ; +- l'unicité des codes des tables de référence ; +- la valeur `schema_version = 1` ; +- l'intégrité SQLite avec : + +```sql +PRAGMA integrity_check; +``` + +--- + +## Critères d'acceptation + +- [ ] Le schéma historique a été audité. +- [ ] Chaque table possède une décision documentée. +- [ ] Un schéma V1 officiel existe. +- [ ] Les conventions de nommage sont uniformes. +- [ ] Toutes les clés primaires sont explicites. +- [ ] Toutes les clés étrangères sont explicites. +- [ ] Les relations empêchent les doublons. +- [ ] Les colonnes obligatoires utilisent `NOT NULL`. +- [ ] Les tables de référence sont initialisées. +- [ ] `database_initialize()` installe le schéma complet. +- [ ] `PRAGMA integrity_check` retourne `ok`. +- [ ] Tous les tests restent valides. +- [ ] Aucune dépendance GTK. + +--- + +## Livrables + +```text +docs/database/SCHEMA_AUDIT_V1.md +database/schema_v1.sql +include/database/database.h +src/database/database.c +tests/test_database.c +``` + +--- + +## Commit attendu + +```text +feat(database): consolidate initial investigation schema +``` diff --git a/include/database/database.h b/include/database/database.h index c254658..74301e8 100644 --- a/include/database/database.h +++ b/include/database/database.h @@ -11,19 +11,10 @@ /** * @brief Initialise la base SQLite d'une enquête. * - * La fonction crée ou ouvre le fichier SQLite indiqué, puis initialise - * transactionnellement le schéma minimal de l'enquête. + * La fonction crée ou ouvre le fichier SQLite indiqué, installe le schéma + * courant, puis enregistre les métadonnées de l'enquête. * - * Les informations suivantes sont enregistrées : - * - * - version du schéma ; - * - nom de l'application ; - * - date de création UTC ; - * - UUID de l'enquête ; - * - nom de l'enquête ; - * - chemin racine de l'enquête. - * - * Aucun handle sqlite3 n'est exposé au code appelant. + * Aucun handle SQLite n'est exposé au code appelant. * * @param database_path * Chemin complet du fichier Enquete.sqlite. diff --git a/include/database/schema.h b/include/database/schema.h new file mode 100644 index 0000000..e3f5295 --- /dev/null +++ b/include/database/schema.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * @file schema.h + * @brief Interface interne d'installation du schéma SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_SCHEMA_H +#define LABFY_INVESTIGATION_SCHEMA_H + +#include + +#include + +/** + * @brief Installe le schéma SQLite V1 dans une base ouverte. + * + * La connexion doit être valide et une transaction doit déjà être active. + * + * Cette fonction crée : + * + * - les tables ; + * - les index ; + * - les valeurs de référence. + * + * Elle ne réalise ni COMMIT ni ROLLBACK. + * + * @param database Connexion SQLite ouverte. + * + * @return true si le schéma a été correctement installé, sinon false. + */ +bool schema_install_v1( + sqlite3 *database +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index 93e2c01..e7b6317 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/resources/database.gresource.xml b/resources/database.gresource.xml new file mode 100644 index 0000000..1f4c1a1 --- /dev/null +++ b/resources/database.gresource.xml @@ -0,0 +1,8 @@ + + + + + ../database/schema_v1.sql + + + diff --git a/src/database/database.c b/src/database/database.c index af750a8..04af0e0 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -4,6 +4,7 @@ ******************************************************************************/ #include "database/database.h" +#include "database/schema.h" #include #include @@ -18,28 +19,6 @@ */ #define DATABASE_APPLICATION_NAME "Labfy Investigation" -/** - * @brief Crée la table contenant les métadonnées de l'enquête. - */ -static const char *const database_create_metadata_table_sql = - "CREATE TABLE metadata" - "(" - " key TEXT PRIMARY KEY," - " value TEXT NOT NULL" - ");"; - -/** - * @brief Crée la table représentant l'enquête courante. - */ -static const char *const database_create_investigation_table_sql = - "CREATE TABLE investigation" - "(" - " id INTEGER PRIMARY KEY CHECK (id = 1)," - " name TEXT NOT NULL," - " root_path TEXT NOT NULL," - " created_at TEXT NOT NULL" - ");"; - /** * @brief Requête d'insertion d'une métadonnée. */ @@ -55,11 +34,13 @@ static const char *const database_insert_investigation_sql = " id," " name," " root_path," - " created_at" + " created_at," + " updated_at" ")" "VALUES" "(" - " 1," + " ?," + " ?," " ?," " ?," " ?" @@ -319,6 +300,7 @@ static bool database_insert_all_metadata( */ static bool database_insert_investigation( sqlite3 *database, + const char *investigation_uuid, const char *investigation_name, const char *investigation_root_path, const char *created_at @@ -329,6 +311,7 @@ static bool database_insert_investigation( bool success = false; if (database == NULL || + investigation_uuid == NULL || investigation_name == NULL || investigation_root_path == NULL || created_at == NULL) @@ -357,7 +340,7 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 1, - investigation_name, + investigation_uuid, -1, SQLITE_TRANSIENT ); @@ -370,7 +353,7 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 2, - investigation_root_path, + investigation_name, -1, SQLITE_TRANSIENT ); @@ -383,6 +366,32 @@ static bool database_insert_investigation( result = sqlite3_bind_text( statement, 3, + investigation_root_path, + -1, + SQLITE_TRANSIENT + ); + + if (result != SQLITE_OK) + { + goto cleanup; + } + + result = sqlite3_bind_text( + statement, + 4, + created_at, + -1, + SQLITE_TRANSIENT + ); + + if (result != SQLITE_OK) + { + goto cleanup; + } + + result = sqlite3_bind_text( + statement, + 5, created_at, -1, SQLITE_TRANSIENT @@ -530,22 +539,11 @@ bool database_initialize( transaction_started = true; - if (!database_execute_sql( - database, - database_create_metadata_table_sql - )) + if (!schema_install_v1(database)) { goto rollback; } - - if (!database_execute_sql( - database, - database_create_investigation_table_sql - )) - { - goto rollback; - } - + if (!database_insert_all_metadata( database, created_at, @@ -557,6 +555,7 @@ bool database_initialize( if (!database_insert_investigation( database, + investigation_uuid, investigation_name, investigation_root_path, created_at diff --git a/src/database/schema.c b/src/database/schema.c new file mode 100644 index 0000000..b237b9c --- /dev/null +++ b/src/database/schema.c @@ -0,0 +1,91 @@ +/****************************************************************************** + * @file schema.c + * @brief Installation du schéma SQLite de Labfy Investigation. + ******************************************************************************/ + +#include "database/schema.h" + +#include + +/** + * @brief Charge le schéma SQL V1 depuis les ressources intégrées. + * + * @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), + * ou NULL en cas d'échec. + */ +static char *schema_load_v1_sql(void) +{ + char *schema_sql = NULL; + GError *error = NULL; + + if (!g_file_get_contents( + "database/schema_v1.sql", + &schema_sql, + NULL, + &error + )) + { + g_warning( + "Impossible de charger database/schema_v1.sql : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + g_clear_error(&error); + + return NULL; + } + + return schema_sql; +} + +bool schema_install_v1( + sqlite3 *database +) +{ + char *schema_sql = NULL; + char *error_message = NULL; + + int result = SQLITE_ERROR; + + if (database == NULL) + { + return false; + } + + schema_sql = schema_load_v1_sql(); + + if (schema_sql == NULL) + { + return false; + } + + result = sqlite3_exec( + database, + schema_sql, + NULL, + NULL, + &error_message + ); + + g_free(schema_sql); + + if (result != SQLITE_OK) + { + g_warning( + "Impossible d'installer le schéma SQLite V1 : %s", + error_message != NULL + ? error_message + : sqlite3_errmsg(database) + ); + + sqlite3_free(error_message); + + return false; + } + + sqlite3_free(error_message); + + return true; +} diff --git a/tests/test_database b/tests/test_database index bad3911..a6f4d90 100755 Binary files a/tests/test_database and b/tests/test_database differ diff --git a/tests/test_database.c b/tests/test_database.c index b227363..7912035 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -131,6 +131,8 @@ static void test_database_initialize_valid_database(void) char *investigation_name = NULL; char *investigation_root_path = NULL; char *investigation_created_at = NULL; + char *investigation_id = NULL; + char *investigation_count = NULL; sqlite3 *database = NULL; GError *error = NULL; @@ -214,19 +216,31 @@ static void test_database_initialize_valid_database(void) investigation_name = test_database_read_single_text( database, "SELECT name FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" ); investigation_root_path = test_database_read_single_text( database, "SELECT root_path FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" ); investigation_created_at = test_database_read_single_text( database, "SELECT created_at FROM investigation " - "WHERE id = 1;" + "LIMIT 1;" + ); + + investigation_id = test_database_read_single_text( + database, + "SELECT id FROM investigation " + "LIMIT 1;" + ); + + investigation_count = test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM investigation;" ); assert(strcmp(schema_version, "1") == 0); @@ -246,6 +260,11 @@ static void test_database_initialize_valid_database(void) assert(g_remove(database_path) == 0); assert(g_rmdir(temporary_directory) == 0); + assert(investigation_id[0] != '\0'); + assert(g_uuid_string_is_valid(investigation_id)); + + assert(strcmp(investigation_count, "1") == 0); + g_free(investigation_created_at); g_free(investigation_root_path); g_free(investigation_name); @@ -255,6 +274,8 @@ static void test_database_initialize_valid_database(void) g_free(schema_version); g_free(database_path); g_free(temporary_directory); + g_free(investigation_id); + g_free(investigation_count); } /** diff --git a/tests/test_investigation_project b/tests/test_investigation_project index 6bca42e..18cee8e 100755 Binary files a/tests/test_investigation_project and b/tests/test_investigation_project differ