Files
cannamanage/docs/security-code-review-final.md
T
Patrick Plate 6f7352124d
Deploy to Production / test (push) Failing after 10m44s
Deploy to Production / deploy (push) Has been skipped
fix(security): hardening — rate limiting, CORS config, audit safety, CSP headers, validation
- Fix 1: Login rate limiting (5 attempts/min/IP) on POST /api/v1/auth/login
  - New LoginRateLimiter (ConcurrentHashMap + @Scheduled reset every 60s)
  - HTTP 429 with German message on exceed
  - Client IP via X-Forwarded-For with proxy fallback
  - @EnableScheduling on CannaManageApplication

- Fix 2: CORS origins configurable via cannamanage.cors.allowed-origins env var
  - Defaults to localhost + docker frontend for dev
  - SecurityConfig reads with @Value, splits comma-separated list

- Fix 3: Audit JSON safety — replaced manual string concat with Jackson ObjectMapper
  - New AuditService.toMetadataJson(Map) helper
  - RetentionService and AuthorityExportService refactored

- Fix 4: Tomcat max-http-form-post-size=2MB prevents DoS via oversized payloads

- Fix 5: @Valid added to @RequestBody on 17+ endpoints across
  ComplianceRecordsController, FinanceController, ConsentController,
  StaffController, ComplianceDeadlineController, SubscriptionController,
  ForumController (admin + portal)

- Fix 6: Content-Security-Policy 'default-src \'self\'; frame-ancestors \'none\''
  + frameOptions(deny) on both API + portal filter chains
2026-06-15 19:29:32 +02:00

292 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CannaManage — Final Security & Code Review
**Datum:** 2026-06-15
**Reviewer:** Lumen (Code mode, SonarQube + manual deep-scan)
**Scope:** Vollständiges Backend (Sprints 110) — ca. 29.000 LOC Java Production, 3.600 LOC Tests, 26.000 LOC Frontend TypeScript
**Branch:** master
**BigMind Session:** `43f1d5c3-4805-42a6-8408-145784f6603e`
---
## 🎯 Verdict: ⚠️ CONDITIONAL PASS
**Production-Deployment ist NICHT freigegeben**, bis die 4 BLOCKER unten behoben sind. Das Fundament ist solide — Architektur, Tenant-Isolation, Compliance-Layer, Spring-Boot-CVE-Hardening, XXE-Schutz, BCrypt+SHA-256, GoBD-Append-Only-Ledger sind allesamt korrekt umgesetzt. Aber 4 konkrete, vermeidbare Schwachstellen blockieren den Go-Live.
| Kategorie | Score | Status |
|-----------|-------|--------|
| Architektur & Patterns | 9/10 | ✅ Sehr gut |
| Authentication (JWT/BCrypt) | 7/10 | ⚠️ Dev-Fallback noch da |
| Authorization (Roles + Permissions) | 6/10 | ❌ DocumentController ungeschützt |
| Tenant-Isolation | 8/10 | ⚠️ Lücke bei Download-by-UUID |
| Input Validation | 7/10 | ⚠️ Path Traversal in DocumentService |
| Data Protection (DSGVO/KCanG/GoBD) | 10/10 | ✅ Vollständig |
| Dependency Hygiene | 9/10 | ✅ Snyk-Overrides 2026-06-12 |
| Error Handling | 9/10 | ✅ RFC 9457, keine Info-Disclosure |
| Test Coverage | 4/10 | ❌ 20 Tests für 29K LOC zu wenig |
| Logging Quality | 10/10 | ✅ Keine Concat, alles parameterized |
---
## 1. SonarQube SAST — Findings Summary
7 Dateien gescannt mit `analyze_code_snippet`. Nur echte Findings (S1598 Pfad-Rauschen ignoriert):
| Datei | Rule | Schweregrad | Befund |
|-------|------|-------------|--------|
| [`DocumentService.uploadDocument()`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:60) | S2184 | MAJOR | `file.getSize() > MAX_FILE_SIZE` — int overflow risk (cast `(long)` fehlt) |
| [`DocumentService.uploadDocument()`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:62) | S1075 | MAJOR | Hard-coded path delimiter `"/"` |
| [`DocumentService`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:36) | S1068 | MAJOR | Unbenutztes Feld `auditService` (Dead Code) |
| [`Camt053Parser`](cannamanage-service/src/main/java/de/cannamanage/service/bankimport/Camt053Parser.java:102) | S112 | MAJOR | Generisches `throw new Exception(...)` |
| [`Camt053Parser`](cannamanage-service/src/main/java/de/cannamanage/service/bankimport/Camt053Parser.java:308) | S3252 | MINOR | Statischer Zugriff via Instanz statt Class |
| [`AuthService.sha256()`](cannamanage-api/src/main/java/de/cannamanage/api/service/AuthService.java:144) | S112 | MAJOR | Generisches `RuntimeException` |
| [`AuthService`](cannamanage-api/src/main/java/de/cannamanage/api/service/AuthService.java:54) | S1192 | MINOR | String `"Invalid credentials"` 3× dupliziert |
| [`SecurityConfig`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:25) | S1068 | MAJOR | (Snippet-Artefakt; im Original wird `portalUserDetailsService` genutzt — kein echtes Finding) |
`JwtService` und `BankImportService` waren bis auf S1598-Rauschen sauber.
---
## 2. Manual 16-Item Security Checklist
| # | Regel | Prüfpunkt | Ergebnis | Begründung |
|---|-------|-----------|----------|-----------|
| 1 | SEC-001..004 | Keine hartkodierten Credentials in Production-Code | ⚠️ Teilweise | JWT-Dev-Secret in [`application.properties:8`](cannamanage-api/src/main/resources/application.properties:8) — Production-Profile nutzt Env-Var, aber Default-Profile hat Fallback |
| 2 | SEC-005 | Credentials via `@Value`/env | ✅ | Production-Properties durchgängig `${...}`; Stripe-Keys, DB-Credentials, JWT-Secret korrekt |
| 3 | SEC-011 | Keine SQL-Injection | ✅ | **Null** `nativeQuery=true` im gesamten Codebase, **null** String-Concat in `@Query`. JPA/HQL nur mit benannten Parametern |
| 4 | SEC-012 | Kein Path Traversal | ❌ | [`DocumentService.uploadDocument()`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:62) nutzt `file.getOriginalFilename()` unsaniert. BankImportService macht es richtig mit `FilenameUtils.getName()` — Inkonsistenz |
| 5 | SEC-016 | Input-Validierung | ✅ | DTOs mit `@Valid`, `MethodArgumentNotValidException` zentral behandelt, File-Type-Whitelist auf DocumentService |
| 6 | SEC-018 | Keine Info-Disclosure in Fehlern | ✅ | [`GlobalExceptionHandler`](cannamanage-api/src/main/java/de/cannamanage/api/exception/GlobalExceptionHandler.java:133) loggt intern, gibt nur "An unexpected error occurred" zurück. Production-Properties: `server.error.include-stacktrace=never` |
| 7 | SEC-033 | PII-Verschlüsselung (at rest) | ✅ | PostgreSQL TDE-fähig; Passwords BCrypt, Refresh-Tokens SHA-256 hashed. Keine Klartext-PII-Spalten neu eingeführt |
| 8 | SEC-035 | Kein PII in LLM-Verarbeitung | ✅ N/A | Keine LLM-Integration im Code |
| 9 | SEC-040 | Keine sensiblen Daten in Logs | ✅ | **Null** Treffer für `log.x(".." + ...)` Concat-Antipattern. Alle Logs parameterized. Stichproben: keine Passwörter, JWT-Tokens, IBANs vollständig geloggt |
| 10 | SEC-055 | Keine generierten Quellen geändert | ✅ N/A | Kein `src.gen/` in diesem Projekt |
| 11 | — | Tenant-Isolation aktiv | ⚠️ | Hibernate `@Filter("tenantFilter")` auf [`AbstractTenantEntity`](cannamanage-domain/src/main/java/de/cannamanage/domain/entity/AbstractTenantEntity.java:22) + [`TenantFilterAspect`](cannamanage-api/src/main/java/de/cannamanage/api/security/TenantFilterAspect.java:30) — solide. ABER: bei Lookups per Raw-UUID ohne `clubId`-Parameter greift Filter nur, wenn Aspect vorher den Tenant gesetzt hat. Defense-in-depth fehlt bei DocumentService.download |
| 12 | — | JWT-Implementierung sicher | ⚠️ | HS256 mit 32-Byte-Base64-Key ✓, jti claim ✓, Access 1h / Refresh 30d ✓ — aber siehe Punkt 1 (Dev-Fallback) |
| 13 | — | CSRF / Session-Management | ✅ | Member-Portal: `CookieCsrfTokenRepository` + Session-Fixation-Schutz (`maximumSessions(1)`). API: stateless JWT, CSRF korrekt disabled |
| 14 | — | CORS restriktiv | ❌ | [`SecurityConfig.corsConfigurationSource()`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:131) hardcoded auf `localhost:3000` + `frontend:3000`. Production-Domains nicht konfigurierbar. `setAllowedHeaders("*")` zu permissiv |
| 15 | — | Rate-Limiting auf kritischen Endpoints | ❌ | Nur [`AuthorityExportService`](cannamanage-service/src/main/java/de/cannamanage/service/report/AuthorityExportService.java:76) hat Rate-Limit (1/h pro Tenant). **Kein Rate-Limit auf `/api/v1/auth/login`** — Brute-Force-Vektor |
| 16 | SEC-064 | Keine Secrets im Source (DataRake) | ⚠️ | Dev-Default-JWT in `application.properties` würde DataRake triggern (Base64 ≥ 6 Zeichen, in nicht-Test-Datei). Sonst sauber |
---
## 3. Vulnerability Summary — Sprint 9 Findings Status
| # | Sprint-9-Finding | Erwartet | Tatsächlicher Status |
|---|-----------------|----------|---------------------|
| 1 | Path Traversal in DocumentService (Medium) | FIXED | ❌ **NICHT FIXED** — Code unverändert, `file.getOriginalFilename()` unsaniert |
| 2 | JWT Dev-Secret Fallback (Medium) | FIXED | ❌ **NICHT FIXED** — [`application.properties:8`](cannamanage-api/src/main/resources/application.properties:8) hat noch Default-Wert |
| 3 | Spring Boot CVE (Medium) | FIXED | ✅ **FIXED** — [`pom.xml`](pom.xml) Override 2026-06-12: Spring 7.0.8, Tomcat 11.0.22, Postgres 42.7.11, "fixes 10 HIGH + 18 MEDIUM Snyk SCA findings" |
| 4 | CORS Hardcoded Localhost (Low) | FIXED | ❌ **NICHT FIXED** — siehe Item 14 |
| 5 | Login Rate-Limit (Low) | FIXED | ❌ **NICHT FIXED** — siehe Item 15 |
### 🚨 Neu entdeckte Findings
#### BLOCKER #1 — IDOR / Horizontal Privilege Escalation in [`DocumentController`](cannamanage-api/src/main/java/de/cannamanage/api/controller/DocumentController.java:52)
```java
@GetMapping("/documents/{id}/download")
public ResponseEntity<byte[]> downloadDocument(@PathVariable UUID id) throws IOException {
Document doc = documentService.getDocument(id);
byte[] content = documentService.downloadDocument(id);
...
}
```
- Kein `clubId`-Parameter, keine `@PreAuthorize`, kein Permission-Check, keine Tenant-Verifikation in der Service-Methode
- `documentService.downloadDocument(id)` lädt das Document direkt per Raw-UUID
- `documentRepository.findById(documentId)` greift zwar Hibernate-Filter — aber **nur wenn `TenantContext.getCurrentTenant()` korrekt aus dem JWT gesetzt ist**. Da der Endpoint im SecurityConfig nicht in einem spezifischen role-Matcher gelistet ist, fällt er unter `.anyRequest().authenticated()`**JEDER authentifizierte User (MEMBER, STAFF eines anderen Clubs)** kann mit einer geratenen/geleakten UUID Dokumente herunterladen
- Gleiches Muster bei `@DeleteMapping("/documents/{id}")`: Auch hier kein Permission-Check
**Severity:** HIGH (CVSS ~7.5 — N/L/N/R/U/N/H/N — Confidentiality breach across tenants)
#### BLOCKER #2 — Path Traversal in [`DocumentService.uploadDocument()`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:62)
```java
String filename = file.getOriginalFilename() != null ? file.getOriginalFilename() : "document";
String storagePath = clubId + "/" + documentId + "_" + filename;
Path fullPath = Paths.get(UPLOAD_BASE, storagePath);
```
- Filename wie `../../etc/passwd.pdf` schreibt außerhalb von `UPLOAD_BASE`
- BankImportService verwendet bereits korrekt `FilenameUtils.getName()` — Fix-Muster vorhanden, nur nicht angewendet
**Severity:** HIGH (CVSS ~7.5 — Arbitrary file write)
#### BLOCKER #3 — JWT Dev-Secret Fallback in [`application.properties:8`](cannamanage-api/src/main/resources/application.properties:8)
```properties
cannamanage.security.jwt.secret=Y2FubmFtYW5hZ2Utand0LXNlY3JldC1rZXktZm9yLWRldmVsb3BtZW50LW9ubHktMzI=
```
- Wenn Production-Deployment vergisst, `SPRING_PROFILES_ACTIVE=production` zu setzen, wird der bekannte Dev-Secret verwendet
- Angreifer kann beliebige JWTs signieren und sich als ADMIN ausgeben
**Severity:** HIGH (im Worst Case CRITICAL) — Misconfiguration-Risiko
#### BLOCKER #4 — Kein DocumentController-Matcher in SecurityConfig
Im [`SecurityConfig.apiSecurityFilterChain()`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:50) fehlen Matcher für `/api/v1/documents/**`. Defense-in-depth-Schicht entfällt. Sollte mindestens `.hasAnyRole("ADMIN", "STAFF")` für POST/DELETE und `.hasAnyRole("ADMIN", "STAFF", "MEMBER")` für GET sein, kombiniert mit Service-seitiger Tenant-Prüfung.
### Weitere mittlere Findings
| # | Befund | Severity |
|---|--------|----------|
| M1 | `DocumentService.downloadDocument()` / `deleteDocument()` haben kein AuditService-Logging (Feld ist sogar als ungenutzt markiert) — DSGVO Art. 30 verlangt Verarbeitungsverzeichnis für Document-Access | MEDIUM |
| M2 | CORS `setAllowedHeaders("*")` + `setAllowCredentials(true)` ist seit CVE-2017-* zwar kein direkter Bug, aber Best-Practice ist explizite Header-Liste | LOW |
| M3 | `AuthService.sha256()` wirft generisches `RuntimeException` bei `NoSuchAlgorithmException` — unreachable, aber sollte spezifische Custom-Exception sein | LOW |
| M4 | Refresh-Token-Hash in User-Entity: einzelner Slot pro User → kein Multi-Device-Support; Logout auf einem Device killt Session auf anderen. Aktuell akzeptabel, dokumentieren | INFO |
---
## 4. Code Quality Assessment
### ✅ Stärken
| Aspekt | Bewertung |
|--------|-----------|
| Pattern-Konsistenz | Sehr hoch — Service+Repository+Controller-Trennung sauber, `AbstractTenantEntity` als gemeinsame Basis durchgezogen |
| Transaction Boundaries | Korrekt — `@Transactional` auf Service-Methoden, `readOnly=true` wo angebracht (RetentionService, DsgvoService) |
| N+1 Query Risk | Niedrig — [`PaymentMatchingService.buildContexts()`](cannamanage-service/src/main/java/de/cannamanage/service/bankimport/PaymentMatchingService.java:196) preloadet bewusst in 3 Bulk-Queries |
| Null Safety | Hoch — `Optional` wird konsistent verwendet, `.orElseThrow()` mit aussagekräftigen Messages |
| Error Handling | RFC 9457 application/problem+json über [`GlobalExceptionHandler`](cannamanage-api/src/main/java/de/cannamanage/api/exception/GlobalExceptionHandler.java:28), 13 spezifische `@ExceptionHandler` |
| Audit Logging | 63 Aufrufe von `auditService.log(...)` verteilt — sehr gute Abdeckung |
| Compliance Domain | GoBD §147 (Append-Only Ledger, kompensierende Buchungen), KCanG §22/§24, DSGVO Art. 15/17/30 vollständig implementiert |
| XML/XXE | [`Camt053Parser`](cannamanage-service/src/main/java/de/cannamanage/service/bankimport/Camt053Parser.java:69) defensiv konfiguriert (DTD off, External Entities off, Entity Refs off) — vorbildlich |
| Streaming für große Exports | [`AuthorityExportService`](cannamanage-service/src/main/java/de/cannamanage/service/report/AuthorityExportService.java:92) `OutputStream`-basiert, kein Heap-Risiko |
| Stripe Webhook | Signature-Validierung via `Webhook.constructEvent(...)` ✓ |
### ⚠️ Schwächen
| Aspekt | Bewertung |
|--------|-----------|
| **Test Coverage** | **Kritisch niedrig** — 20 Tests für 29.000 LOC Production. Verhältnis ~12% nach LOC. Domain-Modul (`cannamanage-domain`) hat **0 Tests** trotz `AbstractTenantEntity`-Logik und Entity-Validierungen |
| Magic Strings | `"Invalid credentials"`, `"GELÖSCHT"`, `"DOCUMENT_UPLOADED"` — sollten Konstanten in `enum`/`class` sein |
| Dead Code | `DocumentService.auditService` ungenutzt; Sprint-Cleanup steht aus |
| Inkonsistente File-Sanitization | BankImport ✓, Document ✗ — gleiches Risiko, zwei verschiedene Lösungen |
| Cookie-Härtung explizit | `Secure`/`HttpOnly`/`SameSite` nicht in application.properties gesetzt; Spring-Defaults greifen, sollte aber explizit dokumentiert sein |
### Code-Smells nach SonarQube
- 4× S1192 (Duplicated String Literals) — kosmetisch
- 3× S112 (Generic Exception) — in Edge-Paths (sha256, XML-Parse)
- 1× S2184 (long cast missing) — `file.getSize()` ist bereits `long`, Konstante `MAX_FILE_SIZE` als `int` deklariert → potenziell `int * 1024 * 1024` Overflow, falls jemand `MAX_FILE_SIZE` erhöht
- 1× S1075 (hard-coded `"/"`) — gleichzeitig OS-Inkompatibilität
---
## 5. Test Coverage Analyse
```
Total Production LOC: 28.977
Total Test LOC: 3.609
Test Files: 20 (domain: 0, service: 9, api: 11)
Ratio: ~12% (LOC) / ~7 Tests pro 10K LOC
```
**Branchenstandard für Finanz/Compliance-Software: 7080% Coverage, mindestens 1 Test pro 100 LOC.**
Kritische Lücken:
- `cannamanage-domain` — 0 Tests trotz `AbstractTenantEntity`, Enums mit Business-Logik, Entity-Validierung
- `RetentionService.anonymizeExpiredMembers()` — KCanG §24-Compliance, sollte voll getestet sein
- `DsgvoService.exportUserData()` — Art. 15 DSGVO, rechtlich kritisch
- `PaymentMatchingService.scoreOne()` — komplexer Algorithmus mit Schwellwerten
- `Camt053Parser` / `Mt940Parser` — externe Datenformate, perfekter Fall für umfangreiche Testdaten
---
## 6. Compliance Snapshot
| Verordnung | Erfüllt? | Beleg |
|-----------|---------|-------|
| **DSGVO Art. 15** (Auskunft) | ✅ | [`DsgvoService.exportUserData()`](cannamanage-service/src/main/java/de/cannamanage/service/DsgvoService.java:50) |
| **DSGVO Art. 17** (Löschung) | ✅ | [`DsgvoService.deleteUserData()`](cannamanage-service/src/main/java/de/cannamanage/service/DsgvoService.java:123) — anonymisiert statt löscht (KCanG-konform) |
| **DSGVO Art. 30** (Verarbeitungsverzeichnis) | ⚠️ | AuditService vorhanden, aber Document-Download nicht geloggt |
| **DSGVO Art. 32** (Sicherheit der Verarbeitung) | ⚠️ | Blockiert durch IDOR-Findings |
| **DSGVO Art. 33/34** (Breach Notification) | ✅ | Workflow dokumentiert, audit_event_log persistent |
| **DSGVO Art. 35** (DPIA) | ✅ | Templates in Sprint-7-Plan |
| **KCanG §22** (Mitgliederverzeichnis) | ✅ | Members-Modul vollständig |
| **KCanG §24** (Aufbewahrung 5J) | ✅ | [`RetentionService`](cannamanage-service/src/main/java/de/cannamanage/service/RetentionService.java:44) Scheduled Job, anonymisiert nach 5 Jahren |
| **AO §147** (GoBD 10J) | ✅ | Append-Only Ledger, voids = kompensierende Buchungen, immutable nach completion |
| **PSD2 / IBAN-Handling** | ✅ | Normalisierung, kein Klartext-Logging |
| **Stripe PCI-DSS** | ✅ | Stripe Hosted Checkout, kein Karten-Daten-Touch |
---
## 7. Recommendations — Priorisiert
### 🚨 BLOCKER (vor Production-Deploy beheben — geschätzt 46h)
1. **[`DocumentController`](cannamanage-api/src/main/java/de/cannamanage/api/controller/DocumentController.java:52)**: `@PreAuthorize` einbauen, `clubId` als Path/Param fordern, Service-seitig Tenant-Match prüfen:
```java
@GetMapping("/documents/{id}/download")
@PreAuthorize("hasAnyRole('ADMIN','STAFF','MEMBER')")
public ResponseEntity<byte[]> downloadDocument(@PathVariable UUID id, Principal principal) {
byte[] content = documentService.downloadDocumentForTenant(id, getCurrentTenantFromPrincipal(principal));
...
}
```
Service muss `doc.getTenantId().equals(currentTenantId)` verifizieren.
2. **[`DocumentService.uploadDocument()`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:60)**: Filename sanitization:
```java
String safeName = org.apache.commons.io.FilenameUtils.getName(file.getOriginalFilename());
if (safeName == null || safeName.isBlank()) safeName = "document";
```
3. **[`application.properties:8`](cannamanage-api/src/main/resources/application.properties:8)**: Default-JWT-Secret entfernen, durch Fail-Fast ersetzen:
```properties
cannamanage.security.jwt.secret=${CANNAMANAGE_SECURITY_JWT_SECRET}
```
Plus `JwtService` Postconstruct-Validation: Mindestlänge 32 Bytes, sonst Startup-Failure.
4. **[`SecurityConfig`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:50)**: `/api/v1/documents/**`-Matcher hinzufügen (Defense-in-Depth):
```java
.requestMatchers(HttpMethod.GET, "/api/v1/documents/**").hasAnyRole("ADMIN","STAFF","MEMBER")
.requestMatchers("/api/v1/documents/**").hasAnyRole("ADMIN","STAFF")
```
### 🟡 SHOULD-FIX (innerhalb der nächsten 2 Wochen — geschätzt 812h)
5. CORS konfigurierbar machen über `app.cors.allowed-origins` Property
6. Rate-Limiting auf `/api/v1/auth/login` (z.B. Bucket4j, 10 req/min pro IP)
7. Audit-Logging für Document-Download/Delete-Operationen
8. `MAX_FILE_SIZE` als `long` deklarieren
9. Konstanten für duplizierte Strings (`Constants.AUTH_INVALID_CREDENTIALS`)
10. Custom Exceptions statt generischer `RuntimeException`/`Exception`
### 🟢 NICE-TO-HAVE (technische Schuld, abarbeiten nach Go-Live)
11. **Test-Coverage auf 60% steigern** — primär `cannamanage-domain` + `RetentionService` + `DsgvoService` + Parser
12. Dead-Code-Eliminierung (ungenutzte Felder)
13. Cookie-Security explizit in `application-production.properties`
14. Multi-Device-Refresh-Tokens (separates Table statt einzelner Hash)
---
## 8. Was wirklich gut ist (Anerkennung)
- **GoBD-Append-Only-Ledger in [`FinanceService`](cannamanage-service/src/main/java/de/cannamanage/service/FinanceService.java:201)** — Voids als kompensierende Buchungen, nicht als Updates. Lehrbuch-konform.
- **XXE-Härtung in [`Camt053Parser`](cannamanage-service/src/main/java/de/cannamanage/service/bankimport/Camt053Parser.java:69)** — `SUPPORT_DTD=false` + `IS_SUPPORTING_EXTERNAL_ENTITIES=false` + `IS_REPLACING_ENTITY_REFERENCES=false`. Vorbildlich.
- **Spring Boot 4.0.6 + Snyk Override 2026-06-12** — 10 HIGH + 18 MEDIUM Findings proaktiv eliminiert.
- **PaymentMatching Weighted Scoring** — 35/30/20/15-Verteilung mit Schwellwerten AUTO=90/SUGGEST=60 und Doppelzahlungs-Schutz. Sauberes Algorithmus-Design.
- **Tenant-Filter über Hibernate `@Filter` + AspectJ** — funktional korrekt, performant, idiomatic.
- **RFC 9457 Problem Details** — moderner Standard im [`GlobalExceptionHandler`](cannamanage-api/src/main/java/de/cannamanage/api/exception/GlobalExceptionHandler.java:28).
- **63 Audit-Log-Calls** — solides Activity-Trail-Niveau.
- **KCanG-konforme Anonymisierung statt Löschung** — wichtiger Compliance-Nuance, korrekt umgesetzt.
---
## 9. Final Verdict
**⚠️ CONDITIONAL PASS — Production-Freigabe nach Behebung der 4 BLOCKER (geschätzt 46 Stunden Entwicklungszeit).**
Das Codebase ist insgesamt überraschend reif für 10 Sprints. Die Architektur ist solide, der Compliance-Layer ist außergewöhnlich gut, und die Dependency-Hygiene wurde aktiv gepflegt. Die 4 Blocker sind alle in einem Sprint-Tag fixbar — keine architektonischen Probleme, sondern konkrete vergessene Sicherheitsschichten beim `documents`-Modul + ein vergessener Cleanup-Schritt der Sprint-9-Findings.
**Nach Fix der Blocker und Re-Verification:** ✅ FULL PASS empfohlen.
Empfehlung für die nächste Iteration: **Test-Coverage von 12% auf ≥ 60%** ist die wichtigste mittelfristige Investition. Für eine Cannabis-Club-Management-Software mit Finanz- und Behörden-Reporting ist das aktuelle Niveau für einen produktiven Betrieb mit Echtkunden zu niedrig.
---
**Reviewer-Signatur:** Lumen (Claude-Opus-4.7), BigMind Session `43f1d5c3-4805-42a6-8408-145784f6603e`, 2026-06-15 18:46 CEST