feat: clean install (Gitea en dur, scan off) + durcissement sécurité entreprise
Gitea - lien « Code source » hardcodé (REPO_SOURCE_URL) vers le dépôt, toujours visible - retrait complet du réglage configurable (api.ts, route+store settings, SettingsView, i18n, help, tests) Découverte des dépôts - aucune racine de scan par défaut → pas de scan au premier démarrage (clean install) - corrige la découverte des dépôts à l'INTÉRIEUR d'une racine qui est elle-même un repo (depth 0 = conteneur de scan, on descend ; depth > 0 = feuille) Sécurité « enterprise-deployable » - en-têtes HTTP durcis (CSP, X-Frame-Options, nosniff, Referrer-Policy, HSTS conditionnel, no-store API), header Server retiré - permissions DB 0o600 / dossier de données 0o700 ; error handler sanitisé ; garde Content-Type sur les mutations - chiffrement au repos AES-256-GCM des secrets (server_secret, clé privée VAPID) via SecretBox - journal d'audit (migration #7) + endpoint /audit-logs + RGPD export/effacement + UI Réglages - SECURITY.md, docs/ENTERPRISE_DEPLOYMENT.md, sections README EN/FR, SBOM CycloneDX en CI CI - pack-smoke packe depuis le contexte du package (cd packages/server) au lieu de « npm pack -w » : corrige l'embarquement de @arboretum/shared dans le tarball Purge des données personnelles du dépôt public - suppression de spikes/s4-discovery/result.json, reformulation du VERDICT - chemin de test générique, anonymisation des 5 fixtures de dialogues
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Routes Réglages : GET expose la config non sensible (jamais les secrets), PATCH n'écrit que
|
||||
// l'allow-list et valide l'URL Gitea (http/https only, anti-XSS).
|
||||
// l'allow-list (découverte des dépôts) et ignore toute clé hors allow-list.
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -65,7 +65,7 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
describe('GET /api/v1/settings', () => {
|
||||
it('renvoie la config serveur non sensible et giteaUrl null par défaut', async () => {
|
||||
it('renvoie la config serveur non sensible et aucune racine de scan par défaut', async () => {
|
||||
const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json() as SettingsResponse;
|
||||
@@ -74,10 +74,8 @@ describe('GET /api/v1/settings', () => {
|
||||
expect(body.server.bind).toBe('127.0.0.1');
|
||||
expect(body.server.allowedOrigins).toEqual(['https://host.tailnet.ts.net']);
|
||||
expect(body.server.vapidPublicKey).toBeTruthy(); // clé publique = sûre à exposer
|
||||
expect(body.settings.giteaUrl).toBeNull();
|
||||
// défauts de découverte : home (1 racine) + intervalle 5 min
|
||||
expect(Array.isArray(body.settings.scanRoots)).toBe(true);
|
||||
expect(body.settings.scanRoots).toHaveLength(1);
|
||||
// défauts de découverte : AUCUNE racine (clean install → pas de scan) + intervalle 5 min
|
||||
expect(body.settings.scanRoots).toEqual([]);
|
||||
expect(body.settings.scanIntervalMin).toBe(5);
|
||||
});
|
||||
|
||||
@@ -93,29 +91,7 @@ describe('GET /api/v1/settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/settings', () => {
|
||||
it('enregistre une URL Gitea valide et la renvoie', async () => {
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { giteaUrl: 'https://git.lidge.fr' } });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect((res.json() as SettingsResponse).settings.giteaUrl).toBe('https://git.lidge.fr/');
|
||||
// persisté
|
||||
const get = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
|
||||
expect((get.json() as SettingsResponse).settings.giteaUrl).toBe('https://git.lidge.fr/');
|
||||
});
|
||||
|
||||
it('efface l’URL avec null ou chaîne vide', async () => {
|
||||
await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { giteaUrl: 'https://git.lidge.fr' } });
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { giteaUrl: null } });
|
||||
expect((res.json() as SettingsResponse).settings.giteaUrl).toBeNull();
|
||||
});
|
||||
|
||||
it('rejette une URL non http(s) — anti-XSS (400)', async () => {
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { giteaUrl: 'javascript:alert(1)' } });
|
||||
expect(res.statusCode).toBe(400);
|
||||
const notUrl = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { giteaUrl: 'pas une url' } });
|
||||
expect(notUrl.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/settings — sécurité', () => {
|
||||
it('ignore toute clé hors allow-list (ne touche pas aux secrets)', async () => {
|
||||
const before = getSetting(db, 'server_secret');
|
||||
await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { server_secret: 'pwned', vapid_private: 'pwned' } });
|
||||
@@ -124,7 +100,7 @@ describe('PATCH /api/v1/settings', () => {
|
||||
});
|
||||
|
||||
it('sans authentification → 401', async () => {
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', payload: { giteaUrl: 'https://x.example' } });
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', payload: { scanRoots: ['/home/u/work'] } });
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user