Commit a0c05f5d authored by Kourser's avatar Kourser
Browse files

Avatars : marques dédiées pour les comptes récurrents des kits

Un visage tiré au sort pour « @ANSSI_FR » ou « @gouv_officiel » casse la crédibilité
d'un exercice. Ces comptes reçoivent désormais une marque institutionnelle, résolue
avant le tirage de la banque : agence de cybersécurité, compte gouvernemental,
autorité publique, éditeur logiciel, assistance, média.

Les fichiers sont des CRÉATIONS ORIGINALES, pas les logos réels — et c'est un choix,
pas un raccourci. La Licence Ouverte 2.0 sous laquelle les chronogrammes REMPAR25
sont repris exclut explicitement les logos et marques de sa concession, et le
bloc-marque de l'État est réservé aux administrations : l'apposer dans un produit
redistribué sous AGPL pourrait laisser croire à un adoubement officiel.

Le mécanisme laisse la décision à l'exploitant : substituer un vrai logo revient à
remplacer un fichier de `apps/web/public/avatars/accounts/`, sans toucher au code.
Ajouter un compte reconnu = une ligne dans la table, plus son SVG. C'est documenté
dans SECURITY.md, sous une section dédiée aux marques de tiers.

Le pseudo est normalisé avant comparaison (arobase, casse, ponctuation), les kits
écrivant « ANSSI_FR » là où le moteur manipule des formes minuscules.

Six cas de test, dont la priorité des sources : logo d'organisation, puis marque
dédiée, puis banque — et la stabilité du tirage pour les comptes inconnus.

Non compilé ni exécuté (pas de toolchain Node dans l'environnement utilisé).

Co-Authored-By: Claude (RCA)
parent 8455d740
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -65,6 +65,22 @@ Cet accès est encadré :
- **Audité** : chaque ouverture est consignée (`participant.impersonate` — qui a observé qui,
  jamais le jeton).

## Marques et logos de tiers

Les comptes récurrents des kits (agence de cybersécurité, compte gouvernemental,
éditeur, assistance) portent des avatars **créés pour le produit**, et non les logos
réels des organismes cités.

C'est délibéré. La Licence Ouverte 2.0, sous laquelle les chronogrammes REMPAR25 sont
repris, **exclut les logos et marques** de sa concession. Le bloc-marque de l'État
(Marianne) est par ailleurs réservé aux administrations : l'apposer dans un produit
redistribué pourrait laisser croire à un adoubement officiel.

Un exploitant qui dispose des autorisations nécessaires — ou qui n'utilise la
plateforme qu'en interne — peut substituer ses propres fichiers dans
`apps/web/public/avatars/accounts/` sans modifier une ligne de code. Cette décision
lui appartient ; elle n'est pas prise par défaut à sa place.

## RGPD

- **Minimisation** : les joueurs n'ont pas de compte ; les scénarios doivent utiliser des
+38 −7
Original line number Diff line number Diff line
import {
  AVATAR_BANK_SIZE,
  avatarForAuthor,
  bankAvatar,
  DEFAULT_ORG_AVATAR,
  fnv1a,
} from './avatar-bank';
import { AVATAR_BANK_SIZE, DEFAULT_ORG_AVATAR, accountAvatar, avatarForAuthor, bankAvatar, fnv1a } from './avatar-bank';

describe('fnv1a', () => {
  it('est deterministe et sensible a la moindre difference', () => {
@@ -81,3 +75,40 @@ describe('avatarForAuthor', () => {
    ).not.toBe('/l');
  });
});

describe('accountAvatar', () => {
  it('reconnaît les comptes récurrents des kits', () => {
    expect(accountAvatar('ANSSI_FR')).toBe('/avatars/accounts/anssi.svg');
    expect(accountAvatar('gouv_officiel')).toBe('/avatars/accounts/gouv.svg');
  });

  it('normalise arobase, casse et ponctuation du pseudo', () => {
    expect(accountAvatar('@ANSSI_FR')).toBe(accountAvatar('anssifr'));
    expect(accountAvatar('Gouv-Officiel')).toBe('/avatars/accounts/gouv.svg');
  });

  it('renvoie null pour un compte quelconque', () => {
    expect(accountAvatar('theo_lyon')).toBeNull();
  });
});

describe('avatarForAuthor — priorité des sources', () => {
  const official = { officialHandle: 'villedemeridien', logoUrl: '/api/exercises/x/logo' };

  it('donne son logo au compte officiel de l’organisation', () => {
    expect(avatarForAuthor({ handle: 'villedemeridien', ...official })).toBe(official.logoUrl);
  });

  it('préfère la marque dédiée au tirage de la banque', () => {
    // Sans cette priorité, une autorité publique héritait d'un visage quelconque.
    expect(avatarForAuthor({ handle: 'ANSSI_FR', ...official })).toBe(
      '/avatars/accounts/anssi.svg',
    );
  });

  it('retombe sur la banque pour un compte inconnu, de façon stable', () => {
    const a = avatarForAuthor({ handle: 'karim_actu', ...official });
    expect(a).toMatch(/^\/avatars\/a\d{2}\.svg$/);
    expect(avatarForAuthor({ handle: 'karim_actu', ...official })).toBe(a);
  });
});
+39 −3
Original line number Diff line number Diff line
/**
 * Avatars des comptes sur les reseaux simules — logique pure et testable.
 *
 * Trois cas, dans cet ordre :
 * Quatre cas, dans cet ordre :
 * 1. compte officiel de l'organisation -> son logo, s'il en existe un ;
 * 2. compte officiel sans logo -> avatar « organisation » generique ;
 * 3. tout autre compte -> une image de la banque embarquee.
 * 3. compte recurrent des kits (agence, gouvernement, editeur...) -> sa marque dediee ;
 * 4. tout autre compte -> une image de la banque embarquee.
 *
 * L'attribution dans la banque est DETERMINISTE (empreinte du pseudo) et non
 * aleatoire a chaque rendu : un compte qui change de visage d'un rechargement a
@@ -22,6 +23,39 @@ const BANK_BASE = '/avatars';
/** Avatar d'une organisation sans logo televerse. */
export const DEFAULT_ORG_AVATAR = `${BANK_BASE}/org.svg`;

/**
 * Avatars dedies a des comptes recurrents des kits, par pseudo normalise.
 *
 * Un visage tire au sort pour « @ANSSI_FR » casse la credibilite d'un exercice :
 * ces comptes-la meritent une marque institutionnelle. Les fichiers sont des
 * creations ORIGINALES et non les logos reels — la Licence Ouverte 2.0 qui couvre
 * les chronogrammes ANSSI exclut les logos et marques de sa concession, et le
 * bloc-marque de l'Etat est reserve aux administrations.
 *
 * Un exploitant qui dispose des autorisations necessaires peut remplacer le
 * fichier correspondant dans `apps/web/public/avatars/accounts/` : aucun code a
 * modifier. Ajouter un compte = une ligne ici, plus le SVG.
 */
const ACCOUNT_AVATARS: Record<string, string> = {
  anssifr: 'anssi',
  gouvofficiel: 'gouv',
  prefecture: 'agence',
  ars: 'agence',
  editeuredr: 'editeur',
  assistancesecours24: 'support',
};

/** Pseudo ramene a sa forme comparable : minuscules, sans ponctuation. */
function normalizeHandle(handle: string): string {
  return handle.replace(/^@+/, '').toLowerCase().replace(/[^a-z0-9]/g, '');
}

/** Avatar dedie d'un compte connu, ou null s'il n'en a pas. */
export function accountAvatar(handle: string): string | null {
  const file = ACCOUNT_AVATARS[normalizeHandle(handle)];
  return file ? `${BANK_BASE}/accounts/${file}.svg` : null;
}

/**
 * Empreinte FNV-1a 32 bits. Choisie pour sa stabilite et sa brievete : on ne
 * cherche aucune propriete cryptographique, seulement une repartition correcte.
@@ -63,5 +97,7 @@ export function avatarForAuthor(opts: {
  if (handle === opts.officialHandle.replace(/^@+/, '').toLowerCase()) {
    return opts.logoUrl ?? DEFAULT_ORG_AVATAR;
  }
  return bankAvatar(handle);
  // Compte reconnu des kits avant le tirage : c'est ce qui evite un visage
  // quelconque sur une autorite publique.
  return accountAvatar(handle) ?? bankAvatar(handle);
}
+10 −0
Original line number Diff line number Diff line
<!-- Marque générique d'autorité publique (préfecture, ARS, régulateur…). -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Autorité publique">
  <rect width="64" height="64" rx="12" fill="#1c4b7a"/>
  <path d="M32 15l17 9H15z" fill="#cfe3f7"/>
  <g fill="#cfe3f7">
    <rect x="19" y="27" width="5" height="16"/><rect x="29.5" y="27" width="5" height="16"/>
    <rect x="40" y="27" width="5" height="16"/>
  </g>
  <rect x="15" y="45" width="34" height="4" fill="#cfe3f7"/>
</svg>
+8 −0
Original line number Diff line number Diff line
<!-- Marque ORIGINALE d'agence de cybersécurité. Ce n'est PAS le logo de l'ANSSI,
     dont la Licence Ouverte 2.0 des chronogrammes exclut la concession. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Agence de cybersécurité">
  <rect width="64" height="64" rx="12" fill="#12305c"/>
  <path d="M32 13l15 6v13c0 11-6.6 17.6-15 20-8.4-2.4-15-9-15-20V19z" fill="none" stroke="#7fb2ff" stroke-width="3"/>
  <text x="32" y="41" text-anchor="middle" font-family="Helvetica,Arial,sans-serif"
        font-size="15" font-weight="700" fill="#ffffff" letter-spacing="0.5">ANSSI</text>
</svg>
Loading