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 "$BACKEND" || { echo "ERROR: $BACKEND not found"; exit 1; }
|
||||||
docker buildx imagetools inspect "$FRONTEND" || { echo "ERROR: $FRONTEND 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 rollout status deployment/xpeditis-backend -n ${{ env.K8S_NAMESPACE }} --timeout=180s
|
||||||
|
|
||||||
kubectl set image deployment/xpeditis-frontend frontend="$FRONTEND" -n ${{ env.K8S_NAMESPACE }}
|
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
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
.env.local
|
.env.local
|
||||||
.env.development
|
.env.development
|
||||||
.env.test
|
.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/lib-storage": "^3.906.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.906.0",
|
"@aws-sdk/s3-request-presigner": "^3.906.0",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^10.2.10",
|
"@nestjs/common": "^11.2.6",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^10.2.10",
|
"@nestjs/core": "^11.2.6",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^10.2.10",
|
"@nestjs/platform-express": "^11.2.6",
|
||||||
"@nestjs/platform-socket.io": "^10.4.20",
|
"@nestjs/platform-socket.io": "^11.2.6",
|
||||||
"@nestjs/schedule": "^4.1.2",
|
"@nestjs/schedule": "^6.1.3",
|
||||||
"@nestjs/swagger": "^7.1.16",
|
"@nestjs/swagger": "^11.4.7",
|
||||||
"@nestjs/throttler": "^6.4.0",
|
"@nestjs/throttler": "^6.4.0",
|
||||||
"@nestjs/typeorm": "^10.0.1",
|
"@nestjs/typeorm": "^11.0.3",
|
||||||
"@nestjs/websockets": "^10.4.20",
|
"@nestjs/websockets": "^11.2.6",
|
||||||
"@sentry/node": "^10.19.0",
|
"@sentry/node": "^10.19.0",
|
||||||
"@sentry/profiling-node": "^10.19.0",
|
"@sentry/profiling-node": "^10.19.0",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/mjml": "^4.7.4",
|
|
||||||
"@types/nodemailer": "^7.0.2",
|
"@types/nodemailer": "^7.0.2",
|
||||||
"@types/opossum": "^8.1.9",
|
"@types/opossum": "^8.1.9",
|
||||||
"@types/pdfkit": "^0.17.3",
|
"@types/pdfkit": "^0.17.3",
|
||||||
@ -61,10 +60,10 @@
|
|||||||
"ioredis": "^5.8.1",
|
"ioredis": "^5.8.1",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"mjml": "^4.16.1",
|
"mjml": "^5.4.1",
|
||||||
"nestjs-i18n": "^10.6.5",
|
"nestjs-i18n": "^10.6.5",
|
||||||
"nestjs-pino": "^4.4.1",
|
"nestjs-pino": "^4.4.1",
|
||||||
"nodemailer": "^7.0.9",
|
"nodemailer": "^10.0.10",
|
||||||
"opossum": "^8.1.3",
|
"opossum": "^8.1.3",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@ -86,14 +85,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^10.0.0",
|
"@faker-js/faker": "^10.0.0",
|
||||||
"@nestjs/cli": "^10.2.1",
|
"@nestjs/cli": "^11.0.20",
|
||||||
"@nestjs/schematics": "^10.0.3",
|
"@nestjs/schematics": "^11.1.0",
|
||||||
"@nestjs/testing": "^10.2.10",
|
"@nestjs/testing": "^11.2.6",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/compression": "^1.8.1",
|
"@types/compression": "^1.8.1",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/mjml": "^5.0.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
const Stripe = require('stripe');
|
const Stripe = require('stripe');
|
||||||
|
|
||||||
const stripe = new Stripe(
|
if (!process.env.STRIPE_SECRET_KEY) {
|
||||||
process.env.STRIPE_SECRET_KEY ||
|
throw new Error('STRIPE_SECRET_KEY is required');
|
||||||
'sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr'
|
}
|
||||||
);
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||||
|
|
||||||
async function listPrices() {
|
async function listPrices() {
|
||||||
console.log('Fetching Stripe prices...\n');
|
console.log('Fetching Stripe prices...\n');
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule, JwtSignOptions } from '@nestjs/jwt';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
@ -36,7 +36,7 @@ import { AuditModule } from '../audit/audit.module';
|
|||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('JWT_SECRET'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: {
|
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 { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule, JwtSignOptions } from '@nestjs/jwt';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { NotificationsController } from '../controllers/notifications.controller';
|
import { NotificationsController } from '../controllers/notifications.controller';
|
||||||
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
import { NotificationsGateway } from '../gateways/notifications.gateway';
|
||||||
@ -25,7 +25,7 @@ import { NOTIFICATION_REPOSITORY } from '@domain/ports/out/notification.reposito
|
|||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
secret: configService.get<string>('JWT_SECRET'),
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
expiresIn: configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
|
expiresIn: configService.get<JwtSignOptions['expiresIn']>('JWT_ACCESS_EXPIRATION', '15m'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
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>`;
|
`<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. */
|
/** 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 year = new Date().getFullYear();
|
||||||
|
|
||||||
const mjml = `
|
const mjml = `
|
||||||
@ -156,7 +156,7 @@ export function renderEmail(layout: EmailLayout): string {
|
|||||||
</mj-body>
|
</mj-body>
|
||||||
</mjml>`;
|
</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).
|
// Doublon volontaire de lang/dir sur le contenu du <body> (voir en-tete).
|
||||||
return html
|
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 });
|
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.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') || '';
|
this.webhookSecret = this.configService.get<string>('STRIPE_WEBHOOK_SECRET') || '';
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ test-results
|
|||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
.env.local
|
.env.local
|
||||||
.env.development
|
.env.development
|
||||||
.env.test
|
.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é.
|
Créez votre compte en 2 minutes et lancez votre premier chiffrage maritime instantané.
|
||||||
C'est gratuit et sans engagement.
|
C'est gratuit et sans engagement.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<Link
|
||||||
href="/register"
|
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"
|
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
|
Créer mon compte gratuitement
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Back + Share */}
|
{/* Back + Share */}
|
||||||
|
|||||||
@ -79,9 +79,10 @@ function buildJsonLd(post: BlogPostMeta, slug: string) {
|
|||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { slug: string; locale: string };
|
params: Promise<{ slug: string; locale: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const post = await fetchPostMeta(params.slug);
|
const { slug } = await params;
|
||||||
|
const post = await fetchPostMeta(slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return { title: 'Blog — Xpeditis' };
|
return { title: 'Blog — Xpeditis' };
|
||||||
@ -117,7 +118,7 @@ export async function generateMetadata({
|
|||||||
images: coverImage ? [coverImage] : [],
|
images: coverImage ? [coverImage] : [],
|
||||||
},
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `/blog/${params.slug}`,
|
canonical: `/blog/${slug}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -125,10 +126,11 @@ export async function generateMetadata({
|
|||||||
export default async function BlogPostPage({
|
export default async function BlogPostPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { slug: string; locale: string };
|
params: Promise<{ slug: string; locale: string }>;
|
||||||
}) {
|
}) {
|
||||||
const post = await fetchPostMeta(params.slug);
|
const { slug } = await params;
|
||||||
const jsonLd = post ? buildJsonLd(post, params.slug) : null;
|
const post = await fetchPostMeta(slug);
|
||||||
|
const jsonLd = post ? buildJsonLd(post, slug) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -146,7 +148,7 @@ export default async function BlogPostPage({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<BlogPostContent slug={params.slug} />
|
<BlogPostContent slug={slug} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Link } from '@/i18n/navigation';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import { listApiKeys, createApiKey, revokeApiKey } from '@/lib/api/api-keys';
|
import { listApiKeys, createApiKey, revokeApiKey } from '@/lib/api/api-keys';
|
||||||
@ -323,12 +324,12 @@ export default function ApiKeysPage() {
|
|||||||
platinium: () => <strong>{t('noAccess.platinium')}</strong>,
|
platinium: () => <strong>{t('noAccess.platinium')}</strong>,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<Link
|
||||||
href="/pricing"
|
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"
|
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')}
|
{t('noAccess.viewPlans')}
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -383,12 +384,12 @@ export default function ApiKeysPage() {
|
|||||||
</code>
|
</code>
|
||||||
),
|
),
|
||||||
link: () => (
|
link: () => (
|
||||||
<a
|
<Link
|
||||||
href="/dashboard/docs?section=authentication"
|
href="/dashboard/docs?section=authentication"
|
||||||
className="font-medium underline underline-offset-2"
|
className="font-medium underline underline-offset-2"
|
||||||
>
|
>
|
||||||
{t('viewDocs')}
|
{t('viewDocs')}
|
||||||
</a>
|
</Link>
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { LandingHeader } from '@/components/layout/LandingHeader';
|
|||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { locale: string };
|
params: Promise<{ locale: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const t = await getTranslations({ locale: params.locale, namespace: 'marketing.docs' });
|
const t = await getTranslations({ locale: (await params).locale, namespace: 'marketing.docs' });
|
||||||
return {
|
return {
|
||||||
title: t('metadataTitle'),
|
title: t('metadataTitle'),
|
||||||
description: t('metadataDescription'),
|
description: t('metadataDescription'),
|
||||||
|
|||||||
@ -5,10 +5,10 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
|
||||||
|
|
||||||
// Standalone output for Docker (creates optimized server.js)
|
// Standalone output for Docker (creates optimized server.js)
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
outputFileTracingRoot: __dirname,
|
||||||
|
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
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",
|
"isomorphic-dompurify": "^3.16.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"next": "^14.2.35",
|
"next": "^15.5.26",
|
||||||
"next-intl": "^4.9.1",
|
"next-intl": "^4.9.1",
|
||||||
"react": "^18.2.0",
|
"react": "^19.3.0",
|
||||||
"react-day-picker": "^10.0.1",
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^19.3.0",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^5.0.0",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
"tailwind-merge": "^2.1.0",
|
"tailwind-merge": "^2.1.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"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",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.56.0",
|
"@playwright/test": "^1.56.0",
|
||||||
|
"@testing-library/dom": "^10.4.2",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@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/file-saver": "^2.0.7",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
"@types/react": "^18.2.45",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^19.3.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-next": "14.0.4",
|
"eslint-config-next": "^15.5.26",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"next": {
|
||||||
|
"postcss": "8.5.28"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
"@/utils/*": ["./src/utils/*"],
|
"@/utils/*": ["./src/utils/*"],
|
||||||
"@/pages/*": ["./src/pages/*"],
|
"@/pages/*": ["./src/pages/*"],
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
@ -204,8 +204,8 @@ services:
|
|||||||
RATE_LIMIT_MAX: "100"
|
RATE_LIMIT_MAX: "100"
|
||||||
|
|
||||||
# Stripe (Subscriptions & Payments)
|
# Stripe (Subscriptions & Payments)
|
||||||
STRIPE_SECRET_KEY: "sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr"
|
STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:?Set STRIPE_SECRET_KEY}"
|
||||||
STRIPE_WEBHOOK_SECRET: "whsec_0BLJx3J2LXITCq1cgp9ArzBuMG1W3QMj"
|
STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:?Set STRIPE_WEBHOOK_SECRET}"
|
||||||
|
|
||||||
# Stripe Price IDs (from Stripe Dashboard)
|
# Stripe Price IDs (from Stripe Dashboard)
|
||||||
STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD"
|
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
|
Les quatre workflows actifs se trouvent dans `.gitea/workflows/`. Les anciennes
|
||||||
copies `.github/workflows/` sont déplacées pour éviter une double exécution.
|
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
|
Les étapes de sécurité sont exposées directement dans chaque workflow pour que
|
||||||
pipeline ; aucun workflow réutilisable GitHub ni client `gh` n'est nécessaire.
|
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 |
|
| 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
|
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
|
vulnérabilités. Les douze erreurs Prettier du job backend ont été corrigées et
|
||||||
le lint sans correction automatique passe localement.
|
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"
|
RATE_LIMIT_MAX: "100"
|
||||||
|
|
||||||
# Stripe (Subscriptions & Payments)
|
# Stripe (Subscriptions & Payments)
|
||||||
STRIPE_SECRET_KEY: "sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr"
|
STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY:?Set STRIPE_SECRET_KEY}"
|
||||||
STRIPE_WEBHOOK_SECRET: "whsec_0BLJx3J2LXITCq1cgp9ArzBuMG1W3QMj"
|
STRIPE_WEBHOOK_SECRET: "${STRIPE_WEBHOOK_SECRET:?Set STRIPE_WEBHOOK_SECRET}"
|
||||||
|
|
||||||
# Stripe Price IDs (from Stripe Dashboard)
|
# Stripe Price IDs (from Stripe Dashboard)
|
||||||
STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD"
|
STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD"
|
||||||
|
|||||||
@ -27,3 +27,6 @@ RUN set -eux; \
|
|||||||
wal-g --version
|
wal-g --version
|
||||||
|
|
||||||
# Les scripts de sauvegarde / restauration sont montes depuis backup/.
|
# 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
|
# 60 s : laisse le temps aux requetes en cours et aux connexions
|
||||||
# WebSocket de se fermer proprement.
|
# WebSocket de se fermer proprement.
|
||||||
terminationGracePeriodSeconds: 60
|
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:
|
containers:
|
||||||
- name: backend
|
- name: backend
|
||||||
# Le tag est remplace au deploiement (kubectl set image).
|
# Le tag est remplace au deploiement (kubectl set image).
|
||||||
@ -122,12 +137,15 @@ spec:
|
|||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
capabilities:
|
capabilities:
|
||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
# NON active volontairement : le conteneur ecrit dans /app/logs et
|
readOnlyRootFilesystem: true
|
||||||
# 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
|
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- name: rates
|
||||||
|
mountPath: /app/src/infrastructure/storage/csv-storage/rates
|
||||||
|
- name: logs
|
||||||
|
mountPath: /app/logs
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
lifecycle:
|
lifecycle:
|
||||||
preStop:
|
preStop:
|
||||||
exec:
|
exec:
|
||||||
@ -135,6 +153,17 @@ spec:
|
|||||||
# processus ne commence a refuser des connexions.
|
# processus ne commence a refuser des connexions.
|
||||||
command: ["sh", "-c", "sleep 10"]
|
command: ["sh", "-c", "sleep 10"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: rates
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 1Gi
|
||||||
|
- name: logs
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 512Mi
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 256Mi
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|||||||
@ -73,4 +73,12 @@ spec:
|
|||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
capabilities:
|
capabilities:
|
||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
readOnlyRootFilesystem: false
|
readOnlyRootFilesystem: true
|
||||||
|
|
||||||
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 128Mi
|
||||||
|
|||||||
@ -140,14 +140,20 @@ spec:
|
|||||||
memory: 1Gi
|
memory: 1Gi
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
capabilities:
|
capabilities:
|
||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/loki
|
mountPath: /etc/loki
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /loki
|
mountPath: /loki
|
||||||
volumes:
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 128Mi
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: loki-config
|
name: loki-config
|
||||||
|
|||||||
@ -18,7 +18,7 @@ metadata:
|
|||||||
rules:
|
rules:
|
||||||
# Lecture seule, strictement ce qu'exige la decouverte de pods.
|
# Lecture seule, strictement ce qu'exige la decouverte de pods.
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: [nodes, nodes/proxy, services, endpoints, pods]
|
resources: [nodes, services, endpoints, pods]
|
||||||
verbs: [get, list, watch]
|
verbs: [get, list, watch]
|
||||||
---
|
---
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
|||||||
@ -317,9 +317,12 @@ spec:
|
|||||||
memory: 1536Mi
|
memory: 1536Mi
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
capabilities:
|
capabilities:
|
||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/prometheus/prometheus.yml
|
mountPath: /etc/prometheus/prometheus.yml
|
||||||
subPath: prometheus.yml
|
subPath: prometheus.yml
|
||||||
@ -329,6 +332,9 @@ spec:
|
|||||||
- name: data
|
- name: data
|
||||||
mountPath: /prometheus
|
mountPath: /prometheus
|
||||||
volumes:
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 128Mi
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: prometheus-config
|
name: prometheus-config
|
||||||
|
|||||||
@ -144,9 +144,12 @@ spec:
|
|||||||
memory: 512Mi
|
memory: 512Mi
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
capabilities:
|
capabilities:
|
||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
- name: provisioning-datasources
|
- name: provisioning-datasources
|
||||||
mountPath: /etc/grafana/provisioning/datasources
|
mountPath: /etc/grafana/provisioning/datasources
|
||||||
- name: provisioning-dashboards
|
- name: provisioning-dashboards
|
||||||
@ -156,6 +159,9 @@ spec:
|
|||||||
- name: data
|
- name: data
|
||||||
mountPath: /var/lib/grafana
|
mountPath: /var/lib/grafana
|
||||||
volumes:
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 128Mi
|
||||||
- name: provisioning-datasources
|
- name: provisioning-datasources
|
||||||
configMap:
|
configMap:
|
||||||
name: grafana-provisioning
|
name: grafana-provisioning
|
||||||
|
|||||||
@ -92,9 +92,11 @@ kubectl -n "$NAMESPACE" logs "job/${JOB_NAME}" --tail=50
|
|||||||
|
|
||||||
# --- 3. Deploiement ----------------------------------------------------------
|
# --- 3. Deploiement ----------------------------------------------------------
|
||||||
log "Mise a jour du backend"
|
log "Mise a jour du backend"
|
||||||
kubectl -n "$NAMESPACE" patch deploy xpeditis-backend --type=strategic -p \
|
# Apply the writable volumes and init container as well as both image versions.
|
||||||
"{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"xpeditis.com/config-checksum\":\"${CONFIG_SUM}\"}}}}}"
|
# One pod-template update keeps the CSV seed and application release aligned.
|
||||||
kubectl -n "$NAMESPACE" set image deploy/xpeditis-backend "backend=${BACKEND_IMAGE}"
|
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
|
kubectl -n "$NAMESPACE" rollout status deploy/xpeditis-backend --timeout=300s || rollback
|
||||||
|
|
||||||
log "Mise a jour du frontend"
|
log "Mise a jour du frontend"
|
||||||
|
|||||||
@ -13,8 +13,9 @@ for project in root backend frontend log-exporter; do
|
|||||||
done
|
done
|
||||||
source_dir=$(mktemp -d "$RUNNER_TEMP/security-source.XXXXXX")
|
source_dir=$(mktemp -d "$RUNNER_TEMP/security-source.XXXXXX")
|
||||||
git archive HEAD | tar -x -C "$source_dir"
|
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 \
|
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
|
failed=1
|
||||||
fi
|
fi
|
||||||
python3 - <<'PYTHON'
|
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