feat(sprint9): Phase 6 — Compliance dashboard, RetentionService, testing
Backend: - ComplianceDashboardService: traffic-light status per ComplianceArea (KCANG/FINANCE/DSGVO/VEREIN) based on deadlines, payments, board positions - RetentionService: scheduled anonymization of expired member data (KCanG §24, 5 years), with dry-run preview and retention report endpoints - ComplianceDeadlineSeeder: seeds 5 standard recurring deadlines on club creation - ComplianceDashboardController: GET /api/v1/compliance/dashboard, GET /retention, POST /retention/preview - Repository additions: countOverdue, countActive board positions/members Frontend: - /compliance page with traffic-light status cards per area - Overdue deadlines section (highlighted red) with 'days overdue' badges - Upcoming deadlines with 'days until due' badges and 'Complete' buttons - Retention info cards (KCanG §24: 5y, AO §147: 10y, DSGVO: 2y) - Navigation: added 'Compliance-Status' to sidebar under Compliance group - compliance-dashboard.ts service with mock data for dev mode Build verified: pnpm build passes clean.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Security Scan Report: CannaManage (Sprint 7–9 Focus)
|
||||
|
||||
**Date:** 2026-06-15
|
||||
**Reviewer:** Roo (Security Reviewer)
|
||||
**Scope:** Full codebase with focus on Sprint 7–9 additions
|
||||
**Tools Used:** SonarQube SAST (MCP), Snyk SCA, Manual Checklist
|
||||
|
||||
---
|
||||
|
||||
## Verdict: ✅ PASS (with Medium advisories)
|
||||
|
||||
No Critical or High severity security vulnerabilities found.
|
||||
2 Medium findings require attention in the next sprint.
|
||||
|
||||
---
|
||||
|
||||
## 1. Automated Scan Results
|
||||
|
||||
### 1.1 SonarQube SAST (Static Application Security Testing)
|
||||
|
||||
| File | Issues | Security Impact |
|
||||
|------|--------|-----------------|
|
||||
| `SecurityConfig.java` | 4× string duplication (S1192), 1× package FP | None — maintainability only |
|
||||
| `JwtAuthFilter.java` | 1× package FP | None |
|
||||
| `AuthController.java` | 1× package FP | None |
|
||||
| `DocumentService.java` | 1× integer overflow in constant (S2184), 1× generic exception (S112), 2× hardcoded path delimiter (S1075) | **Low** — see findings |
|
||||
|
||||
**Summary:** 0 security vulnerabilities detected. All SAST findings are maintainability/reliability issues, not exploitable security weaknesses.
|
||||
|
||||
### 1.2 Snyk SCA (Software Composition Analysis)
|
||||
|
||||
| Component | Severity | Vulnerability | Fix Available |
|
||||
|-----------|----------|---------------|---------------|
|
||||
| `spring-boot-autoconfigure@4.0.6` | **Medium** | Insecure Temporary File (SNYK-JAVA-ORGSPRINGFRAMEWORKBOOT-17308346) | Upgrade to `4.0.7` |
|
||||
| `openpdf@2.0.4` | **Medium** | Dual License (LGPL-2.1/MPL-2.0) — license compliance risk | Accept or replace |
|
||||
|
||||
**Frontend (npm):** ✅ 0 vulnerabilities across 18 dependencies
|
||||
**Backend (Maven):** 2 medium issues (1 vulnerability, 1 license)
|
||||
**Total projects tested:** 7
|
||||
|
||||
---
|
||||
|
||||
## 2. Manual Security Checklist
|
||||
|
||||
| # | Check | Result | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | No hardcoded secrets in source | ⚠️ Medium | JWT dev secret in `application.properties` line 8 — acceptable for dev profile, production uses `${CANNAMANAGE_SECURITY_JWT_SECRET}` env var |
|
||||
| 2 | JWT secret from environment variable only | ✅ | Production profile (`application-production.properties:22`) uses `${CANNAMANAGE_SECURITY_JWT_SECRET}` |
|
||||
| 3 | CSRF protection configured properly | ✅ | API (stateless JWT) correctly disables CSRF; Portal (session-based) uses `CookieCsrfTokenRepository` |
|
||||
| 4 | CORS not overly permissive | ✅ | Restricted to `localhost:3000` and `frontend:3000` (Docker internal). Production should add production domain. |
|
||||
| 5 | File upload size/type restrictions | ✅ | `DocumentService.java:26-33` — 10MB max, allowlist: PDF/DOCX/XLSX/PNG/JPG |
|
||||
| 6 | SQL injection prevention | ✅ | All queries use JPQL with named parameters (`:param`). No native queries with string concatenation. 12 `@Query` annotations reviewed — all parameterized. |
|
||||
| 7 | Path traversal prevention in DocumentService | ⚠️ Medium | `DocumentService.java:62` — filename from `file.getOriginalFilename()` is used in path construction without sanitization. UUID prefix mitigates exploitation but the original filename is concatenated directly. |
|
||||
| 8 | Rate limiting on sensitive endpoints | ✅ | Authority export: 1/hour per tenant (`AuthorityExportService.java:76-79`). Email: rate-limited to 50/min. Login: no explicit rate limit but mitigated by BCrypt cost factor. |
|
||||
| 9 | Password hashing with BCrypt | ✅ | `SecurityConfig.java:124` — `BCryptPasswordEncoder`. Password validation with complexity regex in `SetPasswordRequest.java`. |
|
||||
| 10 | Tenant isolation (club_id filtering) | ✅ | All service methods accept `clubId` from `TenantContext.getCurrentTenant()` (JWT claim, not user input). Portal endpoints derive `memberId` from authenticated `userId`. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Detailed Findings
|
||||
|
||||
### 3.1 ⚠️ Medium: Path Traversal Risk in DocumentService
|
||||
|
||||
**File:** [`DocumentService.java`](cannamanage-service/src/main/java/de/cannamanage/service/DocumentService.java:62)
|
||||
**Rule:** OWASP A01:2021 — Broken Access Control
|
||||
|
||||
```java
|
||||
String filename = file.getOriginalFilename() != null ? file.getOriginalFilename() : "document";
|
||||
String storagePath = clubId + "/" + documentId + "_" + filename;
|
||||
Path fullPath = Paths.get(UPLOAD_BASE, storagePath);
|
||||
```
|
||||
|
||||
**Risk:** A malicious filename like `../../../etc/passwd` could theoretically escape the upload directory. The UUID prefix (`documentId + "_"`) and the fact that `clubId` is server-controlled reduce exploitability, but the original filename is not sanitized for path separators.
|
||||
|
||||
**Recommendation:** Add filename sanitization:
|
||||
```java
|
||||
String sanitized = Paths.get(filename).getFileName().toString();
|
||||
// or: filename.replaceAll("[^a-zA-Z0-9.\\-_]", "_");
|
||||
```
|
||||
|
||||
**Exploitability:** Low (UUID prefix + server-controlled clubId make it very hard to construct a useful path traversal), but defense-in-depth principle applies.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 ⚠️ Medium: JWT Dev Secret in Default Properties
|
||||
|
||||
**File:** [`application.properties`](cannamanage-api/src/main/resources/application.properties:8)
|
||||
|
||||
```properties
|
||||
cannamanage.security.jwt.secret=Y2FubmFtYW5hZ2Utand0LXNlY3JldC1rZXktZm9yLWRldmVsb3BtZW50LW9ubHktMzI=
|
||||
```
|
||||
|
||||
**Risk:** This is a base64-encoded development-only secret. Production correctly overrides via `${CANNAMANAGE_SECURITY_JWT_SECRET}`. However, if the production environment variable is ever missing, Spring Boot falls back to this known value.
|
||||
|
||||
**Recommendation:**
|
||||
- Add a startup check that fails if running with `production` profile and the default secret is detected
|
||||
- Or remove the default and require the env var in all profiles
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ⚠️ Medium: Spring Boot Insecure Temporary File (CVE)
|
||||
|
||||
**Component:** `spring-boot-autoconfigure@4.0.6`
|
||||
**Fix:** Upgrade to Spring Boot `4.0.7`
|
||||
**Impact:** Temporary file creation may use insecure permissions on some OS configurations.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 ℹ️ Low: No Login Rate Limiting
|
||||
|
||||
**File:** [`AuthController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/AuthController.java:30)
|
||||
|
||||
The `/api/v1/auth/login` endpoint has no explicit rate limiting. BCrypt's computational cost provides some natural brute-force resistance (~100ms per attempt), but a dedicated rate limiter (e.g., Bucket4j or Spring Security's `AuthenticationFailureHandler` with exponential backoff) would strengthen defense.
|
||||
|
||||
**Recommendation:** Add rate limiting: max 5 failed attempts per IP per 15 minutes.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 ℹ️ Low: CORS Missing Production Domain
|
||||
|
||||
**File:** [`SecurityConfig.java`](cannamanage-api/src/main/java/de/cannamanage/api/security/SecurityConfig.java:131)
|
||||
|
||||
CORS `allowedOrigins` only includes `localhost:3000` and `frontend:3000`. The production domain (`cannamanage.plate-software.de`) is not listed. This is likely handled by the reverse proxy (nginx), but if the API is ever accessed directly, CORS will block legitimate requests.
|
||||
|
||||
**Recommendation:** Make CORS origins configurable via `@Value` from application properties.
|
||||
|
||||
---
|
||||
|
||||
### 3.6 ℹ️ Low: AuthorityExportService — JSON Injection in Audit Log
|
||||
|
||||
**File:** [`AuthorityExportService.java`](cannamanage-service/src/main/java/de/cannamanage/service/report/AuthorityExportService.java:104)
|
||||
|
||||
```java
|
||||
"{\"year\":" + year + ",\"reason\":\"" + escapeJson(reason) + "\"}"
|
||||
```
|
||||
|
||||
The `reason` field is escaped via `escapeJson()`, which is good. However, manual JSON construction is fragile. Consider using a proper JSON library (Jackson `ObjectMapper`) for audit metadata serialization.
|
||||
|
||||
---
|
||||
|
||||
## 4. Security Architecture Assessment
|
||||
|
||||
### Strengths ✅
|
||||
|
||||
1. **Multi-layer authentication:** JWT for API, session-based for portal, re-authentication for sensitive exports
|
||||
2. **RBAC with granular permissions:** 23+ `StaffPermission` enum values, checked via `StaffPermissionChecker`
|
||||
3. **Tenant isolation:** `TenantContext` from JWT claims, not user-controllable input
|
||||
4. **Token revocation:** JTI-based blacklist checked on every request
|
||||
5. **Append-only financial data:** `LedgerEntry` per §147 AO — cannot delete or modify
|
||||
6. **Audit trail:** Comprehensive `AuditService.log()` calls on all sensitive operations
|
||||
7. **File upload validation:** Size limit + content-type allowlist + UUID-based storage paths
|
||||
8. **Production hardening:** Error details hidden (`server.error.include-message=never`), Swagger disabled, minimal actuator exposure
|
||||
9. **Session security:** `httpOnly=true`, `sameSite=strict`, 30min timeout, max 1 concurrent session
|
||||
10. **Secure error messages:** `GlobalExceptionHandler` returns generic messages, no stack traces
|
||||
|
||||
### Areas for Improvement 📋
|
||||
|
||||
1. Add explicit login rate limiting (Bucket4j or similar)
|
||||
2. Sanitize original filename in `DocumentService`
|
||||
3. Upgrade Spring Boot to 4.0.7
|
||||
4. Make CORS origins environment-configurable
|
||||
5. Add Content-Security-Policy headers
|
||||
6. Consider adding request signing for webhook endpoints (`/api/v1/webhooks/**`)
|
||||
|
||||
---
|
||||
|
||||
## 5. Compliance Notes
|
||||
|
||||
| Standard | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| OWASP Top 10 (2021) | ✅ Good | No A01-A10 critical findings |
|
||||
| DSGVO/GDPR | ✅ Good | PII minimization in authority export (anonymized member list), audit trail |
|
||||
| §147 AO (Aufbewahrung) | ✅ Good | Append-only ledger, no deletion of financial records |
|
||||
| KCanG (Cannabis law) | ✅ Good | Compliance deadlines, quantity tracking, authority reporting |
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary Table
|
||||
|
||||
| Severity | Count | Action Required |
|
||||
|----------|-------|-----------------|
|
||||
| Critical | 0 | — |
|
||||
| High | 0 | — |
|
||||
| Medium | 3 | Fix in next sprint (path traversal sanitization, JWT fallback guard, Spring Boot upgrade) |
|
||||
| Low | 3 | Advisory — address when convenient |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
### ✅ PASS
|
||||
|
||||
No Critical or High severity findings. The application demonstrates strong security architecture with proper authentication, authorization, tenant isolation, and audit logging. The 3 Medium findings are defense-in-depth improvements, not actively exploitable vulnerabilities.
|
||||
|
||||
**Recommended next actions (priority order):**
|
||||
1. `mvn versions:set -DnewVersion=...` — upgrade Spring Boot to 4.0.7
|
||||
2. Add `Paths.get(filename).getFileName().toString()` sanitization in DocumentService
|
||||
3. Add startup validation that rejects the default JWT secret in production profile
|
||||
Reference in New Issue
Block a user