fix
Some checks failed
Dev CI / Security gate (push) Successful in 31s
Dev CI / Backend — Lint (push) Successful in 1m8s
Dev CI / Frontend — Lint & Type-check (push) Successful in 1m14s
Dev CI / Backend — Unit Tests (push) Successful in 1m10s
Dev CI / Frontend — Unit Tests (push) Successful in 46s
Dev CI / Notify Failure (push) Has been skipped
CD Preprod / Security gate (push) Successful in 29s
CD Preprod / Backend — Lint (push) Successful in 1m7s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 1m13s
CD Preprod / Backend — Unit Tests (push) Successful in 1m6s
CD Preprod / Frontend — Unit Tests (push) Successful in 44s
CD Preprod / Backend — Integration Tests (push) Failing after 37s
CD Preprod / Build Log Exporter (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, backend) (push) Has been skipped
CD Preprod / Build Backend (push) Has been skipped
CD Preprod / Build Frontend (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, frontend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (amd64, log-exporter) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, backend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, frontend) (push) Has been skipped
CD Preprod / Image security (${{ matrix.service }}, ${{ matrix.arch }}) (arm64, log-exporter) (push) Has been skipped
CD Preprod / Deploy to Preprod (push) Has been skipped
CD Preprod / Notify Success (push) Has been skipped
CD Preprod / Notify Failure (push) Has been skipped

This commit is contained in:
David 2026-09-24 21:32:47 +02:00
parent 5c59ef044b
commit 99e01f97fc
33 changed files with 5047 additions and 2869 deletions

View File

@ -126,7 +126,7 @@ jobs:
docker buildx imagetools inspect "$BACKEND" || { echo "ERROR: $BACKEND not found"; exit 1; }
docker buildx imagetools inspect "$FRONTEND" || { echo "ERROR: $FRONTEND not found"; exit 1; }
kubectl set image deployment/xpeditis-backend backend="$BACKEND" -n ${{ env.K8S_NAMESPACE }}
kubectl set image deployment/xpeditis-backend backend="$BACKEND" seed-rates="$BACKEND" -n ${{ env.K8S_NAMESPACE }}
kubectl rollout status deployment/xpeditis-backend -n ${{ env.K8S_NAMESPACE }} --timeout=180s
kubectl set image deployment/xpeditis-frontend frontend="$FRONTEND" -n ${{ env.K8S_NAMESPACE }}

22
.trivyignore.yaml Normal file
View File

@ -0,0 +1,22 @@
# Approved monitoring exceptions, reviewed 2026-09-24. No dependency/secret exclusions.
misconfigurations:
- id: AVD-KSV-0047
paths: [infra/prod/k8s/monitoring/03-prometheus.yaml]
expired_at: 2026-10-24
statement: Prometheus scrapes kubelet cAdvisor through the API server node proxy; retain until direct authenticated kubelet scraping is validated.
- id: AVD-KSV-0009
paths: [infra/prod/k8s/monitoring/04-node-exporter.yaml]
expired_at: 2026-10-24
statement: Node exporter observes host network metrics; isolated to the monitoring DaemonSet.
- id: AVD-KSV-0010
paths: [infra/prod/k8s/monitoring/04-node-exporter.yaml]
expired_at: 2026-10-24
statement: Node exporter observes host processes; runs as non-root with all capabilities dropped.
- id: AVD-KSV-0024
paths: [infra/prod/k8s/monitoring/04-node-exporter.yaml]
expired_at: 2026-10-24
statement: Existing host-network exporter listens on port 9101; access remains constrained by infrastructure firewall rules.
- id: AVD-KSV-0121
paths: [infra/prod/k8s/monitoring/04-node-exporter.yaml]
expired_at: 2026-10-24
statement: Host proc/sys/root mounts provide disk and system metrics and are read-only; required for disk-full alerts.

View File

@ -24,6 +24,7 @@ e2e
# Environment files
.env
.env.*
.env.local
.env.development
.env.test

File diff suppressed because it is too large Load Diff

View File

@ -29,22 +29,21 @@
"@aws-sdk/lib-storage": "^3.906.0",
"@aws-sdk/s3-request-presigner": "^3.906.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^10.2.10",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.10",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.2.10",
"@nestjs/platform-socket.io": "^10.4.20",
"@nestjs/schedule": "^4.1.2",
"@nestjs/swagger": "^7.1.16",
"@nestjs/common": "^11.2.6",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.2.6",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.2.6",
"@nestjs/platform-socket.io": "^11.2.6",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^11.4.7",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^10.0.1",
"@nestjs/websockets": "^10.4.20",
"@nestjs/typeorm": "^11.0.3",
"@nestjs/websockets": "^11.2.6",
"@sentry/node": "^10.19.0",
"@sentry/profiling-node": "^10.19.0",
"@types/leaflet": "^1.9.21",
"@types/mjml": "^4.7.4",
"@types/nodemailer": "^7.0.2",
"@types/opossum": "^8.1.9",
"@types/pdfkit": "^0.17.3",
@ -61,10 +60,10 @@
"ioredis": "^5.8.1",
"joi": "^17.11.0",
"leaflet": "^1.9.4",
"mjml": "^4.16.1",
"mjml": "^5.4.1",
"nestjs-i18n": "^10.6.5",
"nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9",
"nodemailer": "^10.0.10",
"opossum": "^8.1.3",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
@ -86,14 +85,15 @@
},
"devDependencies": {
"@faker-js/faker": "^10.0.0",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.10",
"@nestjs/cli": "^11.0.20",
"@nestjs/schematics": "^11.1.0",
"@nestjs/testing": "^11.2.6",
"@types/bcrypt": "^5.0.2",
"@types/compression": "^1.8.1",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^4.17.21",
"@types/express": "^5.0.6",
"@types/jest": "^29.5.11",
"@types/mjml": "^5.0.0",
"@types/multer": "^2.0.0",
"@types/node": "^20.10.5",
"@types/passport-google-oauth20": "^2.0.14",

View File

@ -5,10 +5,10 @@
const Stripe = require('stripe');
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY ||
'sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr'
);
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is required');
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
async function listPrices() {
console.log('Fetching Stripe prices...\n');

View File

@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { JwtModule, JwtSignOptions } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@ -36,7 +36,7 @@ import { AuditModule } from '../audit/audit.module';
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
expiresIn: configService.get<JwtSignOptions['expiresIn']>('JWT_ACCESS_EXPIRATION', '15m'),
},
}),
}),

View File

@ -7,7 +7,7 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { JwtModule, JwtSignOptions } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { NotificationsController } from '../controllers/notifications.controller';
import { NotificationsGateway } from '../gateways/notifications.gateway';
@ -25,7 +25,7 @@ import { NOTIFICATION_REPOSITORY } from '@domain/ports/out/notification.reposito
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
expiresIn: configService.get<JwtSignOptions['expiresIn']>('JWT_ACCESS_EXPIRATION', '15m'),
},
}),
inject: [ConfigService],

View File

@ -95,7 +95,7 @@ const label = (text: string, align: 'left' | 'center' | 'right' = 'left', paddin
`<mj-text align="${align}" font-size="11px" line-height="16px" font-weight="700" letter-spacing="1.2px" text-transform="uppercase" color="${C.muted}" padding="${padding}">${esc(text)}</mj-text>`;
/** Compile le gabarit complet en HTML pret a l'envoi. */
export function renderEmail(layout: EmailLayout): string {
export async function renderEmail(layout: EmailLayout): Promise<string> {
const year = new Date().getFullYear();
const mjml = `
@ -156,7 +156,7 @@ export function renderEmail(layout: EmailLayout): string {
</mj-body>
</mjml>`;
const { html } = mjml2html(mjml, { validationLevel });
const { html } = await mjml2html(mjml, { validationLevel });
// Doublon volontaire de lang/dir sur le contenu du <body> (voir en-tete).
return html

View File

@ -93,7 +93,7 @@ export class EmailTemplates {
);
}
private render(layout: Omit<EmailLayout, 'appUrl' | 'logoUrl'>): string {
private render(layout: Omit<EmailLayout, 'appUrl' | 'logoUrl'>): Promise<string> {
return renderEmail({ ...layout, appUrl: this.appUrl, logoUrl: this.logoUrl });
}

View File

@ -35,7 +35,7 @@ export class StripeAdapter implements StripePort {
this.logger.warn('STRIPE_SECRET_KEY not configured - Stripe features will be disabled');
}
this.stripe = new Stripe(apiKey || 'sk_test_placeholder');
this.stripe = new Stripe(apiKey || 'stripe-disabled');
this.webhookSecret = this.configService.get<string>('STRIPE_WEBHOOK_SECRET') || '';

View File

@ -27,6 +27,7 @@ test-results
# Environment files
.env
.env.*
.env.local
.env.development
.env.test

View File

@ -317,12 +317,12 @@ export default function BlogPostContent({ slug }: { slug: string }) {
Créez votre compte en 2 minutes et lancez votre premier chiffrage maritime instantané.
C&apos;est gratuit et sans engagement.
</p>
<a
<Link
href="/register"
className="inline-flex items-center gap-2 px-6 py-3 bg-brand-turquoise text-white rounded-xl font-semibold hover:bg-brand-turquoise/90 transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5"
>
Créer mon compte gratuitement
</a>
</Link>
</div>
{/* Back + Share */}

View File

@ -79,9 +79,10 @@ function buildJsonLd(post: BlogPostMeta, slug: string) {
export async function generateMetadata({
params,
}: {
params: { slug: string; locale: string };
params: Promise<{ slug: string; locale: string }>;
}): Promise<Metadata> {
const post = await fetchPostMeta(params.slug);
const { slug } = await params;
const post = await fetchPostMeta(slug);
if (!post) {
return { title: 'Blog — Xpeditis' };
@ -117,7 +118,7 @@ export async function generateMetadata({
images: coverImage ? [coverImage] : [],
},
alternates: {
canonical: `/blog/${params.slug}`,
canonical: `/blog/${slug}`,
},
};
}
@ -125,10 +126,11 @@ export async function generateMetadata({
export default async function BlogPostPage({
params,
}: {
params: { slug: string; locale: string };
params: Promise<{ slug: string; locale: string }>;
}) {
const post = await fetchPostMeta(params.slug);
const jsonLd = post ? buildJsonLd(post, params.slug) : null;
const { slug } = await params;
const post = await fetchPostMeta(slug);
const jsonLd = post ? buildJsonLd(post, slug) : null;
return (
<>
@ -146,7 +148,7 @@ export default async function BlogPostPage({
)}
</>
)}
<BlogPostContent slug={params.slug} />
<BlogPostContent slug={slug} />
</>
);
}

View File

@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
import { Link } from '@/i18n/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslations, useLocale } from 'next-intl';
import { listApiKeys, createApiKey, revokeApiKey } from '@/lib/api/api-keys';
@ -323,12 +324,12 @@ export default function ApiKeysPage() {
platinium: () => <strong>{t('noAccess.platinium')}</strong>,
})}
</p>
<a
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-brand-navy hover:bg-brand-navy/90 text-white text-sm font-medium rounded-xl transition-colors"
>
{t('noAccess.viewPlans')}
</a>
</Link>
</div>
);
}
@ -383,12 +384,12 @@ export default function ApiKeysPage() {
</code>
),
link: () => (
<a
<Link
href="/dashboard/docs?section=authentication"
className="font-medium underline underline-offset-2"
>
{t('viewDocs')}
</a>
</Link>
),
})}
</p>

View File

@ -5,9 +5,9 @@ import { LandingHeader } from '@/components/layout/LandingHeader';
export async function generateMetadata({
params,
}: {
params: { locale: string };
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const t = await getTranslations({ locale: params.locale, namespace: 'marketing.docs' });
const t = await getTranslations({ locale: (await params).locale, namespace: 'marketing.docs' });
return {
title: t('metadataTitle'),
description: t('metadataDescription'),

View File

@ -5,10 +5,10 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
// Standalone output for Docker (creates optimized server.js)
output: 'standalone',
outputFileTracingRoot: __dirname,
experimental: {
serverActions: {

File diff suppressed because it is too large Load Diff

View File

@ -48,36 +48,42 @@
"isomorphic-dompurify": "^3.16.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.294.0",
"next": "^14.2.35",
"next": "^15.5.26",
"next-intl": "^4.9.1",
"react": "^18.2.0",
"react": "^19.3.0",
"react-day-picker": "^10.0.1",
"react-dom": "^18.2.0",
"react-dom": "^19.3.0",
"react-hook-form": "^7.64.0",
"react-leaflet": "^4.2.1",
"react-leaflet": "^5.0.0",
"recharts": "^3.2.1",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
"xlsx": "^0.18.5",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^3.25.76",
"zustand": "^5.0.8"
},
"devDependencies": {
"@playwright/test": "^1.56.0",
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react": "^16.3.3",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "14.0.4",
"eslint-config-next": "^15.5.26",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.3.3"
},
"overrides": {
"next": {
"postcss": "8.5.28"
}
}
}

View File

@ -29,7 +29,8 @@
"@/utils/*": ["./src/utils/*"],
"@/pages/*": ["./src/pages/*"],
"@/*": ["./src/*"]
}
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": [

View File

@ -204,8 +204,8 @@ services:
RATE_LIMIT_MAX: "100"
# Stripe (Subscriptions & Payments)
STRIPE_SECRET_KEY: "sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr"
STRIPE_WEBHOOK_SECRET: "whsec_0BLJx3J2LXITCq1cgp9ArzBuMG1W3QMj"
STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:?Set STRIPE_SECRET_KEY}"
STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:?Set STRIPE_WEBHOOK_SECRET}"
# Stripe Price IDs (from Stripe Dashboard)
STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD"

View File

@ -8,8 +8,9 @@ avec des jobs exécutés dans des conteneurs Linux AMD64 ou ARM64.
Les quatre workflows actifs se trouvent dans `.gitea/workflows/`. Les anciennes
copies `.github/workflows/` sont déplacées pour éviter une double exécution.
L'action composite `.gitea/actions/security` est appelée directement par chaque
pipeline ; aucun workflow réutilisable GitHub ni client `gh` n'est nécessaire.
Les étapes de sécurité sont exposées directement dans chaque workflow pour que
Gitea affiche leurs journaux ; les scripts restent communs. Aucun workflow
réutilisable GitHub ni client `gh` n'est nécessaire.
| Pipeline | Déclenchement | Contrôles |
| --- | --- | --- |
@ -189,3 +190,47 @@ alertes d'infrastructure. Ces nombres décrivent ce run, pas un audit futur.
Les seuils restent bloquants ; cette correction d'affichage ne corrige pas les
vulnérabilités. Les douze erreurs Prettier du job backend ont été corrigées et
le lint sans correction automatique passe localement.
## Correction des audits du 24 septembre 2026
Les dépendances ont été actualisées sans suppression de l'audit : Next 15.5.26,
React 19, Nest 11.2.6, MJML 5 (rendu attendu de façon asynchrone) et Nodemailer 10.
PostCSS 8.5.28 est imposé uniquement sous Next pour corriger sa dépendance épinglée.
Les modules Tiptap sont alignés. SheetJS utilise la distribution officielle 0.20.3,
avec intégrité dans le lockfile, en conservant l'API des exports Excel.
Les audits actuels passent au seuil HIGH/CRITICAL : zéro vulnérabilité frontend,
trois alertes modérées backend (csv-parse, uuid et la dépendance d'exceljs).
Ces résultats sont datés et ne garantissent pas l'absence de nouvelles alertes.
Les clés Stripe embarquées dans le script de configuration et les deux stacks
préprod sont retirées. Renseigner `STRIPE_SECRET_KEY` et `STRIPE_WEBHOOK_SECRET`
dans Portainer avant de redéployer ces stacks, puis renouveler les valeurs qui
étaient dans Git. Le placeholder de l'adaptateur n'est pas une véritable clé.
Les exceptions approuvées sont dans `.trivyignore.yaml`, chargées explicitement
par l'audit source : quatre règles pour node-exporter et une pour le proxy
cAdvisor de Prometheus, uniquement sur leurs chemins précis, jusqu'au
**24 octobre 2026**. Aucune exclusion npm ou de secret. Le test réel Trivy vérifie
qu'une copie dans un autre fichier et une exception expirée échouent toujours.
Le droit nodes/proxy inutile de Promtail est retiré.
Les racines des conteneurs backend, migration, Loki, Prometheus et Grafana sont
en lecture seule, avec volumes d'écriture dédiés. Le backend initialise son
volume CSV à partir de la même image que l'application ; le script Kubernetes
applique le manifeste complet pour prendre en charge les déploiements existants.
Les données temporaires des volumes emptyDir restent éphémères, comme les anciens
fichiers écrits dans les pods. PostgreSQL démarre en UID 999 : le provisionnement
du nœud attribue déjà les volumes et certificats à cet utilisateur. L'image WAL-G
configurée reste AMD64 ; son archive ARM64 n'est pas publiée à cette URL.
Validation locale : lint backend/frontend, types frontend, 27 tests de scripts
plus un test d'intégration Trivy (trois scénarios), builds Docker Node 22 backend
et frontend, rendu email et copie CSV sur système en lecture seule, démarrage
PostgreSQL AMD64 sans root et scan source Trivy. Les tests applicatifs couvrent
les exports, emails, contrôles d'accès et TLS. Aucun déploiement réel ni nouvelle
exécution Gitea n'est effectué par cette correction locale.
Sources : [migration Next 15](https://nextjs.org/docs/app/guides/upgrading/version-15),
[distribution SheetJS](https://docs.sheetjs.com/docs/getting-started/installation/nodejs/),
[exceptions Trivy](https://trivy.dev/docs/dev/configuration/filtering/).

View File

@ -222,8 +222,8 @@ services:
RATE_LIMIT_MAX: "100"
# Stripe (Subscriptions & Payments)
STRIPE_SECRET_KEY: "sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr"
STRIPE_WEBHOOK_SECRET: "whsec_0BLJx3J2LXITCq1cgp9ArzBuMG1W3QMj"
STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:?Set STRIPE_SECRET_KEY}"
STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:?Set STRIPE_WEBHOOK_SECRET}"
# Stripe Price IDs (from Stripe Dashboard)
STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD"

View File

@ -27,3 +27,6 @@ RUN set -eux; \
wal-g --version
# Les scripts de sauvegarde / restauration sont montes depuis backup/.
# Host provisioning assigns pgdata and certificates to postgres (UID 999).
USER postgres

View File

@ -57,6 +57,21 @@ spec:
# 60 s : laisse le temps aux requetes en cours et aux connexions
# WebSocket de se fermer proprement.
terminationGracePeriodSeconds: 60
initContainers:
- name: seed-rates
image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:latest
command: [sh, -ec, 'cp -R /app/src/infrastructure/storage/csv-storage/rates/. /rates/']
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
volumeMounts:
- name: rates
mountPath: /rates
containers:
- name: backend
# Le tag est remplace au deploiement (kubectl set image).
@ -122,12 +137,15 @@ spec:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# NON active volontairement : le conteneur ecrit dans /app/logs et
# dans /app/src/infrastructure/storage/csv-storage/rates (chemin
# resolu au runtime par le chargeur de grilles CSV). Monter des
# emptyDir par-dessus masquerait les fichiers livres dans l'image.
readOnlyRootFilesystem: false
readOnlyRootFilesystem: true
volumeMounts:
- name: rates
mountPath: /app/src/infrastructure/storage/csv-storage/rates
- name: logs
mountPath: /app/logs
- name: tmp
mountPath: /tmp
lifecycle:
preStop:
exec:
@ -135,6 +153,17 @@ spec:
# processus ne commence a refuser des connexions.
command: ["sh", "-c", "sleep 10"]
volumes:
- name: rates
emptyDir:
sizeLimit: 1Gi
- name: logs
emptyDir:
sizeLimit: 512Mi
- name: tmp
emptyDir:
sizeLimit: 256Mi
---
apiVersion: v1
kind: Service

View File

@ -73,4 +73,12 @@ spec:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: false
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi

View File

@ -140,14 +140,20 @@ spec:
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: config
mountPath: /etc/loki
- name: data
mountPath: /loki
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
- name: config
configMap:
name: loki-config

View File

@ -18,7 +18,7 @@ metadata:
rules:
# Lecture seule, strictement ce qu'exige la decouverte de pods.
- apiGroups: [""]
resources: [nodes, nodes/proxy, services, endpoints, pods]
resources: [nodes, services, endpoints, pods]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1

View File

@ -317,9 +317,12 @@ spec:
memory: 1536Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: config
mountPath: /etc/prometheus/prometheus.yml
subPath: prometheus.yml
@ -329,6 +332,9 @@ spec:
- name: data
mountPath: /prometheus
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
- name: config
configMap:
name: prometheus-config

View File

@ -144,9 +144,12 @@ spec:
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: provisioning-datasources
mountPath: /etc/grafana/provisioning/datasources
- name: provisioning-dashboards
@ -156,6 +159,9 @@ spec:
- name: data
mountPath: /var/lib/grafana
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
- name: provisioning-datasources
configMap:
name: grafana-provisioning

View File

@ -92,9 +92,11 @@ kubectl -n "$NAMESPACE" logs "job/${JOB_NAME}" --tail=50
# --- 3. Deploiement ----------------------------------------------------------
log "Mise a jour du backend"
kubectl -n "$NAMESPACE" patch deploy xpeditis-backend --type=strategic -p \
"{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"xpeditis.com/config-checksum\":\"${CONFIG_SUM}\"}}}}}"
kubectl -n "$NAMESPACE" set image deploy/xpeditis-backend "backend=${BACKEND_IMAGE}"
# Apply the writable volumes and init container as well as both image versions.
# One pod-template update keeps the CSV seed and application release aligned.
sed -e "s|rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:latest|${BACKEND_IMAGE}|g" \
-e "s|PLACEHOLDER|${CONFIG_SUM}|g" "${K8S_DIR}/base/04-backend.yaml" \
| kubectl -n "$NAMESPACE" apply -f -
kubectl -n "$NAMESPACE" rollout status deploy/xpeditis-backend --timeout=300s || rollback
log "Mise a jour du frontend"

View File

@ -13,8 +13,9 @@ for project in root backend frontend log-exporter; do
done
source_dir=$(mktemp -d "$RUNNER_TEMP/security-source.XXXXXX")
git archive HEAD | tar -x -C "$source_dir"
echo "Scoped monitoring exceptions: .trivyignore.yaml (see expiration dates in that file)"
if ! trivy fs --scanners secret,misconfig --severity HIGH,CRITICAL --exit-code 1 \
--timeout 10m --format json --output "$RUNNER_TEMP/source-security-raw.json" "$source_dir"; then
--ignorefile "$source_dir/.trivyignore.yaml" --timeout 10m --format json --output "$RUNNER_TEMP/source-security-raw.json" "$source_dir"; then
failed=1
fi
python3 - <<'PYTHON'

View File

@ -0,0 +1,42 @@
"""Exercise the real scanner: monitoring exceptions must be scoped and expire."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
TRIVY = os.environ.get('TRIVY_BIN') or shutil.which('trivy')
@unittest.skipUnless(TRIVY, 'Trivy is required for scanner policy integration checks')
class TrivyPolicy(unittest.TestCase):
def test_exception_does_not_cover_other_paths_or_survive_expiration(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
relative = Path('infra/prod/k8s/monitoring/04-node-exporter.yaml')
approved = root / relative
approved.parent.mkdir(parents=True)
shutil.copyfile(ROOT / relative, approved)
policy = root / '.trivyignore.yaml'
policy.write_text((ROOT / '.trivyignore.yaml').read_text())
def scan():
result = subprocess.run(
[TRIVY, 'config', '--skip-check-update', '--severity', 'HIGH,CRITICAL',
'--ignorefile', str(policy), '--exit-code', '1', '--format', 'json',
str(root)], capture_output=True, text=True, timeout=60)
# A scanner crash or invalid report cannot count as a successful rejection.
import json
report = json.loads(result.stdout)
self.assertIn('Results', report)
return result.returncode
self.assertEqual(scan(), 0, 'Approved monitoring policy should pass before expiry')
other = root / 'unapproved.yaml'
approved.rename(other)
self.assertEqual(scan(), 1, 'The same access outside the approved path must fail')
other.rename(approved)
policy.write_text(policy.read_text().replace('2026-10-24', '2000-01-01'))
self.assertEqual(scan(), 1, 'Expired exceptions must fail closed')