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
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:
parent
5c59ef044b
commit
99e01f97fc
@ -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
22
.trivyignore.yaml
Normal 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.
|
||||
@ -24,6 +24,7 @@ e2e
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
|
||||
5186
apps/backend/package-lock.json
generated
5186
apps/backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -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') || '';
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ test-results
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
|
||||
@ -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'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 */}
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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: {
|
||||
|
||||
2392
apps/frontend/package-lock.json
generated
2392
apps/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,8 @@
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/pages/*": ["./src/pages/*"],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": [
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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/).
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'
|
||||
|
||||
42
scripts/ci/test_trivy_policy.py
Normal file
42
scripts/ci/test_trivy_policy.py
Normal 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')
|
||||
Loading…
Reference in New Issue
Block a user