diff --git a/messages/de.json b/messages/de.json
index 518d086..d3157a0 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -252,10 +252,11 @@
"linkMeasure": "Maßnahme verknüpfen",
"newMeasure": "Neue Maßnahme anlegen & verknüpfen",
"measureTitle": "Titel der Maßnahme",
- "reductionL": "Minderung Wahrscheinlichkeit (0–4)",
- "reductionI": "Minderung Schaden (0–4)",
+ "reductionL": "Minderung Wahrscheinlichkeit (0,00–4,00)",
+ "reductionI": "Minderung Schaden (0,00–4,00)",
"reduction": "Minderung",
- "createFromAsset": "Risiko erstellen"
+ "createFromAsset": "Risiko erstellen",
+ "catalogHint": "Aus Katalog wählen oder frei eingeben …"
},
"riskTreatment": {
"AVOID": "Vermeiden",
diff --git a/messages/en.json b/messages/en.json
index 3c486d9..9d08def 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -252,10 +252,11 @@
"linkMeasure": "Link measure",
"newMeasure": "Create & link new measure",
"measureTitle": "Measure title",
- "reductionL": "Reduction likelihood (0–4)",
- "reductionI": "Reduction damage (0–4)",
+ "reductionL": "Reduction likelihood (0.00–4.00)",
+ "reductionI": "Reduction damage (0.00–4.00)",
"reduction": "Reduction",
- "createFromAsset": "Create risk"
+ "createFromAsset": "Create risk",
+ "catalogHint": "Pick from catalog or type freely …"
},
"riskTreatment": {
"AVOID": "Avoid",
diff --git a/prisma/migrations/20260702144829_decimal_reductions_and_catalogs/migration.sql b/prisma/migrations/20260702144829_decimal_reductions_and_catalogs/migration.sql
new file mode 100644
index 0000000..0480c08
--- /dev/null
+++ b/prisma/migrations/20260702144829_decimal_reductions_and_catalogs/migration.sql
@@ -0,0 +1,32 @@
+-- AlterTable
+ALTER TABLE "risk_measures" ALTER COLUMN "reduction_likelihood" SET DEFAULT 0,
+ALTER COLUMN "reduction_likelihood" SET DATA TYPE DOUBLE PRECISION,
+ALTER COLUMN "reduction_impact" SET DEFAULT 0,
+ALTER COLUMN "reduction_impact" SET DATA TYPE DOUBLE PRECISION;
+
+-- AlterTable
+ALTER TABLE "risks" ALTER COLUMN "residual_likelihood" SET DATA TYPE DOUBLE PRECISION,
+ALTER COLUMN "residual_impact" SET DATA TYPE DOUBLE PRECISION,
+ALTER COLUMN "residual_score" SET DATA TYPE DOUBLE PRECISION;
+
+-- CreateTable
+CREATE TABLE "threats" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "threats_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "vulnerabilities" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "vulnerabilities_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "threats_name_key" ON "threats"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "vulnerabilities_name_key" ON "vulnerabilities"("name");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index a73f817..d432c8b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -279,10 +279,10 @@ model Risk {
impact Int @default(1)
score Int @default(1) // redundant für Listen/Heatmap
- // Rest-Risiko nach Maßnahmen
- residualLikelihood Int? @map("residual_likelihood")
- residualImpact Int? @map("residual_impact")
- residualScore Int? @map("residual_score")
+ // Rest-Risiko nach Maßnahmen (berechnet, dezimal wegen Teil-Minderungen)
+ residualLikelihood Float? @map("residual_likelihood")
+ residualImpact Float? @map("residual_impact")
+ residualScore Float? @map("residual_score")
treatment RiskTreatment @default(MITIGATE)
status RiskStatus @default(OPEN)
@@ -366,9 +366,10 @@ model RiskMeasure {
riskId String @map("risk_id")
measureId String @map("measure_id")
- // Minderung in Stufen (0–4) je Dimension
- reductionLikelihood Int @default(0) @map("reduction_likelihood")
- reductionImpact Int @default(0) @map("reduction_impact")
+ // Minderung je Dimension (0,00–4,00) — mehrere Maßnahmen summieren sich,
+ // bis ein voller Punkt (oder mehr) erreicht ist
+ reductionLikelihood Float @default(0) @map("reduction_likelihood")
+ reductionImpact Float @default(0) @map("reduction_impact")
risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade)
measure Measure @relation(fields: [measureId], references: [id], onDelete: Cascade)
@@ -378,6 +379,23 @@ model RiskMeasure {
@@map("risk_measures")
}
+// Globale Kataloge (SPEC §5: Threat/Vulnerability, mandantenübergreifend).
+// Risiken speichern weiterhin Freitext — der Katalog liefert Vorschläge (datalist),
+// eigene Formulierungen bleiben möglich.
+model Threat {
+ id String @id @default(cuid())
+ name String @unique
+
+ @@map("threats")
+}
+
+model Vulnerability {
+ id String @id @default(cuid())
+ name String @unique
+
+ @@map("vulnerabilities")
+}
+
model AuditLog {
id String @id @default(cuid())
tenantId String @map("tenant_id")
diff --git a/prisma/seed.ts b/prisma/seed.ts
index d7e0078..6cee2b5 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -15,7 +15,52 @@ const prisma = new PrismaClient({
const DEMO_PASSWORD = "Demo1234!";
+const THREATS = [
+ "Schadsoftware / Ransomware",
+ "Phishing / Social Engineering",
+ "Innentäter / Missbrauch von Berechtigungen",
+ "Diebstahl oder Verlust von Geräten",
+ "Ausfall von IT-Systemen oder Diensten",
+ "Ausfall eines Dienstleisters / Lieferanten",
+ "Stromausfall / Infrastrukturausfall",
+ "Feuer / Wasser / Elementarschäden",
+ "Unbefugter physischer Zutritt",
+ "Denial-of-Service-Angriff",
+ "Datenabfluss / Industriespionage",
+ "Fehlbedienung durch Mitarbeitende",
+ "Softwarefehler / fehlerhafte Updates",
+ "Kompromittierte Zugangsdaten",
+ "Rechtliche / regulatorische Verstöße",
+];
+
+const VULNERABILITIES = [
+ "Fehlende oder veraltete Backups",
+ "Fehlende Netzwerksegmentierung",
+ "Unzureichendes Patch-Management",
+ "Schwache oder wiederverwendete Passwörter",
+ "Fehlende Multi-Faktor-Authentifizierung",
+ "Übermäßige Berechtigungen / fehlendes Least-Privilege",
+ "Fehlende Awareness / Schulungen",
+ "Unverschlüsselte Datenträger oder Übertragungen",
+ "Kein Notfallkonzept / ungetesteter Wiederanlauf",
+ "Single Point of Failure (Technik oder Person)",
+ "Unzureichende Protokollierung / Überwachung",
+ "Veraltete oder nicht mehr unterstützte Software",
+ "Fehlende Vertrags-/AV-Regelungen mit Dienstleistern",
+ "Offene, ungenutzte Dienste und Ports",
+ "Unklare Verantwortlichkeiten",
+];
+
async function main() {
+ // 0. Globale Kataloge Bedrohungen/Schwachstellen (Vorschläge, Freitext bleibt möglich)
+ for (const name of THREATS) {
+ await prisma.threat.upsert({ where: { name }, update: {}, create: { name } });
+ }
+ for (const name of VULNERABILITIES) {
+ await prisma.vulnerability.upsert({ where: { name }, update: {}, create: { name } });
+ }
+ console.log(`✔ Kataloge: ${THREATS.length} Bedrohungen, ${VULNERABILITIES.length} Schwachstellen`);
+
// 1. Global permission catalog
for (const key of PERMISSIONS) {
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
diff --git a/src/app/(app)/risks/page.tsx b/src/app/(app)/risks/page.tsx
index d7b2c15..86f448d 100644
--- a/src/app/(app)/risks/page.tsx
+++ b/src/app/(app)/risks/page.tsx
@@ -2,7 +2,7 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { Plus } from "lucide-react";
import { requireSession } from "@/server/auth";
-import { dbForTenant } from "@/server/db";
+import { dbForTenant, prisma } from "@/server/db";
import { hasPermission, requirePermission } from "@/server/rbac";
import { Button } from "@/components/ui/button";
import { PageHead, Pill, SectTitle, Tag } from "@/components/mockup-ui";
@@ -82,7 +82,7 @@ export default async function RisksPage({
: null;
const needsFormData = canWrite && (params.edit || params.new);
- const [users, processes] = needsFormData
+ const [users, processes, threats, vulnerabilities] = needsFormData
? await Promise.all([
db.user.findMany({
where: { status: "ACTIVE" },
@@ -90,8 +90,13 @@ export default async function RisksPage({
orderBy: { name: "asc" },
}),
db.process.findMany({ select: { id: true, name: true }, orderBy: { name: "asc" } }),
+ // Globale Kataloge (kein Mandanten-Bezug)
+ prisma.threat.findMany({ orderBy: { name: "asc" } }).then((r) => r.map((x) => x.name)),
+ prisma.vulnerability
+ .findMany({ orderBy: { name: "asc" } })
+ .then((r) => r.map((x) => x.name)),
])
- : [[], []];
+ : [[], [], [], []];
const availableAssets =
canWrite && params.edit && modalRisk
? await db.asset.findMany({
@@ -263,11 +268,19 @@ export default async function RisksPage({
processes={processes}
availableAssets={availableAssets}
availableMeasures={availableMeasures}
+ threats={threats}
+ vulnerabilities={vulnerabilities}
/>
) : modalRisk ? (
{t("measures")}
{t("residualAuto")}
{risk.riskMeasures.length === 0 &&{tc("none")}
} -