From 5c59ef044bddab5476d66c47146fa00f124d70cd Mon Sep 17 00:00:00 2001 From: David Date: Wed, 23 Sep 2026 22:56:46 +0200 Subject: [PATCH] fix --- .gitea/actions/security/action.yml | 10 ++++- .gitea/workflows/cd-main.yml | 27 ++++++++++- .gitea/workflows/cd-preprod.yml | 27 ++++++++++- .gitea/workflows/ci.yml | 27 ++++++++++- .gitea/workflows/pr-checks.yml | 27 ++++++++++- .../controllers/users.security.spec.ts | 8 ++-- .../src/application/dto/delete-account.dto.ts | 2 +- .../application/services/gdpr.service.spec.ts | 4 +- .../src/application/services/gdpr.service.ts | 12 +++-- .../services/retention.service.spec.ts | 2 +- .../domain/ports/out/shipment-counter.port.ts | 5 +-- .../typeorm-csv-rate-config.repository.ts | 5 +-- docs/CI-CD-SECURITY.md | 19 +++++++- scripts/ci/summarize-security.py | 45 +++++++++++++++++++ scripts/ci/test_security_summary.py | 45 +++++++++++++++++++ 15 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 scripts/ci/summarize-security.py create mode 100644 scripts/ci/test_security_summary.py diff --git a/.gitea/actions/security/action.yml b/.gitea/actions/security/action.yml index 8b9703f..7c5103b 100644 --- a/.gitea/actions/security/action.yml +++ b/.gitea/actions/security/action.yml @@ -4,7 +4,11 @@ runs: using: composite steps: - uses: ./.gitea/actions/setup-node - - uses: ./.gitea/actions/setup-trivy + - name: Install Trivy + shell: bash + run: | + trivy_bin=$(bash scripts/ci/install-tool.sh trivy) + "$trivy_bin" --version - name: Validate workflows and deployment checks shell: bash run: | @@ -13,6 +17,10 @@ runs: - name: Audit dependencies, secrets and infrastructure shell: bash run: bash scripts/ci/security-audit.sh + - name: Show security results + if: always() + shell: bash + run: python3 scripts/ci/summarize-security.py - name: Save security reports on Gitea if: always() uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol diff --git a/.gitea/workflows/cd-main.yml b/.gitea/workflows/cd-main.yml index 8f3b941..8f8bca4 100644 --- a/.gitea/workflows/cd-main.yml +++ b/.gitea/workflows/cd-main.yml @@ -40,7 +40,32 @@ jobs: - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - uses: ./.gitea/actions/security + - uses: ./.gitea/actions/setup-node + - name: Install Trivy + shell: bash + run: | + trivy_bin=$(bash scripts/ci/install-tool.sh trivy) + "$trivy_bin" --version + - name: Validate workflows and deployment checks + shell: bash + run: | + actionlint_bin=$(bash scripts/ci/install-tool.sh actionlint) + ACTIONLINT_BIN="$actionlint_bin" bash scripts/ci/validate-workflows.sh + - name: Audit dependencies, secrets and infrastructure + shell: bash + run: bash scripts/ci/security-audit.sh + - name: Show security results + if: always() + shell: bash + run: python3 scripts/ci/summarize-security.py + - name: Save security reports on Gitea + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: security-reports + path: ${{ runner.temp }}/security-reports/*.json + retention-days: 7 + if-no-files-found: error # ═══ 1. Qualité ══════════════════════════════════════════════════════════ backend-quality: diff --git a/.gitea/workflows/cd-preprod.yml b/.gitea/workflows/cd-preprod.yml index 1adc7bc..6c78a22 100644 --- a/.gitea/workflows/cd-preprod.yml +++ b/.gitea/workflows/cd-preprod.yml @@ -28,7 +28,32 @@ jobs: - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - uses: ./.gitea/actions/security + - uses: ./.gitea/actions/setup-node + - name: Install Trivy + shell: bash + run: | + trivy_bin=$(bash scripts/ci/install-tool.sh trivy) + "$trivy_bin" --version + - name: Validate workflows and deployment checks + shell: bash + run: | + actionlint_bin=$(bash scripts/ci/install-tool.sh actionlint) + ACTIONLINT_BIN="$actionlint_bin" bash scripts/ci/validate-workflows.sh + - name: Audit dependencies, secrets and infrastructure + shell: bash + run: bash scripts/ci/security-audit.sh + - name: Show security results + if: always() + shell: bash + run: python3 scripts/ci/summarize-security.py + - name: Save security reports on Gitea + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: security-reports + path: ${{ runner.temp }}/security-reports/*.json + retention-days: 7 + if-no-files-found: error # ── 1. Lint ───────────────────────────────────────────────────────── backend-quality: diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 61c56cb..f6f739f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,7 +14,32 @@ jobs: - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - uses: ./.gitea/actions/security + - uses: ./.gitea/actions/setup-node + - name: Install Trivy + shell: bash + run: | + trivy_bin=$(bash scripts/ci/install-tool.sh trivy) + "$trivy_bin" --version + - name: Validate workflows and deployment checks + shell: bash + run: | + actionlint_bin=$(bash scripts/ci/install-tool.sh actionlint) + ACTIONLINT_BIN="$actionlint_bin" bash scripts/ci/validate-workflows.sh + - name: Audit dependencies, secrets and infrastructure + shell: bash + run: bash scripts/ci/security-audit.sh + - name: Show security results + if: always() + shell: bash + run: python3 scripts/ci/summarize-security.py + - name: Save security reports on Gitea + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: security-reports + path: ${{ runner.temp }}/security-reports/*.json + retention-days: 7 + if-no-files-found: error backend-quality: name: Backend — Lint diff --git a/.gitea/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml index 712122c..a5ca2b3 100644 --- a/.gitea/workflows/pr-checks.yml +++ b/.gitea/workflows/pr-checks.yml @@ -16,7 +16,32 @@ jobs: - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - uses: ./.gitea/actions/security + - uses: ./.gitea/actions/setup-node + - name: Install Trivy + shell: bash + run: | + trivy_bin=$(bash scripts/ci/install-tool.sh trivy) + "$trivy_bin" --version + - name: Validate workflows and deployment checks + shell: bash + run: | + actionlint_bin=$(bash scripts/ci/install-tool.sh actionlint) + ACTIONLINT_BIN="$actionlint_bin" bash scripts/ci/validate-workflows.sh + - name: Audit dependencies, secrets and infrastructure + shell: bash + run: bash scripts/ci/security-audit.sh + - name: Show security results + if: always() + shell: bash + run: python3 scripts/ci/summarize-security.py + - name: Save security reports on Gitea + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: security-reports + path: ${{ runner.temp }}/security-reports/*.json + retention-days: 7 + if-no-files-found: error backend-quality: name: Backend — Lint diff --git a/apps/backend/src/application/controllers/users.security.spec.ts b/apps/backend/src/application/controllers/users.security.spec.ts index 2848fd7..5c87085 100644 --- a/apps/backend/src/application/controllers/users.security.spec.ts +++ b/apps/backend/src/application/controllers/users.security.spec.ts @@ -70,10 +70,10 @@ describe('administrator target protection', () => { const controller = new UsersController( { findById: jest.fn(async () => user), save } as unknown as UserRepository, {} as SubscriptionService, - {} as never, - { assertKeepsAnActiveAdmin: jest.fn() } as never, - { sendUserInvitation: jest.fn().mockResolvedValue(undefined) } as never, - { findById: jest.fn().mockResolvedValue(null) } as never + {} as never, + { assertKeepsAnActiveAdmin: jest.fn() } as never, + { sendUserInvitation: jest.fn().mockResolvedValue(undefined) } as never, + { findById: jest.fn().mockResolvedValue(null) } as never ); const result = controller.updateUser(user.id, { firstName: 'Changed' }, actor); if (role === UserRole.ADMIN) { diff --git a/apps/backend/src/application/dto/delete-account.dto.ts b/apps/backend/src/application/dto/delete-account.dto.ts index 07584ff..1c4b0b0 100644 --- a/apps/backend/src/application/dto/delete-account.dto.ts +++ b/apps/backend/src/application/dto/delete-account.dto.ts @@ -11,7 +11,7 @@ import { IsEmail, IsOptional, IsString, MaxLength } from 'class-validator'; export class DeleteAccountDto { @ApiProperty({ example: 'personne@example.com', - description: "Adresse du compte, ressaisie pour confirmer un acte irréversible", + description: 'Adresse du compte, ressaisie pour confirmer un acte irréversible', }) @IsEmail({}, { message: 'Une adresse email valide est requise pour confirmer.' }) confirmEmail: string; diff --git a/apps/backend/src/application/services/gdpr.service.spec.ts b/apps/backend/src/application/services/gdpr.service.spec.ts index 7301b30..4c5fe74 100644 --- a/apps/backend/src/application/services/gdpr.service.spec.ts +++ b/apps/backend/src/application/services/gdpr.service.spec.ts @@ -54,7 +54,9 @@ function buildService(options: { user?: UserOrmEntity | null } = {}) { } as unknown as EntityManager; const dataSource = { - transaction: jest.fn(async (callback: (m: EntityManager) => Promise) => callback(manager)), + transaction: jest.fn(async (callback: (m: EntityManager) => Promise) => + callback(manager) + ), query: jest.fn(async (sql: string, parameters: unknown[]) => { executed.push({ sql, parameters }); return []; diff --git a/apps/backend/src/application/services/gdpr.service.ts b/apps/backend/src/application/services/gdpr.service.ts index a4706a1..cd3791d 100644 --- a/apps/backend/src/application/services/gdpr.service.ts +++ b/apps/backend/src/application/services/gdpr.service.ts @@ -208,7 +208,9 @@ export class GDPRService { const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) throw new NotFoundException('User not found'); - this.logger.warn(`GDPR erasure starting for user ${userId} — reason: ${reason ?? 'unspecified'}`); + this.logger.warn( + `GDPR erasure starting for user ${userId} — reason: ${reason ?? 'unspecified'}` + ); const deleted: Record = {}; const anonymised: Record = {}; @@ -228,7 +230,9 @@ export class GDPRService { userId, ]); deleted.apiKeys = await rows('DELETE FROM api_keys WHERE user_id = $1', [userId]); - deleted.cookieConsent = await rows('DELETE FROM cookie_consents WHERE user_id = $1', [userId]); + deleted.cookieConsent = await rows('DELETE FROM cookie_consents WHERE user_id = $1', [ + userId, + ]); deleted.passwordResetTokens = await rows( 'DELETE FROM password_reset_tokens WHERE user_id = $1', [userId] @@ -361,7 +365,9 @@ export class GDPRService { const next: UpdateConsentDto = { essential: true, - functional: consentType ? consentType !== 'functional' && (current?.functional ?? false) : false, + functional: consentType + ? consentType !== 'functional' && (current?.functional ?? false) + : false, analytics: consentType ? consentType !== 'analytics' && (current?.analytics ?? false) : false, marketing: consentType ? consentType !== 'marketing' && (current?.marketing ?? false) : false, }; diff --git a/apps/backend/src/application/services/retention.service.spec.ts b/apps/backend/src/application/services/retention.service.spec.ts index ba663b0..a9c7133 100644 --- a/apps/backend/src/application/services/retention.service.spec.ts +++ b/apps/backend/src/application/services/retention.service.spec.ts @@ -49,7 +49,7 @@ describe('RetentionService', () => { } }); - it("épargne les traces de traitement des demandes de droits", async () => { + it('épargne les traces de traitement des demandes de droits', async () => { const { service, executed } = buildService(); await service.purge(); diff --git a/apps/backend/src/domain/ports/out/shipment-counter.port.ts b/apps/backend/src/domain/ports/out/shipment-counter.port.ts index 776f960..d6457ed 100644 --- a/apps/backend/src/domain/ports/out/shipment-counter.port.ts +++ b/apps/backend/src/domain/ports/out/shipment-counter.port.ts @@ -18,8 +18,5 @@ export interface ShipmentCounterPort { * an organization in a given year. Unpaid drafts (QUOTE), rejected and * cancelled bookings are excluded. */ - countPaidShipmentsForOrganizationInYear( - organizationId: string, - year: number - ): Promise; + countPaidShipmentsForOrganizationInYear(organizationId: string, year: number): Promise; } diff --git a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository.ts b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository.ts index fe03619..588e19d 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/repositories/typeorm-csv-rate-config.repository.ts @@ -1,10 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { - CsvRateConfigOrmEntity, - CsvRateDirection, -} from '../entities/csv-rate-config.orm-entity'; +import { CsvRateConfigOrmEntity, CsvRateDirection } from '../entities/csv-rate-config.orm-entity'; /** * CSV Rate Config Repository Port diff --git a/docs/CI-CD-SECURITY.md b/docs/CI-CD-SECURITY.md index 154ab7c..348fb21 100644 --- a/docs/CI-CD-SECURITY.md +++ b/docs/CI-CD-SECURITY.md @@ -142,7 +142,7 @@ ces configurations ne signale plus ce secret après modification. ## Validation -Les 25 tests offline de promotion et santé passent : arbre différent, marqueurs +Les 27 tests offline de promotion et santé passent : arbre différent, marqueurs manquants, panne du registre, digest invalide, mauvaise branche, saisie hostile et échec HTTP sont refusés. Les tests vérifient aussi que les échecs et délais des audits restent bloquants et que le rapport masque les valeurs de secrets. Actionlint valide la syntaxe après normalisation des URLs @@ -172,3 +172,20 @@ sur les runners de l'instance. Références : [différences Gitea 1.22](https://docs.gitea.com/1.22/usage/actions/comparison/), [Renovate sur Gitea](https://docs.renovatebot.com/modules/platform/gitea/). + +## Diagnostic du run Gitea 146 + +Le [journal complet du job sécurité](https://gitea.ops.xpeditis.com/David/xpeditis2.0/actions/runs/146/jobs/0/logs) +confirme que Node 22 et Trivy démarrent, que les validations passent et que +l'artefact `security-reports` est bien téléversé. L'affichage de l'action composite +s'arrête pourtant visuellement au lancement de Trivy. Les quatre workflows +exposent désormais installation, validation, audit, résumé et téléversement comme +étapes distinctes. Le résumé s'exécute même si l'audit échoue, sans afficher de +valeurs de secrets. Un rapport absent ou invalide est signalé comme indisponible. + +Ce run échoue réellement sur les audits : 50 vulnérabilités élevées backend, +13 élevées et une critique frontend, quatre détections de secrets et douze +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. diff --git a/scripts/ci/summarize-security.py b/scripts/ci/summarize-security.py new file mode 100644 index 0000000..f8a5266 --- /dev/null +++ b/scripts/ci/summarize-security.py @@ -0,0 +1,45 @@ +"""Print audit counts without exposing secret matches or source snippets.""" +import json +import os +from pathlib import Path + + +def summarize(reports): + incomplete = False + + def read(name): + nonlocal incomplete + try: + report = json.loads((reports / name).read_text()) + if not isinstance(report, dict) or report.get('error'): + raise ValueError('Invalid audit report') + return report + except (OSError, ValueError): + print(f'{name}: report missing, invalid or scanner error; inspect the audit step.') + incomplete = True + return {} + + for project in ('root', 'backend', 'frontend', 'log-exporter'): + report = read(f'npm-audit-{project}.json') + counts = report.get('metadata', {}).get('vulnerabilities', {}) + if 'high' not in counts or 'critical' not in counts: + print(f'{project}: dependency audit unavailable.') + incomplete = True + continue + print(f'{project}: {counts["high"]} high, {counts["critical"]} critical vulnerabilities.') + + report = read('source-security.json') + if 'Results' not in report: + print('Source audit unavailable.') + incomplete = True + else: + results = report['Results'] or [] + secrets = sum(len(result.get('Secrets') or []) for result in results) + misconfigs = sum(len(result.get('Misconfigurations') or []) for result in results) + print(f'Source: {secrets} secret findings, {misconfigs} infrastructure findings.') + print('Download the security-reports artifact for details. The audit step determines pass/fail.') + return int(incomplete) + + +if __name__ == '__main__': + raise SystemExit(summarize(Path(os.environ['RUNNER_TEMP']) / 'security-reports')) diff --git a/scripts/ci/test_security_summary.py b/scripts/ci/test_security_summary.py new file mode 100644 index 0000000..575ba9d --- /dev/null +++ b/scripts/ci/test_security_summary.py @@ -0,0 +1,45 @@ +"""Diagnostics must not leak scanner evidence or report missing audits as clean.""" +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +import unittest + +spec = importlib.util.spec_from_file_location( + 'security_summary', Path(__file__).with_name('summarize-security.py')) +summary = importlib.util.module_from_spec(spec) +spec.loader.exec_module(summary) + + +class SecuritySummary(unittest.TestCase): + def test_real_findings_are_counted_without_printing_evidence(self): + with tempfile.TemporaryDirectory() as temp: + reports = Path(temp) + for project in ('root', 'backend', 'frontend', 'log-exporter'): + (reports / f'npm-audit-{project}.json').write_text(json.dumps({ + 'metadata': {'vulnerabilities': {'high': 2, 'critical': 1}}})) + (reports / 'source-security.json').write_text(json.dumps({'Results': [{ + 'Secrets': [{'Match': 'DO-NOT-PRINT', 'Code': 'PRIVATE-CODE'}], + 'Misconfigurations': [{'Description': 'PRIVATE-DESCRIPTION'}]}]})) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(summary.summarize(reports), 0) + text = output.getvalue() + self.assertIn('2 high, 1 critical', text) + self.assertIn('1 secret findings, 1 infrastructure findings', text) + self.assertNotIn('DO-NOT-PRINT', text) + self.assertNotIn('PRIVATE-', text) + + def test_missing_and_failed_audits_are_not_reported_as_clean(self): + with tempfile.TemporaryDirectory() as temp: + reports = Path(temp) + (reports / 'npm-audit-root.json').write_text('{invalid') + (reports / 'npm-audit-backend.json').write_text('{"error": {"code": "NETWORK"}}') + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(summary.summarize(reports), 1) + self.assertIn('dependency audit unavailable', output.getvalue()) + self.assertIn('Source audit unavailable', output.getvalue()) + self.assertNotIn('0 high', output.getvalue())