fix: update l1Category method to return 'Övrigt' for empty categoryPath
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,729 @@
|
|||||||
|
# 🔒 Säkerhetshärdningsplan för Recipe-App (Flutter + NestJS + MariaDB)
|
||||||
|
|
||||||
|
**Mål:** Täppa till IDOR, Full Table Dump, och andra kritiska säkerhetshål i **backend (NestJS/Prisma/MariaDB)**, **Flutter-frontend**, och **infrastruktur (Docker/Gitea/Ubuntu)**.
|
||||||
|
|
||||||
|
**Prioritet:** CRITICAL → HIGH → MEDIUM
|
||||||
|
**Tidsuppskattning:** 1–3 dagar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 **0. Förberedelser**
|
||||||
|
|
||||||
|
### 0.1. Miljö och verktyg
|
||||||
|
|
||||||
|
- **Installera säkerhetsverktyg:**
|
||||||
|
```bash
|
||||||
|
# Scanna efter läckta hemligheter i Git
|
||||||
|
npm install -g gitleaks
|
||||||
|
gitleaks detect --source . --report-path gitleaks-report.json
|
||||||
|
|
||||||
|
# Scanna Docker-containers
|
||||||
|
docker scan --file Dockerfile
|
||||||
|
```
|
||||||
|
- **Skapa en `security-audit`-gren:**
|
||||||
|
```bash
|
||||||
|
git checkout -b security-audit-$(date +%Y%m%d)
|
||||||
|
```
|
||||||
|
- **Dokumentera nuvarande säkerhetsstatus:**
|
||||||
|
- Lista alla **API-endpoints** (särskilt de som hanterar användardata: `recipes`, `inventory`, `users`).
|
||||||
|
- Lista alla **databastabeller** och deras innehåll.
|
||||||
|
- Lista alla **webhooks** (t.ex. Gitea).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **1. IDOR (Insecure Direct Object Reference) – CRITICAL**
|
||||||
|
|
||||||
|
**Mål:** Se till att användare endast kan komma åt sina egna resurser via API:er.
|
||||||
|
|
||||||
|
### 1.1. Backend (NestJS/Prisma)
|
||||||
|
|
||||||
|
#### **Steg 1: Lägg till ägarskapskontroll i alla endpoints**
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **NestJS-guard** som automatiskt kontrollerar att den inloggade användaren äger resursen (t.ex. `Recipe`, `Inventory`). Använd `req.user.id` för att jämföra med `resource.userId`. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/guards/ownership.guard.ts
|
||||||
|
> import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
> import { Reflector } from '@nestjs/core';
|
||||||
|
>
|
||||||
|
> @Injectable()
|
||||||
|
> export class OwnershipGuard implements CanActivate {
|
||||||
|
> constructor(private reflector: Reflector) {}
|
||||||
|
>
|
||||||
|
> canActivate(context: ExecutionContext): boolean {
|
||||||
|
> const requiredResource = this.reflector.get<string>('resource', context.getHandler());
|
||||||
|
> if (!requiredResource) return true; // Ingen resurs specificerad = ingen kontroll
|
||||||
|
>
|
||||||
|
> const request = context.switchToHttp().getRequest();
|
||||||
|
> const resourceId = request.params.id;
|
||||||
|
> const userId = request.user.id;
|
||||||
|
>
|
||||||
|
> // Hämta resursen och kontrollera ägarskap
|
||||||
|
> // (Anta att vi har en service för detta)
|
||||||
|
> const resource = await request[requiredResource + 'Service'].findOne(resourceId);
|
||||||
|
> if (!resource || resource.userId !== userId) {
|
||||||
|
> throw new ForbiddenException('Du har inte tillgång till denna resurs.');
|
||||||
|
> }
|
||||||
|
> return true;
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Registrera guarden globalt i `app.module.ts` eller använd den på specifika routes med `@UseGuards(OwnershipGuard)`."
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Uppdatera alla **controller-metoder** som hanterar `Recipe`, `Inventory`, eller `User` för att använda `@UseGuards(OwnershipGuard)` och `@SetMetadata('resource', 'recipe')` (eller motsvarande). Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/recipes/recipes.controller.ts
|
||||||
|
> import { Controller, Get, Param, UseGuards, SetMetadata } from '@nestjs/common';
|
||||||
|
> import { OwnershipGuard } from '../guards/ownership.guard';
|
||||||
|
>
|
||||||
|
> @Controller('recipes')
|
||||||
|
> @UseGuards(OwnershipGuard)
|
||||||
|
> export class RecipesController {
|
||||||
|
> @Get(':id')
|
||||||
|
> @SetMetadata('resource', 'recipe')
|
||||||
|
> async getRecipe(@Param('id') id: number) {
|
||||||
|
> return this.recipesService.findOne(id);
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
#### **Steg 2: Prisma-middleware för automatisk filtrering**
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **Prisma-middleware** som automatiskt lägger till `userId: req.user.id` till alla `findMany`, `findFirst`, `update`, och `delete`-förfrågningar för tabellerna `Recipe` och `Inventory`. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/prisma/prisma.service.ts
|
||||||
|
> import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
> import { PrismaClient } from '@prisma/client';
|
||||||
|
>
|
||||||
|
> @Injectable()
|
||||||
|
> export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
> private userId: number;
|
||||||
|
>
|
||||||
|
> setUserId(userId: number) {
|
||||||
|
> this.userId = userId;
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> async onModuleInit() {
|
||||||
|
> await this.$connect();
|
||||||
|
> this.$use(async (params, next) => {
|
||||||
|
> if (this.userId && ['Recipe', 'Inventory'].includes(params.model)) {
|
||||||
|
> params.where = { ...params.where, userId: this.userId };
|
||||||
|
> }
|
||||||
|
> return next(params);
|
||||||
|
> });
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Uppdatera sedan `OwnershipGuard` för att sätta `prismaService.setUserId(req.user.id)`."
|
||||||
|
|
||||||
|
#### **Steg 3: Testa för IDOR**
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa **Jest-tester** för att verifiera att IDOR-skyddet fungerar. Testa:
|
||||||
|
>
|
||||||
|
> 1. En användare kan hämta sina egna recept/inventory.
|
||||||
|
> 2. En användare **kan inte** hämta andras recept/inventory.
|
||||||
|
>
|
||||||
|
> Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // test/idor.test.ts
|
||||||
|
> describe('IDOR Protection', () => {
|
||||||
|
> let app;
|
||||||
|
> let user1, user2, recipe1;
|
||||||
|
>
|
||||||
|
> beforeAll(async () => {
|
||||||
|
> app = await createTestApp();
|
||||||
|
> user1 = await createTestUser();
|
||||||
|
> user2 = await createTestUser();
|
||||||
|
> recipe1 = await createTestRecipe(user1.id);
|
||||||
|
> });
|
||||||
|
>
|
||||||
|
> it('should allow user to access their own recipe', async () => {
|
||||||
|
> const response = await request(app.getHttpServer())
|
||||||
|
> .get(`/recipes/${recipe1.id}`)
|
||||||
|
> .set('Authorization', `Bearer ${user1.token}`);
|
||||||
|
> expect(response.status).toBe(200);
|
||||||
|
> });
|
||||||
|
>
|
||||||
|
> it('should block user from accessing another user\'s recipe', async () => {
|
||||||
|
> const response = await request(app.getHttpServer())
|
||||||
|
> .get(`/recipes/${recipe1.id}`)
|
||||||
|
> .set('Authorization', `Bearer ${user2.token}`);
|
||||||
|
> expect(response.status).toBe(403);
|
||||||
|
> });
|
||||||
|
> });
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗃️ **2. Full Table Dump – CRITICAL**
|
||||||
|
|
||||||
|
**Mål:** Förhindra att användare kan hämta alla rader från en tabell.
|
||||||
|
|
||||||
|
### 2.1. Backend (Prisma/NestJS)
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Uppdatera alla **Prisma-förfrågningar** som använder `findMany` för `Recipe`, `Inventory`, eller `User` för att **alltid** inkludera `where: { userId: req.user.id }`. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/recipes/recipes.service.ts
|
||||||
|
> async findAll(userId: number) {
|
||||||
|
> return this.prisma.recipe.findMany({
|
||||||
|
> where: { userId } // Tvinga filtrering
|
||||||
|
> });
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 2.2. MariaDB (RLS-liknande kontroll)
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa **MariaDB-vyer** för `recipes` och `inventory` som automatiskt filtrerar på `userId`. Exempel:
|
||||||
|
>
|
||||||
|
> ```sql
|
||||||
|
> -- Skapa en vy för användarens recept
|
||||||
|
> CREATE VIEW user_recipes AS
|
||||||
|
> SELECT * FROM recipes WHERE userId = @current_user_id;
|
||||||
|
>
|
||||||
|
> -- Skapa en stored procedure för att sätta @current_user_id
|
||||||
|
> DELIMITER //
|
||||||
|
> CREATE PROCEDURE SetCurrentUserId(IN p_userId INT)
|
||||||
|
> BEGIN
|
||||||
|
> SET @current_user_id = p_userId;
|
||||||
|
> END //
|
||||||
|
> DELIMITER ;
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> **Obs:** MariaDB saknar inbyggt RLS, så vyer + stored procedures är det bästa alternativet. Dokumentera att alla förfrågningar ska gå via vyer."
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **stored procedure** för att hämta användarens inventory, med `userId` som parameter. Exempel:
|
||||||
|
>
|
||||||
|
> ```sql
|
||||||
|
> DELIMITER //
|
||||||
|
> CREATE PROCEDURE GetUserInventory(IN p_userId INT)
|
||||||
|
> BEGIN
|
||||||
|
> SELECT * FROM inventory WHERE userId = p_userId;
|
||||||
|
> END //
|
||||||
|
> DELIMITER ;
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 2.3. Testa för Full Table Dump
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa **Jest-tester** för att verifiera att användare inte kan hämta alla rader. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> it('should block full table dump for recipes', async () => {
|
||||||
|
> const user = await createTestUser();
|
||||||
|
> const response = await request(app.getHttpServer())
|
||||||
|
> .get('/recipes')
|
||||||
|
> .set('Authorization', `Bearer ${user.token}`);
|
||||||
|
> expect(response.body.length).toBeLessThanOrEqual(10); // Anta att användaren har <10 recept
|
||||||
|
> });
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 **3. Flutter-Säkerhet – HIGH**
|
||||||
|
|
||||||
|
**Mål:** Säkra kommunikation mellan Flutter och backend, samt hantering av känsliga data.
|
||||||
|
|
||||||
|
### 3.1. API-Anrop
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **Dart-klass** för säkra API-anrop till backend, med:
|
||||||
|
>
|
||||||
|
> - **JWT-autentisering** (skicka `Authorization: Bearer <token>`).
|
||||||
|
> - **Validering av svar** (kontrollera att `userId` i svaret matchar den inloggade användaren).
|
||||||
|
> - **Felhantering** för 403 (Forbidden) och 401 (Unauthorized).
|
||||||
|
> Exempel:
|
||||||
|
>
|
||||||
|
> ```dart
|
||||||
|
> // lib/services/api_service.dart
|
||||||
|
> import 'package:http/http.dart' as http;
|
||||||
|
> import 'dart:convert';
|
||||||
|
>
|
||||||
|
> class ApiService {
|
||||||
|
> final String baseUrl;
|
||||||
|
> final String token;
|
||||||
|
>
|
||||||
|
> ApiService({required this.baseUrl, required this.token});
|
||||||
|
>
|
||||||
|
> Future<Map<String, dynamic>> getRecipe(int recipeId) async {
|
||||||
|
> final response = await http.get(
|
||||||
|
> Uri.parse('$baseUrl/recipes/$recipeId'),
|
||||||
|
> headers: {'Authorization': 'Bearer $token'},
|
||||||
|
> );
|
||||||
|
>
|
||||||
|
> if (response.statusCode == 403) {
|
||||||
|
> throw Exception('Du har inte tillgång till detta recept.');
|
||||||
|
> } else if (response.statusCode != 200) {
|
||||||
|
> throw Exception('Fel vid hämtning av recept.');
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> final data = json.decode(response.body);
|
||||||
|
> // Kontrollera att användar-ID matchar (om backend skickar med det)
|
||||||
|
> if (data['userId'] != await _getCurrentUserId()) {
|
||||||
|
> throw Exception('Ogiltig användare för detta recept.');
|
||||||
|
> }
|
||||||
|
> return data;
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> Future<int> _getCurrentUserId() async {
|
||||||
|
> // Hämta den inloggade användarens ID från lokal lagring
|
||||||
|
> final user = await _getStoredUser();
|
||||||
|
> return user['id'];
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 3.2. Lagring av känsliga data
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **säker lagringslösning** för JWT-tokens och användardata i Flutter med `flutter_secure_storage`. Exempel:
|
||||||
|
>
|
||||||
|
> ```dart
|
||||||
|
> // lib/services/secure_storage.dart
|
||||||
|
> import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
>
|
||||||
|
> class SecureStorage {
|
||||||
|
> final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||||
|
>
|
||||||
|
> Future<void> saveToken(String token) async {
|
||||||
|
> await _storage.write(key: 'auth_token', value: token);
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> Future<String?> getToken() async {
|
||||||
|
> return await _storage.read(key: 'auth_token');
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> Future<void> deleteToken() async {
|
||||||
|
> await _storage.delete(key: 'auth_token');
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 3.3. Inputvalidering
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **valideringsklass** för all input i Flutter (t.ex. för recept-ID, användar-ID). Exempel:
|
||||||
|
>
|
||||||
|
> ```dart
|
||||||
|
> // lib/utils/validators.dart
|
||||||
|
> class Validators {
|
||||||
|
> static bool isValidId(String id) {
|
||||||
|
> return RegExp(r'^[0-9]+$').hasMatch(id);
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> static bool isValidUuid(String uuid) {
|
||||||
|
> return RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$').hasMatch(uuid);
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 **4. Webhook-Säkerhet – HIGH**
|
||||||
|
|
||||||
|
**Mål:** Validera signaturer för alla inkommande webhooks (t.ex. Gitea).
|
||||||
|
|
||||||
|
### 4.1. Gitea-Webhooks
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **NestJS-controller** för Gitea-webhooks som validerar `X-Gitea-Signature`-headern. Använd `crypto` för att verifiera HMAC-SHA256-signaturen. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/webhooks/gitea.controller.ts
|
||||||
|
> import { Controller, Post, Headers, Body, UnauthorizedException } from '@nestjs/common';
|
||||||
|
> import * as crypto from 'crypto';
|
||||||
|
>
|
||||||
|
> @Controller('webhooks/gitea')
|
||||||
|
> export class GiteaController {
|
||||||
|
> @Post()
|
||||||
|
> async handleWebhook(
|
||||||
|
> @Headers('X-Gitea-Signature') signature: string,
|
||||||
|
> @Body() body: any,
|
||||||
|
> ) {
|
||||||
|
> const secret = process.env.GITEA_WEBHOOK_SECRET;
|
||||||
|
> const hmac = crypto.createHmac('sha256', secret);
|
||||||
|
> hmac.update(JSON.stringify(body));
|
||||||
|
> const expectedSignature = `sha256=${hmac.digest('hex')}`;
|
||||||
|
>
|
||||||
|
> if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
||||||
|
> throw new UnauthorizedException('Ogiltig signatur');
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // Hantera webhook-händelsen (t.ex. triggera backup)
|
||||||
|
> return { status: 'success' };
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 4.2. Miljövariabler
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Lägg till `GITEA_WEBHOOK_SECRET` i `.env.example` och dokumentera att den **måste** sättas i produktion. Exempel:
|
||||||
|
>
|
||||||
|
> ```env
|
||||||
|
> # .env.example
|
||||||
|
> GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret_here
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 **5. Hemligheter och Kryptering – HIGH**
|
||||||
|
|
||||||
|
**Mål:** Skydda API-nycklar, databaslösenord, och krypteringsnycklar.
|
||||||
|
|
||||||
|
### 5.1. `.env`-filer
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en `**.gitignore`-fil** som utesluter alla `.env`-filer och känsliga konfigurationsfiler. Exempel:
|
||||||
|
>
|
||||||
|
> ```gitignore
|
||||||
|
> # .gitignore
|
||||||
|
> .env
|
||||||
|
> .env.*
|
||||||
|
> !.env.example
|
||||||
|
> *.pem
|
||||||
|
> *.key
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en `**.env.example`-fil** med alla nödvändiga miljövariabler (utan värden). Exempel:
|
||||||
|
>
|
||||||
|
> ```env
|
||||||
|
> # .env.example
|
||||||
|
> DATABASE_URL=mysql://user:password@localhost:3306/recipe_app
|
||||||
|
> JWT_SECRET=your_jwt_secret_here
|
||||||
|
> GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret_here
|
||||||
|
> PGP_PRIVATE_KEY_ENCRYPTION_PASSWORD=your_password_here
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 5.2. Docker
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Uppdatera `Dockerfile` och `docker-compose.yml` för att:
|
||||||
|
>
|
||||||
|
> 1. **Ta bort alla `ENV`-instruktioner** som exponerar känsliga data.
|
||||||
|
> 2. Använd `docker run --env-file` eller Docker Secrets för känsliga variabler.
|
||||||
|
>
|
||||||
|
> Exempel:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # docker-compose.yml
|
||||||
|
> services:
|
||||||
|
> app:
|
||||||
|
> build: .
|
||||||
|
> env_file:
|
||||||
|
> - .env
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en `**docker-compose.override.yml**` för lokal utveckling, som aldrig pushas till Git. Exempel:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # docker-compose.override.yml
|
||||||
|
> services:
|
||||||
|
> app:
|
||||||
|
> environment:
|
||||||
|
> - NODE_ENV=development
|
||||||
|
> - DATABASE_URL=mysql://dev_user:dev_password@db:3306/recipe_app
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 5.3. PGP/AES-Kryptering
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **tjänst för kryptering/avkryptering** av PGP-nycklar med `openpgp`. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/crypto/crypto.service.ts
|
||||||
|
> import { Injectable } from '@nestjs/common';
|
||||||
|
> import * as openpgp from 'openpgp';
|
||||||
|
>
|
||||||
|
> @Injectable()
|
||||||
|
> export class CryptoService {
|
||||||
|
> async encryptPrivateKey(privateKey: string, password: string): Promise<string> {
|
||||||
|
> const encrypted = await openpgp.encrypt({
|
||||||
|
> message: openpgp.Message.fromText(privateKey),
|
||||||
|
> passwords: [password],
|
||||||
|
> });
|
||||||
|
> return encrypted.toString();
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> async decryptPrivateKey(encryptedPrivateKey: string, password: string): Promise<string> {
|
||||||
|
> const message = await openpgp.readMessage({ armoredMessage: encryptedPrivateKey });
|
||||||
|
> const { data: decrypted } = await openpgp.decrypt({
|
||||||
|
> message,
|
||||||
|
> passwords: [password],
|
||||||
|
> });
|
||||||
|
> return decrypted.toString();
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Uppdatera **Prisma-schemat** för att lagra `encryptedPrivateKey` istället för `privateKey` i klart text. Exempel:
|
||||||
|
>
|
||||||
|
> ```prisma
|
||||||
|
> model User {
|
||||||
|
> id Int @id @default(autoincrement())
|
||||||
|
> encryptedPrivateKey String // Krypterad PGP-privat nyckel
|
||||||
|
> // ...
|
||||||
|
> }
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ **6. Säkerhetsheaders och CSP – MEDIUM**
|
||||||
|
|
||||||
|
**Mål:** Skydda backend med säkerhetsheaders.
|
||||||
|
|
||||||
|
### 6.1. NestJS (Backend)
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Konfigurera **Helmet** i `main.ts` för att lägga till säkerhetsheaders. Exempel:
|
||||||
|
>
|
||||||
|
> ```typescript
|
||||||
|
> // src/main.ts
|
||||||
|
> import { NestFactory } from '@nestjs/core';
|
||||||
|
> import { AppModule } from './app.module';
|
||||||
|
> import * as helmet from 'helmet';
|
||||||
|
>
|
||||||
|
> async function bootstrap() {
|
||||||
|
> const app = await NestFactory.create(AppModule);
|
||||||
|
> app.use(helmet());
|
||||||
|
> await app.listen(3000);
|
||||||
|
> }
|
||||||
|
> bootstrap();
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 **7. Automatiserade Säkerhetstester – MEDIUM**
|
||||||
|
|
||||||
|
**Mål:** Integrera säkerhetstester i CI/CD.
|
||||||
|
|
||||||
|
### 7.1. GitHub Actions
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **GitHub Actions-workflow** för säkerhetskontroller. Exempel:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # .github/workflows/security-audit.yml
|
||||||
|
> name: Security Audit
|
||||||
|
> on: [push, pull_request]
|
||||||
|
>
|
||||||
|
> jobs:
|
||||||
|
> gitleaks:
|
||||||
|
> runs-on: ubuntu-latest
|
||||||
|
> steps:
|
||||||
|
> - uses: actions/checkout@v4
|
||||||
|
> - uses: gitleaks/gitleaks-action@v2
|
||||||
|
> env:
|
||||||
|
> GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
>
|
||||||
|
> docker-scan:
|
||||||
|
> runs-on: ubuntu-latest
|
||||||
|
> steps:
|
||||||
|
> - uses: actions/checkout@v4
|
||||||
|
> - name: Build Docker image
|
||||||
|
> run: docker build -t recipe-app .
|
||||||
|
> - name: Scan for vulnerabilities
|
||||||
|
> run: docker scan recipe-app
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 7.2. OWASP ZAP
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en **GitHub Actions-workflow** för OWASP ZAP-scanning. Exempel:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # .github/workflows/zap-scan.yml
|
||||||
|
> name: OWASP ZAP Scan
|
||||||
|
> on: [push]
|
||||||
|
>
|
||||||
|
> jobs:
|
||||||
|
> zap_scan:
|
||||||
|
> runs-on: ubuntu-latest
|
||||||
|
> steps:
|
||||||
|
> - uses: actions/checkout@v4
|
||||||
|
> - name: Start app
|
||||||
|
> run: |
|
||||||
|
> docker-compose up -d
|
||||||
|
> sleep 30 # Vänta på att appen ska starta
|
||||||
|
> - name: Run OWASP ZAP
|
||||||
|
> uses: zaproxy/action-full-scan@v0.4.0
|
||||||
|
> with:
|
||||||
|
> target: 'http://localhost:3000'
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 **8. Dokumentation**
|
||||||
|
|
||||||
|
**Uppgift för Copilot:**
|
||||||
|
|
||||||
|
> "Skapa en `**SECURITY.md**`-fil i rotkatalogen med:
|
||||||
|
>
|
||||||
|
> 1. En lista över alla säkerhetsåtgärder som vidtagits.
|
||||||
|
> 2. Instruktioner för hur man rapporterar säkerhetshål.
|
||||||
|
> 3. En checklist för säkerhetsgranskning före deployment.
|
||||||
|
>
|
||||||
|
> Exempel:
|
||||||
|
>
|
||||||
|
> ```markdown
|
||||||
|
> # 🔒 Säkerhetsdokumentation
|
||||||
|
>
|
||||||
|
> ## Vidtagna åtgärder
|
||||||
|
> - [x] IDOR-skydd för alla API-endpoints
|
||||||
|
> - [x] Full Table Dump-skydd via Prisma-middleware
|
||||||
|
> - [x] Webhook-signaturvalidering för Gitea
|
||||||
|
> - [x] `.env`-filer uteslutna från Git
|
||||||
|
> - [x] Flutter: Säker lagring av JWT-tokens
|
||||||
|
>
|
||||||
|
> ## Rapportera säkerhetshål
|
||||||
|
> Skicka ett e-post till security@recipe-app.com.
|
||||||
|
>
|
||||||
|
> ## Checklist före deployment
|
||||||
|
> - [ ] Alla `.env`-filer är uteslutna från Git.
|
||||||
|
> - [ ] Alla API-endpoints har ägarskapskontroll.
|
||||||
|
> - [ ] Webhook-signaturer valideras.
|
||||||
|
> - [ ] Docker-containers är scannade för sårbarheter.
|
||||||
|
> - [ ] Flutter-app använder `flutter_secure_storage` för tokens.
|
||||||
|
> ```"
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ **9. Avslutande Checklista**
|
||||||
|
|
||||||
|
|
||||||
|
| **Åtgärd** | **Status** | **Ansvarsområde** |
|
||||||
|
| -------------------------------- | ---------- | ----------------------- |
|
||||||
|
| IDOR-skydd i alla endpoints | ⬜ | Backend (NestJS/Prisma) |
|
||||||
|
| Full Table Dump-skydd | ⬜ | Backend (Prisma) |
|
||||||
|
| Flutter: Säkra API-anrop | ⬜ | Flutter |
|
||||||
|
| Flutter: Säker lagring av tokens | ⬜ | Flutter |
|
||||||
|
| Webhook-signaturvalidering | ⬜ | Backend (NestJS) |
|
||||||
|
| `.env`-filer skyddade | ⬜ | Git/Docker |
|
||||||
|
| Docker-säkerhet | ⬜ | Docker |
|
||||||
|
| PGP-nycklar krypterade | ⬜ | Backend (CryptoService) |
|
||||||
|
| Säkerhetsheaders (Helmet) | ⬜ | Backend (NestJS) |
|
||||||
|
| Automatiserade säkerhetstester | ⬜ | CI/CD (GitHub Actions) |
|
||||||
|
| Säkerhetsdokumentation | ⬜ | Dokumentation |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **10. Prioriterad Ordning för Implementering**
|
||||||
|
|
||||||
|
1. **IDOR-skydd** (CRITICAL)
|
||||||
|
2. **Full Table Dump-skydd** (CRITICAL)
|
||||||
|
3. **Flutter-säkerhet** (HIGH)
|
||||||
|
4. **Webhook-säkerhet** (HIGH)
|
||||||
|
5. **Hemligheter och kryptering** (HIGH)
|
||||||
|
6. **Säkerhetsheaders** (MEDIUM)
|
||||||
|
7. **Automatiserade tester** (MEDIUM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 **11. Tips för GitHub Copilot**
|
||||||
|
|
||||||
|
- **Var specifik:** Beskriv exakt vad du vill uppnå (t.ex. "Skapa en NestJS-guard för ägarskapskontroll").
|
||||||
|
- **Ge exempel:** Inkludera kodsnuttar för att visa vad du menar.
|
||||||
|
- **Be om tester:** Fråga Copilot att skapa **Jest-tester** för varje säkerhetsåtgärd.
|
||||||
|
- **Iterera:** Om svaret inte är perfekt, be Copilot att **förbättra** eller **förtydliga** koden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 **12. Nästa Steg**
|
||||||
|
|
||||||
|
1. **Börja med IDOR-skyddet** (steg 1.1–1.3).
|
||||||
|
2. **Gå vidare till Full Table Dump-skydd** (steg 2.1–2.3).
|
||||||
|
3. **Implementera Flutter-säkerhet** (steg 3.1–3.3).
|
||||||
|
4. **Säkra webhooks** (steg 4.1–4.2).
|
||||||
|
5. **Säkra hemligheter** (steg 5.1–5.3).
|
||||||
|
6. **Lägg till säkerhetsheaders** (steg 6.1).
|
||||||
|
7. **Automatisera tester** (steg 7.1–7.2).
|
||||||
|
8. **Dokumentera** (steg 8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fråga till dig:**
|
||||||
|
Vill du att jag **anpassar planen ytterligare** för ett specifikt område (t.ex. endast backend eller endast Flutter)? Eller ska vi börja med **IDOR-skyddet** först?
|
||||||
@@ -38,7 +38,7 @@ class InventoryItem {
|
|||||||
|
|
||||||
String get l1Category {
|
String get l1Category {
|
||||||
final path = categoryPath?.trim();
|
final path = categoryPath?.trim();
|
||||||
if (path == null || path.isEmpty) return 'Ovrigt';
|
if (path == null || path.isEmpty) return 'Övrigt';
|
||||||
return path.split('>').first.trim();
|
return path.split('>').first.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user