From d7e3ce3fb60a6ce555ca8f8f7e0bbd274c8f06fd Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Thu, 8 Jan 2026 10:20:24 +0100 Subject: [PATCH 01/23] Implement import for threat modeling files --- .gitignore | 1 + frontend/webEditor/package-lock.json | 9 +- .../commandPalette/commandPaletteProvider.ts | 6 + .../src/labels/ThreatModelingLabelType.ts | 10 + .../webEditor/src/labels/assignmentCommand.ts | 2 +- frontend/webEditor/src/serialize/di.config.ts | 2 + .../src/serialize/loadThreatModelingFile.ts | 197 ++++++++++++++++++ 7 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 frontend/webEditor/src/labels/ThreatModelingLabelType.ts create mode 100644 frontend/webEditor/src/serialize/loadThreatModelingFile.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..62c89355 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index eca35ef6..250ba4cf 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -1072,7 +1072,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -1297,7 +1296,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1654,7 +1652,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2564,8 +2561,7 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/resolve-from": { "version": "4.0.0", @@ -2888,7 +2884,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2948,7 +2943,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3090,7 +3084,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index ad0e53e2..81852425 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -10,6 +10,7 @@ import { LayoutMethod } from "../layout/layoutMethod"; import { LayoutModelAction } from "../layout/command"; import { SaveJsonFileAction } from "../serialize/saveJsonFile"; import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; +import { LoadThreatModelingFileAction } from "../serialize/loadThreatModelingFile.ts"; /** * Provides possible actions for the command palette. @@ -31,6 +32,11 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct [LoadPalladioFileAction.create(), commitAction], "fa-puzzle-piece", ), + new LabeledAction( + "Load Threat Modeling File (JSON)", + [LoadThreatModelingFileAction.create(), commitAction], + "fa-triangle-exclamation" + ) ], "go-to-file", ), diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts new file mode 100644 index 00000000..b9e5b0a6 --- /dev/null +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -0,0 +1,10 @@ +import { LabelType, LabelTypeValue } from "./LabelType.ts"; + +export interface ThreatModelingLabelType extends LabelType { + intendedFor: 'Vertex' | 'Flow' //TODO maybe stattdessen hier 'Node' und 'Edge' verwenden +} + +export interface ThreatModelingLabelTypeValue extends LabelTypeValue { + defaultPinBehavior: string, + additionalInformation: string[] +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/assignmentCommand.ts b/frontend/webEditor/src/labels/assignmentCommand.ts index 4a63efe5..4d305c98 100644 --- a/frontend/webEditor/src/labels/assignmentCommand.ts +++ b/frontend/webEditor/src/labels/assignmentCommand.ts @@ -147,7 +147,7 @@ export class LabelAssignmentCommand implements Command { } } -function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { +export function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { const elementsList: SModelElementImpl[] = []; for (const element of elements) { elementsList.push(element); diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index 2e34a49c..a1301ace 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -8,6 +8,7 @@ import { DfdModelFactory } from "./ModelFactory"; import { SaveJsonFileCommand } from "./saveJsonFile"; import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; import { AnalyzeCommand } from "./analyze"; +import { LoadThreatModelingFileCommand } from "./loadThreatModelingFile.ts"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -15,6 +16,7 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadJsonFileCommand); configureCommand(context, LoadDfdAndDdFileCommand); configureCommand(context, LoadPalladioFileCommand); + configureCommand(context, LoadThreatModelingFileCommand); configureCommand(context, SaveJsonFileCommand); configureCommand(context, SaveDfdAndDdFileCommand); configureCommand(context, AnalyzeCommand); diff --git a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts new file mode 100644 index 00000000..a2c310f0 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts @@ -0,0 +1,197 @@ +import { + Command, + CommandExecutionContext, + CommandReturn, + ILogger, + ISnapper, + SModelElementImpl, + SModelRootImpl, + SNodeImpl, + TYPES, +} from "sprotty"; +import { Action } from "sprotty-protocol"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; +import { ConstraintRegistry } from "../constraint/constraintRegistry.ts"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator.ts"; +import { chooseFile } from "./fileChooser.ts"; +import { inject } from "inversify"; +import { ThreatModelingLabelType, ThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; +import { LabelAssignment, LabelType, LabelTypeValue } from "../labels/LabelType.ts"; +import { Constraint } from "../constraint/Constraint.ts"; +import { getAllElements } from "../labels/assignmentCommand.ts"; +import { ContainsDfdLabels, containsDfdLabels } from "../labels/feature.ts"; +import { snapPortsOfNode } from "../diagram/ports/portSnapper.ts"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; + +// Replaces the type of the `values` of a `LabelType` with a subclass of `LabelTypeValue` +type OverwriteLabelTypeValueType = Omit & { values: S[] } + +type ThreatModelingFileFormat = { + threatKnowledgeName: string, + threatKnowledgeVersion: string, + labels: OverwriteLabelTypeValueType[], + constraints: Constraint[] +} + +export namespace LoadThreatModelingFileAction { + export const KIND = "loadThreatModelingFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadThreatModelingFileCommand extends Command { + static readonly KIND = LoadThreatModelingFileAction.KIND; + + private fileContent: ThreatModelingFileFormat | undefined; + + // UNDO / REDO storage + private oldLabelTypes: LabelType[] | undefined; + private oldLabelAssignments: Map = new Map(); + private oldOutputPortBehavior: Map = new Map(); + private newOutputPortBehavior: Map = new Map(); + private oldConstraints: Constraint[] | undefined; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) private logger: ILogger, + @inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry, + @inject(LoadingIndicator) private loadingIndicator: LoadingIndicator, + @inject(TYPES.ISnapper) private snapper: ISnapper + ) { + super(); + } + + private async getFileContent(): Promise { + const file = await chooseFile(["application/json"]); + if (!file) return undefined + + return JSON.parse(file.content) as ThreatModelingFileFormat; + } + + async execute(context: CommandExecutionContext): Promise { + this.loadingIndicator.showIndicator("Loading labels and constraints..."); + + const fileContent = await this.getFileContent() + if (!fileContent) return context.root; + + this.logger.info(this, "File loaded successfully.") + this.fileContent = fileContent; + + //Import labels + this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); + const newLabelTypes = this.fileContent.labels; + this.labelTypeRegistry.clearLabelTypes(); + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + + //Remove all old LabelAssignments + const allElements = getAllElements(context.root.children); + + const allDfdLabelElements = allElements + .filter((element) => containsDfdLabels(element)); + allDfdLabelElements.forEach(element => { + if (element.labels.length > 0) { + this.oldLabelAssignments.set(element, element.labels); + element.labels = []; + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + } + }); + this.logger.info(this, "Removed label assignments"); + + //Remove OutputPin Behavior except 'forward' + const allOutputPorts = allElements + .filter((element) => element instanceof DfdOutputPortImpl) + allOutputPorts.forEach(outputPort => { + const outputPortBehavior = outputPort.getBehavior() + + this.oldOutputPortBehavior.set(outputPort, outputPortBehavior); + + //Keep only 'forward' behavior, discard the rest + const match = outputPortBehavior.match(/^forward\s+\S+(?:\|\S+)*/); + const newBehavior = match ? match[0] : ""; + this.newOutputPortBehavior.set(outputPort, newBehavior); + outputPort.setBehavior(newBehavior); + }) + this.logger.info(this, "Updated output port behavior"); + + + //Import constraints + this.oldConstraints = this.constraintRegistry.getConstraintList(); + const newConstraints = this.fileContent.constraints; + this.constraintRegistry.clearConstraints(); + this.constraintRegistry.setConstraintsFromArray(newConstraints); + this.logger.info(this, "Constraints loaded successfully"); + + this.loadingIndicator.hide(); + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (!this.oldLabelTypes || !this.oldConstraints) return context.root; + + // LabelTypes and Labels + this.labelTypeRegistry.clearLabelTypes(); + this.labelTypeRegistry.setLabelTypes(this.oldLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + + // LabelAssignments + this.oldLabelAssignments.forEach((labels, element) => { + element.labels = labels; + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + }); + this.logger.info(this, "Label assignments restored"); + + //OutputPin Behavior + this.oldOutputPortBehavior.forEach((behavior, outputPort) => { + outputPort.setBehavior(behavior); + }) + this.logger.info(this, "Updated output port behavior"); + + // Constraints + this.constraintRegistry.clearConstraints(); + this.constraintRegistry.setConstraintsFromArray(this.oldConstraints); + this.logger.info(this, "Constraints loaded successfully"); + + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + const newLabelTypes = this.fileContent?.labels; + const newConstraints = this.fileContent?.constraints; + if (!newLabelTypes || !newConstraints) return context.root; + + // LabelTypes and Labels + this.labelTypeRegistry.clearLabelTypes(); + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + + // LabelAssignments + this.oldLabelAssignments.forEach((_, element) => { + element.labels = []; + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + }); + this.logger.info(this, "Label assignments restored"); + + //OutputPin Behavior + this.newOutputPortBehavior.forEach((behavior, outputPort) => { + outputPort.setBehavior(behavior); + }) + this.logger.info(this, "Updated output port behavior"); + + // Constraints + this.constraintRegistry.clearConstraints(); + this.constraintRegistry.setConstraintsFromArray(newConstraints); + this.logger.info(this, "Constraints loaded successfully"); + + return context.root; + } +} \ No newline at end of file From 8321faaaa9a9d47d64142bf193557759698076f7 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Thu, 8 Jan 2026 10:37:04 +0100 Subject: [PATCH 02/23] Implement export of violated constraints --- .../commandPalette/commandPaletteProvider.ts | 2 + frontend/webEditor/src/serialize/di.config.ts | 2 + .../src/serialize/saveThreatsTable.ts | 110 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 frontend/webEditor/src/serialize/saveThreatsTable.ts diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index 81852425..a46ea535 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -11,6 +11,7 @@ import { LayoutModelAction } from "../layout/command"; import { SaveJsonFileAction } from "../serialize/saveJsonFile"; import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; import { LoadThreatModelingFileAction } from "../serialize/loadThreatModelingFile.ts"; +import { SaveThreatsTableAction } from "../serialize/saveThreatsTable.ts"; /** * Provides possible actions for the command palette. @@ -46,6 +47,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct new LabeledAction("Save diagram as JSON", [SaveJsonFileAction.create()], "json"), new LabeledAction("Save diagram as DFD and DD", [SaveDfdAndDdFileAction.create()], "coffee"), //new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), + new LabeledAction("Save threats table", [SaveThreatsTableAction.create()], "fa-triangle-exclamation") ], "save", ), diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index a1301ace..6c0cac7f 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -9,6 +9,7 @@ import { SaveJsonFileCommand } from "./saveJsonFile"; import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; import { AnalyzeCommand } from "./analyze"; import { LoadThreatModelingFileCommand } from "./loadThreatModelingFile.ts"; +import { SaveThreatsTableCommand } from "./saveThreatsTable.ts"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -19,6 +20,7 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadThreatModelingFileCommand); configureCommand(context, SaveJsonFileCommand); configureCommand(context, SaveDfdAndDdFileCommand); + configureCommand(context, SaveThreatsTableCommand) configureCommand(context, AnalyzeCommand); rebind(TYPES.IModelFactory).to(DfdModelFactory); diff --git a/frontend/webEditor/src/serialize/saveThreatsTable.ts b/frontend/webEditor/src/serialize/saveThreatsTable.ts new file mode 100644 index 00000000..b11c7a3f --- /dev/null +++ b/frontend/webEditor/src/serialize/saveThreatsTable.ts @@ -0,0 +1,110 @@ +import { CommandExecutionContext, TYPES } from "sprotty"; +import { FileData } from "./loadJson"; +import { SaveFileCommand } from "./saveFile"; +import { EditorModeController } from "../settings/editorMode"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { Action } from "sprotty-protocol"; +import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; +import { getAllElements } from "../labels/assignmentCommand.ts"; +import { DfdNodeImpl } from "../diagram/nodes/common.ts"; +import { DfdNodeAnnotation } from "../annotation/DFDNodeAnnotation.ts"; + +const CSV_COLUMN_SEPARATOR = "," +const CSV_LINE_SEPARATOR = "\n" + +export namespace SaveThreatsTableAction { + export const KIND = "saveThreatsTable"; + export function create(): Action { + return { kind: KIND }; + } +} + +export class SaveThreatsTableCommand extends SaveFileCommand { + static readonly KIND = SaveThreatsTableAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super(LabelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); + } + + getFiles(context: CommandExecutionContext): Promise[]> { + const allDfdNodeElements = getAllElements(context.root.children) + .filter((elem) => elem instanceof DfdNodeImpl); + + const toExport: { nodeId: string; nodeText: string; violatedConstraint: string }[] = []; + for (const dfdNode of allDfdNodeElements) { + for (const annotation of dfdNode.annotations) { + if (!SaveThreatsTableCommand.isViolation(annotation)) { + continue; + } + + toExport.push({ + nodeId: dfdNode.id, + nodeText: dfdNode.text, + violatedConstraint: this.extractViolatedConstraintFromMessage(annotation.message), + }); + } + } + + const fileData: FileData = { + fileName: this.fileName.getName() + ".csv", + content: SaveThreatsTableCommand.toCSV(toExport), + }; + return Promise.resolve([fileData]); + } + + private static isViolation(annotation: DfdNodeAnnotation): boolean { + return annotation.message.includes("violated"); + } + + private extractViolatedConstraintFromMessage(message: string): string { + return message + .replace("Constraint ", "") + .replace(" violated", "") + } + + private static toCSV(array: T[]): string { + let csv = "" + + if (array.length == 0) return csv; + + //Header + for (const headerEntry of Object.keys(array[0])) { + csv += SaveThreatsTableCommand.escapeCSVEntry(headerEntry) + csv += CSV_COLUMN_SEPARATOR + } + csv += CSV_LINE_SEPARATOR + + //Content + for (const row of array) { + for (const entry of Object.values(row)) { + csv += SaveThreatsTableCommand.escapeCSVEntry(entry) + csv += CSV_COLUMN_SEPARATOR + } + csv += CSV_LINE_SEPARATOR + } + + return csv + } + + private static escapeCSVEntry(value: unknown): string { + if (value == null) return ""; + + const str = String(value) + if (/[",\n]/.test(str)) { + return `"${str.replace(/"/g, '""')}"`; + } + + return str; + } +} From 5df28766b6614af70c53854248fee2ba443b218e Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 9 Jan 2026 16:23:58 +0100 Subject: [PATCH 03/23] Implement labeling process UI --- frontend/webEditor/src/index.ts | 2 + .../src/labelingProcess/di.config.ts | 13 +++ .../labelingProcess/labelingProcessCommand.ts | 103 ++++++++++++++++++ .../src/labelingProcess/labelingProcessUI.css | 18 +++ .../src/labelingProcess/labelingProcessUi.ts | 91 ++++++++++++++++ .../webEditor/src/labels/LabelTypeEditorUi.ts | 13 +++ .../src/labels/labelTypeEditorUi.css | 24 ++++ 7 files changed, 264 insertions(+) create mode 100644 frontend/webEditor/src/labelingProcess/di.config.ts create mode 100644 frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts create mode 100644 frontend/webEditor/src/labelingProcess/labelingProcessUI.css create mode 100644 frontend/webEditor/src/labelingProcess/labelingProcessUi.ts diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index ce0f74b7..6305d770 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -25,6 +25,7 @@ import { constraintModule } from "./constraint/di.config"; import { assignmentModule } from "./assignment/di.config"; import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; import { loadingIndicatorModule } from "./loadingIndicator/di.config"; +import { labelingProcessModule } from "./labelingProcess/di.config.ts"; const container = new Container(); @@ -48,6 +49,7 @@ container.load( layoutModule, fileNameModule, settingsModule, + labelingProcessModule, toolPaletteModule, constraintModule, assignmentModule, diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts new file mode 100644 index 00000000..8978fc27 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; +import { configureCommand, TYPES } from "sprotty"; +import { LabelingProcessUi } from "./labelingProcessUi.ts"; +import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; +import { EDITOR_TYPES } from "../editorTypes.ts"; + +export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { + bind(LabelingProcessUi).toSelf().inSingletonScope(); + configureCommand({bind, isBound}, LabelingProcessCommand) + + bind(TYPES.IUIExtension).toService(LabelingProcessUi); + bind(EDITOR_TYPES.DefaultUIElement).to(LabelingProcessUi); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts new file mode 100644 index 00000000..8649f268 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -0,0 +1,103 @@ +import { inject, injectable } from "inversify"; +import { + Command, + CommandExecutionContext, + CommandReturn, + TYPES, +} from "sprotty"; +import { Action } from "sprotty-protocol"; +import { LabelingProcessState, LabelingProcessUi, LabelTypeValueWithLabelType } from "./labelingProcessUi.ts"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; +import { LabelType } from "../labels/LabelType.ts"; + +export interface LabelingProcessAction extends Action { + state: LabelingProcessState +} + +export namespace BeginLabelingProcessAction { + export function create( + labelTypeRegistry: LabelTypeRegistry + ): LabelingProcessAction { + const allLabels = transformLabelTypeArray(labelTypeRegistry.getLabelTypes()) + + return { + kind: LabelingProcessCommand.KIND, + state: { + state: 'inProgress', + finishedLabels: [], + activeLabel: allLabels [0] + } + } + } +} + +export namespace NextLabelingProcessAction { + export function create( + labelTypeRegistry: LabelTypeRegistry, + finishedLabels: LabelTypeValueWithLabelType[] + ): LabelingProcessAction { + const pendingLabels = transformLabelTypeArray(labelTypeRegistry.getLabelTypes()) + .filter( + (label) => !finishedLabels.some( + finishedLabel => finishedLabel.labelType === label.labelType && finishedLabel.labelTypeValue === label.labelTypeValue + ) + ) + + if (pendingLabels.length === 0) return CompleteLabelingProcessAction.create(); + + return { + kind: LabelingProcessCommand.KIND, + state: { + state: 'inProgress', + finishedLabels: finishedLabels, + activeLabel: pendingLabels[0] + } + } + } +} + +export namespace CompleteLabelingProcessAction { + export function create(): LabelingProcessAction { + return { + kind: LabelingProcessCommand.KIND, + state: { + state: 'done', + } + } + } +} + +function transformLabelTypeArray(labelTypes: LabelType[]): LabelTypeValueWithLabelType[] { + const transformed: LabelTypeValueWithLabelType[] = [] + for (const labelType of labelTypes) { + for (const labelTypeValue of labelType.values) { + transformed.push({ labelType, labelTypeValue }); + } + } + return transformed; +} + +@injectable() +export class LabelingProcessCommand implements Command { + + public static readonly KIND = "labelingProcess" + + constructor( + @inject(TYPES.Action) private readonly action: LabelingProcessAction, + @inject(LabelingProcessUi) private readonly ui: LabelingProcessUi + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + this.ui.setState(this.action.state) + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css new file mode 100644 index 00000000..0d179379 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -0,0 +1,18 @@ +.labeling-process-container { + position: absolute; + top: 40px; + left: 50%; + transform: translate(-50%, -50%); + + padding: 4px 12px; + + display: flex; + flex-direction: row; + gap: 8px; + justify-content: center; + align-items: center; + + /* Make text of the elements non-selectable */ + -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ + user-select: none; +} \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts new file mode 100644 index 00000000..6069af49 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -0,0 +1,91 @@ +import { + AbstractUIExtension, + IActionDispatcher, + TYPES, +} from "sprotty"; +import { inject, injectable } from "inversify"; +import './labelingProcessUI.css' +import { LabelType, LabelTypeValue } from "../labels/LabelType.ts"; +import { NextLabelingProcessAction } from "./labelingProcessCommand.ts"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; + +export type LabelingProcessState + = { state: 'pending' } + | { state: 'inProgress', finishedLabels: LabelTypeValueWithLabelType[], activeLabel: LabelTypeValueWithLabelType } + | { state: 'done' } + +export type LabelTypeValueWithLabelType = {labelType: LabelType, labelTypeValue: LabelTypeValue} + +@injectable() +export class LabelingProcessUi extends AbstractUIExtension { + static readonly ID = "labeling-process-ui"; + + private state: LabelingProcessState; + + constructor( + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry + ) { + super(); + this.state = { state:'pending' } + } + + + id(): string { + return LabelingProcessUi.ID; + } + + containerClass(): string { + return "labeling-process-container" + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + this.updateContents(); + } + + private updateContents(): void { + switch (this.state.state) { + case "pending": return; + case "inProgress": return this.showInProgressContents(); + case "done": return this.showDoneContents(); + } + } + + private showInProgressContents(): void { + if (this.state.state !== 'inProgress') return; + + const text = document.createElement('span') + text.innerText = `Please click all nodes that are ${this.state.activeLabel?.labelType.name}.${this.state.activeLabel?.labelTypeValue.text}` + + const nextButton = document.createElement('button') + nextButton.innerText = "Next label" + nextButton.addEventListener('click', () => { + if (this.state.state !== 'inProgress') return; + this.actionDispatcher.dispatch(NextLabelingProcessAction.create( + this.labelTypeRegistry, + [...this.state.finishedLabels, this.state.activeLabel] + )) + }) + + this.containerElement.replaceChildren(text, nextButton) + } + + private showDoneContents(): void { + if (this.state.state !== 'done') return; + + const text = document.createElement('span') + text.innerText = 'You have completed this process.' + + this.containerElement.replaceChildren(text) + } + + public getState(): LabelingProcessState { + return this.state; + } + + public setState(state: LabelingProcessState) { + this.state = state; + this.updateContents(); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 9f8af3b4..e397633b 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -12,6 +12,7 @@ import { IActionDispatcher, TYPES } from "sprotty"; import { SETTINGS } from "../settings/Settings"; import { EditorModeController } from "../settings/editorMode"; import { ReplaceAction } from "./renameCommand"; +import { BeginLabelingProcessAction } from "../labelingProcess/labelingProcessCommand.ts"; export class LabelTypeEditorUi extends AccordionUiExtension { static readonly ID = "label-type-editor-ui"; @@ -46,6 +47,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { this.labelSectionContainer = document.createElement("div"); this.renderLabelTypes(); + contentElement.appendChild(this.buildAnnotationProcessButton()) contentElement.appendChild(this.labelSectionContainer); contentElement.appendChild(addButton); } @@ -53,6 +55,17 @@ export class LabelTypeEditorUi extends AccordionUiExtension { headerElement.innerText = "Label Types"; } + private buildAnnotationProcessButton(): HTMLElement { + const button = document.createElement("button"); + button.id = "annotation-process-button"; + button.innerHTML = "Start annotation process"; + button.onclick = () => { + this.actionDispatcher.dispatch(BeginLabelingProcessAction.create(this.labelTypeRegistry)); + }; + + return button; + } + private renderLabelTypes(): void { if (!this.labelSectionContainer) { return; diff --git a/frontend/webEditor/src/labels/labelTypeEditorUi.css b/frontend/webEditor/src/labels/labelTypeEditorUi.css index f1ac3397..1dd558da 100644 --- a/frontend/webEditor/src/labels/labelTypeEditorUi.css +++ b/frontend/webEditor/src/labels/labelTypeEditorUi.css @@ -37,6 +37,30 @@ } } +#annotation-process-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: fit-content; + cursor: pointer; +} + +#annotation-process-button::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/play.svg"); + display: inline-block; + filter: invert(1); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; +} + /* Label Type value */ .label-type-value input { background-color: var(--color-background); From 491157f50c14925c32fe43f99ad5563c4f2fd0b3 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Mon, 12 Jan 2026 14:09:35 +0100 Subject: [PATCH 04/23] Improve UI; Implement provisional output pin label assignment --- .../webEditor/src/assignment/clickListener.ts | 12 +++ .../src/labelingProcess/di.config.ts | 6 +- .../labelingProcess/labelingProcessCommand.ts | 17 ++-- .../src/labelingProcess/labelingProcessUI.css | 16 +++- .../src/labelingProcess/labelingProcessUi.ts | 71 +++++++++++++---- .../threatModelingAssignmehtCommand.ts | 79 +++++++++++++++++++ .../webEditor/src/labels/LabelTypeRegistry.ts | 18 ++++- .../src/labels/ThreatModelingLabelType.ts | 9 +++ 8 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts index 2d021048..25d3d696 100644 --- a/frontend/webEditor/src/assignment/clickListener.ts +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -3,6 +3,7 @@ import { MouseListener, SModelElementImpl, SetUIExtensionVisibilityAction } from import { Action } from "sprotty-protocol"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; import { AssignmentEditUi } from "./AssignmentEditUi"; +import { AddLabelToOutputPortAction } from "../labelingProcess/threatModelingAssignmehtCommand.ts"; /** * Detects when a dfd output port is double clicked and shows the OutputPortEditUI @@ -50,4 +51,15 @@ export class OutputPortEditUIMouseListener extends MouseListener { return []; } + + contextMenu(target:SModelElementImpl, event:MouseEvent): (Action | Promise)[] { + event.preventDefault(); + + if (!(target instanceof DfdOutputPortImpl)) { + return [] + } + return [ + AddLabelToOutputPortAction.create(target) + ] + } } diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts index 8978fc27..d951f26f 100644 --- a/frontend/webEditor/src/labelingProcess/di.config.ts +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -3,11 +3,13 @@ import { configureCommand, TYPES } from "sprotty"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; import { EDITOR_TYPES } from "../editorTypes.ts"; +import { ThreatModelingAddLabelToOutputPortCommand } from "./threatModelingAssignmehtCommand.ts"; export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(LabelingProcessUi).toSelf().inSingletonScope(); - configureCommand({bind, isBound}, LabelingProcessCommand) - bind(TYPES.IUIExtension).toService(LabelingProcessUi); bind(EDITOR_TYPES.DefaultUIElement).to(LabelingProcessUi); + + configureCommand({bind, isBound}, LabelingProcessCommand) + configureCommand({bind, isBound}, ThreatModelingAddLabelToOutputPortCommand); }) \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index 8649f268..e676526f 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -6,9 +6,9 @@ import { TYPES, } from "sprotty"; import { Action } from "sprotty-protocol"; -import { LabelingProcessState, LabelingProcessUi, LabelTypeValueWithLabelType } from "./labelingProcessUi.ts"; +import { LabelingProcessState, LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; -import { LabelType } from "../labels/LabelType.ts"; +import { LabelAssignment, LabelType } from "../labels/LabelType.ts"; export interface LabelingProcessAction extends Action { state: LabelingProcessState @@ -34,12 +34,12 @@ export namespace BeginLabelingProcessAction { export namespace NextLabelingProcessAction { export function create( labelTypeRegistry: LabelTypeRegistry, - finishedLabels: LabelTypeValueWithLabelType[] + finishedLabels: LabelAssignment[] ): LabelingProcessAction { const pendingLabels = transformLabelTypeArray(labelTypeRegistry.getLabelTypes()) .filter( (label) => !finishedLabels.some( - finishedLabel => finishedLabel.labelType === label.labelType && finishedLabel.labelTypeValue === label.labelTypeValue + finishedLabel => finishedLabel.labelTypeId === label.labelTypeId && finishedLabel.labelTypeValueId === label.labelTypeValueId ) ) @@ -67,11 +67,14 @@ export namespace CompleteLabelingProcessAction { } } -function transformLabelTypeArray(labelTypes: LabelType[]): LabelTypeValueWithLabelType[] { - const transformed: LabelTypeValueWithLabelType[] = [] +function transformLabelTypeArray(labelTypes: LabelType[]): LabelAssignment[] { + const transformed: LabelAssignment[] = [] for (const labelType of labelTypes) { for (const labelTypeValue of labelType.values) { - transformed.push({ labelType, labelTypeValue }); + transformed.push({ + labelTypeId: labelType.id, + labelTypeValueId: labelTypeValue.id + }); } } return transformed; diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index 0d179379..f1f64f81 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -3,8 +3,9 @@ top: 40px; left: 50%; transform: translate(-50%, -50%); + width: fit-content; - padding: 4px 12px; + padding: 10px 20px; display: flex; flex-direction: row; @@ -15,4 +16,17 @@ /* Make text of the elements non-selectable */ -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ user-select: none; +} + +.labeling-process-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: fit-content; + cursor: pointer; } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index 6069af49..251c6d3f 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -5,17 +5,20 @@ import { } from "sprotty"; import { inject, injectable } from "inversify"; import './labelingProcessUI.css' -import { LabelType, LabelTypeValue } from "../labels/LabelType.ts"; +import { LabelAssignment } from "../labels/LabelType.ts"; import { NextLabelingProcessAction } from "./labelingProcessCommand.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; +import { AnalyzeAction } from "../serialize/analyze.ts"; +import { SelectConstraintsAction } from "../constraint/selection.ts"; +import { ConstraintRegistry } from "../constraint/constraintRegistry.ts"; +import { SaveThreatsTableAction } from "../serialize/saveThreatsTable.ts"; +import { isThreatModelingLabelType } from "../labels/ThreatModelingLabelType.ts"; export type LabelingProcessState = { state: 'pending' } - | { state: 'inProgress', finishedLabels: LabelTypeValueWithLabelType[], activeLabel: LabelTypeValueWithLabelType } + | { state: 'inProgress', finishedLabels: LabelAssignment[], activeLabel: LabelAssignment } | { state: 'done' } -export type LabelTypeValueWithLabelType = {labelType: LabelType, labelTypeValue: LabelTypeValue} - @injectable() export class LabelingProcessUi extends AbstractUIExtension { static readonly ID = "labeling-process-ui"; @@ -24,10 +27,11 @@ export class LabelingProcessUi extends AbstractUIExtension { constructor( @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, ) { super(); - this.state = { state:'pending' } + this.state = { state: 'pending' } } @@ -39,28 +43,52 @@ export class LabelingProcessUi extends AbstractUIExtension { return "labeling-process-container" } - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); + protected initializeContents(): void { this.updateContents(); } private updateContents(): void { switch (this.state.state) { - case "pending": return; + case "pending": return this.showPendingContents(); case "inProgress": return this.showInProgressContents(); case "done": return this.showDoneContents(); } } + private showPendingContents(): void { + this.containerElement.classList.remove("ui-float") + } + private showInProgressContents(): void { + this.containerElement.classList.add("ui-float"); if (this.state.state !== 'inProgress') return; const text = document.createElement('span') - text.innerText = `Please click all nodes that are ${this.state.activeLabel?.labelType.name}.${this.state.activeLabel?.labelTypeValue.text}` + const { labelType, labelTypeValue } = this.labelTypeRegistry.getLabelAssignment(this.state.activeLabel) + if (!labelType || !labelTypeValue) { + text.innerText = `Couldn't resolve the LabelType or LabelTypeValue` + } else { + let targetElement = "" + if (isThreatModelingLabelType(labelType)) { + switch (labelType.intendedFor) { + case 'Vertex': + targetElement = "nodes"; + break; + case 'Flow': + targetElement = "output pins"; + break; + } + } else { + targetElement = "nodes and output pins" + } + + text.innerText = `Please click all ${targetElement} that are ${labelType.name}.${labelTypeValue.text}` + } - const nextButton = document.createElement('button') - nextButton.innerText = "Next label" - nextButton.addEventListener('click', () => { + const nextStepButton = document.createElement('button') + nextStepButton.innerText = "Next label" + nextStepButton.classList.add("labeling-process-button") + nextStepButton.addEventListener('click', () => { if (this.state.state !== 'inProgress') return; this.actionDispatcher.dispatch(NextLabelingProcessAction.create( this.labelTypeRegistry, @@ -68,16 +96,29 @@ export class LabelingProcessUi extends AbstractUIExtension { )) }) - this.containerElement.replaceChildren(text, nextButton) + this.containerElement.replaceChildren(text, nextStepButton) } private showDoneContents(): void { + this.containerElement.classList.add("ui-float"); if (this.state.state !== 'done') return; const text = document.createElement('span') text.innerText = 'You have completed this process.' - this.containerElement.replaceChildren(text) + const finalStepsButton = document.createElement('button') + finalStepsButton.innerText = "Check constraints and download threats" + finalStepsButton.classList.add("labeling-process-button") + finalStepsButton.addEventListener('click', () => { + this.actionDispatcher.dispatchAll([ + AnalyzeAction.create(), + SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + ]).then(() => + this.actionDispatcher.dispatch(SaveThreatsTableAction.create()) + ) + }) + + this.containerElement.replaceChildren(text, finalStepsButton) } public getState(): LabelingProcessState { diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts new file mode 100644 index 00000000..6a07e61a --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts @@ -0,0 +1,79 @@ +import { Action } from "sprotty-protocol"; +import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty"; +import { inject, injectable } from "inversify"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; +import { isThreatModelingLabelType, isThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; +import { LabelingProcessUi } from "./labelingProcessUi.ts"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; + + +interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { + element: DfdOutputPortImpl;// & SNodeImpl; + //labelAssignment: LabelAssignment; +} + +export namespace AddLabelToOutputPortAction { + export function create( + element: DfdOutputPortImpl,// & SNodeImpl, + ): ThreatModelingLabelAssignmentToOutputPortAction { + return { + kind: ThreatModelingAddLabelToOutputPortCommand.KIND, + element + }; + } +} + +@injectable() +export class ThreatModelingAddLabelToOutputPortCommand implements Command { + public static readonly KIND = "threatModeling-addLabelToOutputPort"; + + private previousBehavior?: string + private newBehavior?: string + + constructor( + @inject(TYPES.Action) private readonly action: ThreatModelingLabelAssignmentToOutputPortAction, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(LabelingProcessUi) private readonly labelingProcessUI: LabelingProcessUi + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + const labelProcessState = this.labelingProcessUI.getState() + if (labelProcessState.state !== "inProgress") return context.root; + + const { labelType, labelTypeValue } = this.labelTypeRegistry.getLabelAssignment(labelProcessState.activeLabel) + if (!labelType || !labelTypeValue) return context.root; + + console.error(labelType) + console.error(labelTypeValue) + + this.previousBehavior = this.action.element.getBehavior() + if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { + this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` + } else { + const regex = /forward\s+([a-zA-Z0-9|\s]+)/; + const match = this.previousBehavior.match(regex); + + this.newBehavior = match ? match[0] : ""; + this.newBehavior += "\n"; + this.newBehavior += labelTypeValue.defaultPinBehavior.replace("{forward}", ""); + } + + this.action.element.setBehavior(this.newBehavior); + + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + if (!this.newBehavior) return context.root; + + this.action.element.setBehavior(this.newBehavior); + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (!this.previousBehavior) return context.root; + + this.action.element.setBehavior(this.previousBehavior); + return context.root; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts index 35ee3295..99c5739b 100644 --- a/frontend/webEditor/src/labels/LabelTypeRegistry.ts +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -1,5 +1,5 @@ import { generateRandomSprottyId } from "../utils/idGenerator"; -import { LabelType, LabelTypeValue } from "./LabelType"; +import { LabelAssignment, LabelType, LabelTypeValue } from "./LabelType"; export class LabelTypeRegistry { private labelTypes: LabelType[] = []; @@ -98,4 +98,20 @@ export class LabelTypeRegistry { public getLabelType(id: string): LabelType | undefined { return this.labelTypes.find((type) => type.id === id); } + + /** + * Resolves a `LabelAssignment` and returns the matching `LabelType` and `LabelTypeValue`. + * If the `LabelAssignment` cannot be resolved, returns `{}`. + * @param labelAssignment The IDs of the `LabelType` and `LabelTypeValue`. to resolve. + */ + public getLabelAssignment(labelAssignment: LabelAssignment): Partial<{ labelType: LabelType, labelTypeValue: LabelTypeValue }> + { + const labelType = this.getLabelType(labelAssignment.labelTypeId); + const labelTypeValue = labelType?.values + .find((value) => value.id === labelAssignment.labelTypeValueId); + + if (!labelType || !labelTypeValue) return {}; + + return {labelType, labelTypeValue}; + } } diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts index b9e5b0a6..9ba28d16 100644 --- a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -7,4 +7,13 @@ export interface ThreatModelingLabelType extends LabelType { export interface ThreatModelingLabelTypeValue extends LabelTypeValue { defaultPinBehavior: string, additionalInformation: string[] +} + +export function isThreatModelingLabelType(labelType: LabelType): labelType is ThreatModelingLabelType { + return "intendedFor" in labelType; +} + +export function isThreatModelingLabelTypeValue(labelTypeValue: LabelTypeValue): labelTypeValue is ThreatModelingLabelTypeValue { + return "defaultPinBehavior" in labelTypeValue + && "additionalInformation" in labelTypeValue } \ No newline at end of file From c505734356cb2b28f7f8181aa316ada5c411aa46 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Tue, 13 Jan 2026 12:13:22 +0100 Subject: [PATCH 05/23] Implement shape highlighting during labeling process; Update label assignment --- .../webEditor/src/assignment/clickListener.ts | 12 ---- .../src/diagram/ports/DfdOutputPort.tsx | 9 +++ .../ClickToAssignMouseListener.ts | 42 +++++++++++ .../src/labelingProcess/di.config.ts | 7 +- .../labelingProcess/labelingProcessCommand.ts | 69 ++++++++++++++----- .../src/labelingProcess/labelingProcessUI.css | 10 +++ .../src/labelingProcess/labelingProcessUi.ts | 15 ++-- ...mand.ts => outputPortAssignmentCommand.ts} | 16 ++--- .../webEditor/src/labels/LabelTypeEditorUi.ts | 2 +- .../webEditor/src/labels/LabelTypeRegistry.ts | 12 +++- frontend/webEditor/src/labels/dragAndDrop.ts | 2 +- 11 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts rename frontend/webEditor/src/labelingProcess/{threatModelingAssignmehtCommand.ts => outputPortAssignmentCommand.ts} (84%) diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts index 25d3d696..2d021048 100644 --- a/frontend/webEditor/src/assignment/clickListener.ts +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -3,7 +3,6 @@ import { MouseListener, SModelElementImpl, SetUIExtensionVisibilityAction } from import { Action } from "sprotty-protocol"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; import { AssignmentEditUi } from "./AssignmentEditUi"; -import { AddLabelToOutputPortAction } from "../labelingProcess/threatModelingAssignmehtCommand.ts"; /** * Detects when a dfd output port is double clicked and shows the OutputPortEditUI @@ -51,15 +50,4 @@ export class OutputPortEditUIMouseListener extends MouseListener { return []; } - - contextMenu(target:SModelElementImpl, event:MouseEvent): (Action | Promise)[] { - event.preventDefault(); - - if (!(target instanceof DfdOutputPortImpl)) { - return [] - } - return [ - AddLabelToOutputPortAction.create(target) - ] - } } diff --git a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx index 32c24814..bb34050f 100644 --- a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx +++ b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx @@ -16,6 +16,9 @@ export interface DfdOutputPort extends SPort { @injectable() export class DfdOutputPortImpl extends DfdPortImpl { + static readonly PORT_COLOR = "var(--color-primary)"; + + private color?: string; private behavior: string = ""; private validBehavior: boolean = true; private tree?: LanguageTreeNode[]; @@ -52,6 +55,8 @@ export class DfdOutputPortImpl extends DfdPortImpl { style["--port-color"] = "#ff6961"; } + if (this.color) style["--port-color"] = this.color + return style; } @@ -75,6 +80,10 @@ export class DfdOutputPortImpl extends DfdPortImpl { public getBehavior() { return this.behavior; } + + public setColor(color: string, override: boolean = true) { + if (override || this.color === DfdOutputPortImpl.PORT_COLOR) this.color = color; + } } @injectable() diff --git a/frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts b/frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts new file mode 100644 index 00000000..e94582e5 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts @@ -0,0 +1,42 @@ +import { MouseListener, SModelElementImpl, SNodeImpl } from "sprotty"; +import { Action } from "sprotty-protocol/lib/actions"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; +import { inject } from "inversify"; +import { LabelingProcessUi } from "./labelingProcessUi.ts"; +import { AddLabelToOutputPortAction } from "./outputPortAssignmentCommand.ts"; +import { containsDfdLabels } from "../labels/feature"; +import { AddLabelAssignmentAction } from "../labels/assignmentCommand.ts"; +import { getParentWithDfdLabels } from "../labels/dragAndDrop.ts"; + +export class ClickToAssignMouseListener extends MouseListener { + + constructor( + @inject(LabelingProcessUi) private readonly labelingProcessUi: LabelingProcessUi + ) { + super(); + } + + override contextMenu(target: SModelElementImpl): Action[] { + //Only do this while the labeling process is in progress + const processState = this.labelingProcessUi.getState(); + if (processState.state !== "inProgress") return []; + + // Adds label to Output Port + if (target instanceof DfdOutputPortImpl) { + return [AddLabelToOutputPortAction.create(target)] + } + + // Adds label to nodes + const dfdLabelElement = getParentWithDfdLabels(target); + if (!dfdLabelElement) return [] + if (containsDfdLabels(dfdLabelElement)) { + if (!(dfdLabelElement instanceof SNodeImpl)) return []; + return [AddLabelAssignmentAction.create( + processState.activeLabel, + dfdLabelElement + )] + } + + return [] + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts index d951f26f..ad93dcd4 100644 --- a/frontend/webEditor/src/labelingProcess/di.config.ts +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -3,13 +3,16 @@ import { configureCommand, TYPES } from "sprotty"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; import { EDITOR_TYPES } from "../editorTypes.ts"; -import { ThreatModelingAddLabelToOutputPortCommand } from "./threatModelingAssignmehtCommand.ts"; +import { OutputPortAssignmentCommand } from "./outputPortAssignmentCommand.ts"; +import { ClickToAssignMouseListener } from "./ClickToAssignMouseListener.ts"; export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(LabelingProcessUi).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(LabelingProcessUi); bind(EDITOR_TYPES.DefaultUIElement).to(LabelingProcessUi); + bind(TYPES.MouseListener).to(ClickToAssignMouseListener).inSingletonScope(); + configureCommand({bind, isBound}, LabelingProcessCommand) - configureCommand({bind, isBound}, ThreatModelingAddLabelToOutputPortCommand); + configureCommand({bind, isBound}, OutputPortAssignmentCommand); }) \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index e676526f..1408fa96 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -8,7 +8,11 @@ import { import { Action } from "sprotty-protocol"; import { LabelingProcessState, LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; -import { LabelAssignment, LabelType } from "../labels/LabelType.ts"; +import { LabelAssignment } from "../labels/LabelType.ts"; +import { isThreatModelingLabelType } from "../labels/ThreatModelingLabelType.ts"; +import { getAllElements } from "../labels/assignmentCommand.ts"; +import { DfdNodeImpl } from "../diagram/nodes/common.ts"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; export interface LabelingProcessAction extends Action { state: LabelingProcessState @@ -18,7 +22,7 @@ export namespace BeginLabelingProcessAction { export function create( labelTypeRegistry: LabelTypeRegistry ): LabelingProcessAction { - const allLabels = transformLabelTypeArray(labelTypeRegistry.getLabelTypes()) + const allLabels = labelTypeRegistry.getAllLabelAssignments() return { kind: LabelingProcessCommand.KIND, @@ -36,7 +40,7 @@ export namespace NextLabelingProcessAction { labelTypeRegistry: LabelTypeRegistry, finishedLabels: LabelAssignment[] ): LabelingProcessAction { - const pendingLabels = transformLabelTypeArray(labelTypeRegistry.getLabelTypes()) + const pendingLabels = labelTypeRegistry.getAllLabelAssignments() .filter( (label) => !finishedLabels.some( finishedLabel => finishedLabel.labelTypeId === label.labelTypeId && finishedLabel.labelTypeValueId === label.labelTypeValueId @@ -67,40 +71,69 @@ export namespace CompleteLabelingProcessAction { } } -function transformLabelTypeArray(labelTypes: LabelType[]): LabelAssignment[] { - const transformed: LabelAssignment[] = [] - for (const labelType of labelTypes) { - for (const labelTypeValue of labelType.values) { - transformed.push({ - labelTypeId: labelType.id, - labelTypeValueId: labelTypeValue.id - }); - } - } - return transformed; -} - @injectable() export class LabelingProcessCommand implements Command { public static readonly KIND = "labelingProcess" + public static readonly HIGHLIGHT_COLOR = '#00FF00' + + private previousState?: LabelingProcessState = undefined; constructor( @inject(TYPES.Action) private readonly action: LabelingProcessAction, - @inject(LabelingProcessUi) private readonly ui: LabelingProcessUi + @inject(LabelingProcessUi) private readonly ui: LabelingProcessUi, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, ) {} execute(context: CommandExecutionContext): CommandReturn { - this.ui.setState(this.action.state) + this.previousState = this.ui.getState(); + + this.ui.setState(this.action.state); + this.highlightShapes(context); + return context.root; } redo(context: CommandExecutionContext): CommandReturn { + if (this.previousState) { + this.ui.setState(this.previousState); + this.highlightShapes(context); + } return context.root; } undo(context: CommandExecutionContext): CommandReturn { + this.ui.setState(this.action.state); + this.highlightShapes(context); return context.root; } + highlightShapes(context: CommandExecutionContext) { + if (this.action.state.state !== "inProgress") return context.root; + + const { labelType } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) + if (!labelType) return; + + let nodeColor = "" + let outputPortColor = "" + if (!isThreatModelingLabelType(labelType)) { + nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR + outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + } else if (labelType.intendedFor === "Vertex") { + nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR + outputPortColor = DfdOutputPortImpl.PORT_COLOR + } else { + nodeColor = DfdNodeImpl.NODE_COLOR + outputPortColor = DfdOutputPortImpl.PORT_COLOR + } + + getAllElements(context.root.children) + .filter((element) => element instanceof DfdNodeImpl) + .forEach(node => node.setColor(nodeColor)) + + getAllElements(context.root.children) + .filter((element) => element instanceof DfdOutputPortImpl) + .forEach(port => port.setColor(outputPortColor)) + } + } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index f1f64f81..06a652e3 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -18,6 +18,16 @@ user-select: none; } +.labeling-highlight { + stroke: green; + fill: green; +} + +.labeling-highlight * { + stroke: inherit; + fill: inherit; +} + .labeling-process-button { background-color: green; color: white; diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index 251c6d3f..b2a629b9 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -64,25 +64,18 @@ export class LabelingProcessUi extends AbstractUIExtension { if (this.state.state !== 'inProgress') return; const text = document.createElement('span') - const { labelType, labelTypeValue } = this.labelTypeRegistry.getLabelAssignment(this.state.activeLabel) + const { labelType, labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(this.state.activeLabel) if (!labelType || !labelTypeValue) { text.innerText = `Couldn't resolve the LabelType or LabelTypeValue` } else { let targetElement = "" if (isThreatModelingLabelType(labelType)) { - switch (labelType.intendedFor) { - case 'Vertex': - targetElement = "nodes"; - break; - case 'Flow': - targetElement = "output pins"; - break; - } + targetElement = labelType.intendedFor === "Vertex" ? "node" : "output pin" } else { - targetElement = "nodes and output pins" + targetElement = "node or output pin" } - text.innerText = `Please click all ${targetElement} that are ${labelType.name}.${labelTypeValue.text}` + text.innerText = `Right click to assign ${labelType.name}.${labelTypeValue.text} to a ${targetElement}` } const nextStepButton = document.createElement('button') diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts similarity index 84% rename from frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts rename to frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 6a07e61a..8a98d768 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmehtCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -8,24 +8,23 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { - element: DfdOutputPortImpl;// & SNodeImpl; - //labelAssignment: LabelAssignment; + element: DfdOutputPortImpl; } export namespace AddLabelToOutputPortAction { export function create( - element: DfdOutputPortImpl,// & SNodeImpl, + element: DfdOutputPortImpl, ): ThreatModelingLabelAssignmentToOutputPortAction { return { - kind: ThreatModelingAddLabelToOutputPortCommand.KIND, + kind: OutputPortAssignmentCommand.KIND, element }; } } @injectable() -export class ThreatModelingAddLabelToOutputPortCommand implements Command { - public static readonly KIND = "threatModeling-addLabelToOutputPort"; +export class OutputPortAssignmentCommand implements Command { + public static readonly KIND = "addLabelToOutputPort"; private previousBehavior?: string private newBehavior?: string @@ -40,12 +39,9 @@ export class ThreatModelingAddLabelToOutputPortCommand implements Command { const labelProcessState = this.labelingProcessUI.getState() if (labelProcessState.state !== "inProgress") return context.root; - const { labelType, labelTypeValue } = this.labelTypeRegistry.getLabelAssignment(labelProcessState.activeLabel) + const { labelType, labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(labelProcessState.activeLabel) if (!labelType || !labelTypeValue) return context.root; - console.error(labelType) - console.error(labelTypeValue) - this.previousBehavior = this.action.element.getBehavior() if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index e397633b..1a641aaf 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -58,7 +58,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { private buildAnnotationProcessButton(): HTMLElement { const button = document.createElement("button"); button.id = "annotation-process-button"; - button.innerHTML = "Start annotation process"; + button.innerHTML = "Start labeling process"; button.onclick = () => { this.actionDispatcher.dispatch(BeginLabelingProcessAction.create(this.labelTypeRegistry)); }; diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts index 99c5739b..800ff0c3 100644 --- a/frontend/webEditor/src/labels/LabelTypeRegistry.ts +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -99,12 +99,22 @@ export class LabelTypeRegistry { return this.labelTypes.find((type) => type.id === id); } + public getAllLabelAssignments(): LabelAssignment[] { + return this.labelTypes + .map(labelType => labelType.values + .map(labelTypeValue => { + return { labelTypeId: labelType.id, labelTypeValueId: labelTypeValue.id } + }) + ) + .flat(); + } + /** * Resolves a `LabelAssignment` and returns the matching `LabelType` and `LabelTypeValue`. * If the `LabelAssignment` cannot be resolved, returns `{}`. * @param labelAssignment The IDs of the `LabelType` and `LabelTypeValue`. to resolve. */ - public getLabelAssignment(labelAssignment: LabelAssignment): Partial<{ labelType: LabelType, labelTypeValue: LabelTypeValue }> + public resolveLabelAssignment(labelAssignment: LabelAssignment): Partial<{ labelType: LabelType, labelTypeValue: LabelTypeValue }> { const labelType = this.getLabelType(labelAssignment.labelTypeId); const labelTypeValue = labelType?.values diff --git a/frontend/webEditor/src/labels/dragAndDrop.ts b/frontend/webEditor/src/labels/dragAndDrop.ts index 2b0fef80..209308fe 100644 --- a/frontend/webEditor/src/labels/dragAndDrop.ts +++ b/frontend/webEditor/src/labels/dragAndDrop.ts @@ -59,7 +59,7 @@ export class DfdLabelMouseDropListener extends MouseListener { } } -function getParentWithDfdLabels( +export function getParentWithDfdLabels( element: SChildElementImpl | SModelElementImpl, ): (SModelElementImpl & ContainsDfdLabels) | undefined { if (containsDfdLabels(element)) { From 5b5f4a9987bce951705d6f9567d69a63788ea7b3 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Tue, 13 Jan 2026 12:22:49 +0100 Subject: [PATCH 06/23] Remove unnecessary css classes --- .../src/labelingProcess/labelingProcessUI.css | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index 06a652e3..f1f64f81 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -18,16 +18,6 @@ user-select: none; } -.labeling-highlight { - stroke: green; - fill: green; -} - -.labeling-highlight * { - stroke: inherit; - fill: inherit; -} - .labeling-process-button { background-color: green; color: white; From 00a50ddc7b3a3c94b594a722d991e50997390246 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Tue, 20 Jan 2026 20:32:25 +0100 Subject: [PATCH 07/23] Refactor 'excludes' to enable better handling in the future --- frontend/webEditor/src/index.ts | 2 + .../src/labelingProcess/di.config.ts | 4 + .../src/labelingProcess/excludesDialog.ts | 46 +++++ .../labelingProcess/labelingProcessCommand.ts | 4 +- .../src/labelingProcess/labelingProcessUi.ts | 1 + .../outputPortAssignmentCommand.ts | 165 ++++++++++++++++-- .../src/labels/ThreatModelingLabelType.ts | 7 +- frontend/webEditor/src/uiDialog/di.config.ts | 7 + frontend/webEditor/src/uiDialog/dialog.css | 31 ++++ frontend/webEditor/src/uiDialog/index.ts | 50 ++++++ .../src/uiDialog/showDialogCommand.ts | 46 +++++ 11 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 frontend/webEditor/src/labelingProcess/excludesDialog.ts create mode 100644 frontend/webEditor/src/uiDialog/di.config.ts create mode 100644 frontend/webEditor/src/uiDialog/dialog.css create mode 100644 frontend/webEditor/src/uiDialog/index.ts create mode 100644 frontend/webEditor/src/uiDialog/showDialogCommand.ts diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 6305d770..e0e6fd84 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -26,6 +26,7 @@ import { assignmentModule } from "./assignment/di.config"; import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; import { loadingIndicatorModule } from "./loadingIndicator/di.config"; import { labelingProcessModule } from "./labelingProcess/di.config.ts"; +import { uiDialogModule } from "./uiDialog/di.config.ts"; const container = new Container(); @@ -51,6 +52,7 @@ container.load( settingsModule, labelingProcessModule, toolPaletteModule, + uiDialogModule, constraintModule, assignmentModule, editorModeOverwritesModule, diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts index ad93dcd4..ef7dac3c 100644 --- a/frontend/webEditor/src/labelingProcess/di.config.ts +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -5,12 +5,16 @@ import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; import { EDITOR_TYPES } from "../editorTypes.ts"; import { OutputPortAssignmentCommand } from "./outputPortAssignmentCommand.ts"; import { ClickToAssignMouseListener } from "./ClickToAssignMouseListener.ts"; +import { ExcludesDialog } from "./excludesDialog.ts"; export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(LabelingProcessUi).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(LabelingProcessUi); bind(EDITOR_TYPES.DefaultUIElement).to(LabelingProcessUi); + bind(ExcludesDialog).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(ExcludesDialog); + bind(TYPES.MouseListener).to(ClickToAssignMouseListener).inSingletonScope(); configureCommand({bind, isBound}, LabelingProcessCommand) diff --git a/frontend/webEditor/src/labelingProcess/excludesDialog.ts b/frontend/webEditor/src/labelingProcess/excludesDialog.ts new file mode 100644 index 00000000..fa283c9c --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/excludesDialog.ts @@ -0,0 +1,46 @@ +import { AbstractDialog } from "../uiDialog"; +import { ThreatModelingLabelType, ThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; + +export class ExcludesDialog extends AbstractDialog { + + private contentContainer: HTMLDivElement + + constructor( + ) { + super(); + this.contentContainer = document.createElement("div"); + } + + id(): string { + return "excludes-collision-dialog"; + } + + public setContent( + previousLabelAssignments: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue}[], + newLabelAssignment: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue}, + ) { + this.contentContainer.innerText = "The labels " + + previousLabelAssignments + .map(assignment => `${assignment.labelType.name}.${assignment.labelTypeValue.text}`) + .join(", ") + + " and " + + `${newLabelAssignment.labelType.name}.${newLabelAssignment.labelTypeValue.text}` + + " cannot be assigned at the same time, since they exclude each other." + } + + protected initializeText(): HTMLElement { + return this.contentContainer + } + + protected initializeButtons(): HTMLButtonElement[] { + const keepPreviousLabelButton = document.createElement("button") + keepPreviousLabelButton.innerText = `Keep previous labels`; + keepPreviousLabelButton.classList.add("labeling-process-button") + + const overwriteWithNewLabelButton = document.createElement("button"); + overwriteWithNewLabelButton.innerText = `Replace with new label`; + overwriteWithNewLabelButton.classList.add("labeling-process-button") + + return [keepPreviousLabelButton, overwriteWithNewLabelButton]; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index 1408fa96..f34f02f7 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -109,7 +109,7 @@ export class LabelingProcessCommand implements Command { } highlightShapes(context: CommandExecutionContext) { - if (this.action.state.state !== "inProgress") return context.root; + if (this.action.state.state !== "inProgress") return; const { labelType } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) if (!labelType) return; @@ -124,7 +124,7 @@ export class LabelingProcessCommand implements Command { outputPortColor = DfdOutputPortImpl.PORT_COLOR } else { nodeColor = DfdNodeImpl.NODE_COLOR - outputPortColor = DfdOutputPortImpl.PORT_COLOR + outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR } getAllElements(context.root.children) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index b2a629b9..272380eb 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -83,6 +83,7 @@ export class LabelingProcessUi extends AbstractUIExtension { nextStepButton.classList.add("labeling-process-button") nextStepButton.addEventListener('click', () => { if (this.state.state !== 'inProgress') return; + this.actionDispatcher.dispatch(NextLabelingProcessAction.create( this.labelTypeRegistry, [...this.state.finishedLabels, this.state.activeLabel] diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 8a98d768..38c18e53 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -1,23 +1,39 @@ import { Action } from "sprotty-protocol"; -import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty"; +import { + Command, + CommandExecutionContext, + CommandReturn, + IActionDispatcher, + TYPES, +} from "sprotty"; import { inject, injectable } from "inversify"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; -import { isThreatModelingLabelType, isThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; +import { + isThreatModelingLabelType, + isThreatModelingLabelTypeValue, + ThreatModelingLabelType, + ThreatModelingLabelTypeValue, +} from "../labels/ThreatModelingLabelType.ts"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; +import { ExcludesDialog } from "./excludesDialog.ts"; +import { CreateShowDialogAction } from "../uiDialog/showDialogCommand.ts"; interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { element: DfdOutputPortImpl; + collisionMode: 'overwrite' | 'askUser' } export namespace AddLabelToOutputPortAction { export function create( element: DfdOutputPortImpl, + collisionMode?: 'overwrite' | 'askUser' ): ThreatModelingLabelAssignmentToOutputPortAction { return { kind: OutputPortAssignmentCommand.KIND, - element + element, + collisionMode: collisionMode ?? 'overwrite' }; } } @@ -32,7 +48,9 @@ export class OutputPortAssignmentCommand implements Command { constructor( @inject(TYPES.Action) private readonly action: ThreatModelingLabelAssignmentToOutputPortAction, @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(LabelingProcessUi) private readonly labelingProcessUI: LabelingProcessUi + @inject(LabelingProcessUi) private readonly labelingProcessUI: LabelingProcessUi, + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(ExcludesDialog) private readonly excludesDialog: ExcludesDialog ) {} execute(context: CommandExecutionContext): CommandReturn { @@ -45,18 +63,37 @@ export class OutputPortAssignmentCommand implements Command { this.previousBehavior = this.action.element.getBehavior() if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` - } else { - const regex = /forward\s+([a-zA-Z0-9|\s]+)/; - const match = this.previousBehavior.match(regex); + this.action.element.setBehavior(this.newBehavior); + return context.root; + } + + let lines = this.previousBehavior + .split("\n") + .map(line => line.trim()); + const collisions = findAllCollisions(lines, labelType, labelTypeValue, this.labelTypeRegistry) + + if (collisions.length == 0) { + lines = addLabelAssignment(lines, labelType, labelTypeValue, this.labelTypeRegistry) + this.newBehavior = lines.join("\n") + this.action.element.setBehavior(this.newBehavior); + return context.root; + } - this.newBehavior = match ? match[0] : ""; - this.newBehavior += "\n"; - this.newBehavior += labelTypeValue.defaultPinBehavior.replace("{forward}", ""); + if (this.action.collisionMode === "askUser") { + this.actionDispatcher.dispatch(CreateShowDialogAction.create(this.excludesDialog)) + //TODO add actions on button presses + return context.root } + //this.action.collisionMode === "overwrite" + for (const collision of collisions) { + lines = removeLabelAssignment(lines, collision.labelType, collision.labelTypeValue) + } + lines = addLabelAssignment(lines, labelType, labelTypeValue, this.labelTypeRegistry) + this.newBehavior = lines.join("\n") this.action.element.setBehavior(this.newBehavior); - return context.root; + return context.root } redo(context: CommandExecutionContext): CommandReturn { @@ -72,4 +109,110 @@ export class OutputPortAssignmentCommand implements Command { this.action.element.setBehavior(this.previousBehavior); return context.root; } +} + +function findAllCollisions( + portBehavior: string[], + newLabelType: ThreatModelingLabelType, + newLabelTypeValue: ThreatModelingLabelTypeValue, + labelTypeRegistry: LabelTypeRegistry +): { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] { + const collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] = []; + + for (let i = 0; i < portBehavior.length; i++) { + const line = portBehavior[i] + + //Search for a previous assignment that excludes the new assignment + if (line.match(`unset ${newLabelType.name}.${newLabelTypeValue.text}`)) { + //Searches for the previous `set` assignment + //Assumes that each `set` assignment is directly followed by their `unset` (`exclude`) assignments + for (let j = i; j >= 0; j--) { + if (portBehavior[j].match(`set`)) { + const parts = portBehavior[j].split(" ") + const label = parts[1] + const [ labelTypeName, labelTypeValueText ] = label.split(".") + + const labelType = labelTypeRegistry.getLabelTypes() + .find((labelType) => labelType.name === labelTypeName) + if (!labelType) continue; + + const labelTypeValue = labelType.values + .find((labelTypeValue) => labelTypeValue.text === labelTypeValueText) + if (!labelTypeValue) continue; + + if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) continue; + + collisions.push({ labelType, labelTypeValue }) + } + } + } + + //Search for a previous assignment that is excluded by the new assignment + for (const exclude of newLabelTypeValue.excludes) { + const { labelType, labelTypeValue } = labelTypeRegistry.resolveLabelAssignment(exclude); + if ( + !labelType + || !labelTypeValue + || !isThreatModelingLabelType(labelType) + || !isThreatModelingLabelTypeValue(labelTypeValue) + ) continue; + + if (line.match(`set ${labelType.name}.${labelTypeValue.text}`)) { + collisions.push({ labelType, labelTypeValue }); + } + } + } + + //TODO what about multiple entries for the same collision?? + return collisions; +} + +/** + * Adds a label assignment to the output port behavior string, including the `excludes` relations. + */ +function addLabelAssignment( + portBehavior: string[], + labelType: ThreatModelingLabelType, + labelTypeValue: ThreatModelingLabelTypeValue, + labelTypeRegistry: LabelTypeRegistry +): string[] { + const setAssignment = `set ${labelType.name}.${labelTypeValue.text}` + const unsetAssignments: string[] = labelTypeValue.excludes.map((exclude) => { + const { labelType, labelTypeValue } = labelTypeRegistry.resolveLabelAssignment(exclude) + if ( !labelType || !labelTypeValue ) return ""; + return `unset ${labelType.name}.${labelTypeValue.text}` + }) + + return [...portBehavior, setAssignment, ...unsetAssignments] +} + +/** + * Removes all assignments of a label from output port behavior string, including the `excludes` relations. + */ +function removeLabelAssignment( + portBehavior: string[], + labelType: ThreatModelingLabelType, + labelTypeValue: ThreatModelingLabelTypeValue +): string[] { + let removing = false; + + return portBehavior.filter(line => { + if (line === `set ${labelType.name}.${labelTypeValue.text}`) { + removing = true; + return false; + } + + if (removing) { + if (line.startsWith("unset ")) { + return false; + } + + if (line.startsWith("set ")) { + removing = false; + return true; + } + } + + return true; + }); } \ No newline at end of file diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts index 9ba28d16..12980ce4 100644 --- a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -1,11 +1,12 @@ -import { LabelType, LabelTypeValue } from "./LabelType.ts"; +import { LabelAssignment, LabelType, LabelTypeValue } from "./LabelType.ts"; export interface ThreatModelingLabelType extends LabelType { intendedFor: 'Vertex' | 'Flow' //TODO maybe stattdessen hier 'Node' und 'Edge' verwenden } export interface ThreatModelingLabelTypeValue extends LabelTypeValue { - defaultPinBehavior: string, + excludes: LabelAssignment[] + //defaultPinBehavior: string, additionalInformation: string[] } @@ -14,6 +15,6 @@ export function isThreatModelingLabelType(labelType: LabelType): labelType is Th } export function isThreatModelingLabelTypeValue(labelTypeValue: LabelTypeValue): labelTypeValue is ThreatModelingLabelTypeValue { - return "defaultPinBehavior" in labelTypeValue + return "excludes" in labelTypeValue && "additionalInformation" in labelTypeValue } \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/di.config.ts b/frontend/webEditor/src/uiDialog/di.config.ts new file mode 100644 index 00000000..8e11d307 --- /dev/null +++ b/frontend/webEditor/src/uiDialog/di.config.ts @@ -0,0 +1,7 @@ +import { ContainerModule } from "inversify"; +import { configureCommand } from "sprotty"; +import { ShowDialogCommand } from "./showDialogCommand.ts"; + +export const uiDialogModule = new ContainerModule((bind, _, isBound) => { + configureCommand({bind, isBound}, ShowDialogCommand); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/dialog.css b/frontend/webEditor/src/uiDialog/dialog.css new file mode 100644 index 00000000..c8c9aab1 --- /dev/null +++ b/frontend/webEditor/src/uiDialog/dialog.css @@ -0,0 +1,31 @@ +.dialog-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.dialog-container.hidden { + display: none; +} + +.dialog { + z-index: 101; + position: relative; + + background-color: var(--color-background); + border-radius: 5px; + padding: 16px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; +} \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/index.ts b/frontend/webEditor/src/uiDialog/index.ts new file mode 100644 index 00000000..4ccc6248 --- /dev/null +++ b/frontend/webEditor/src/uiDialog/index.ts @@ -0,0 +1,50 @@ +import { AbstractUIExtension, SModelRootImpl } from "sprotty"; +import './dialog.css' +import { injectable } from "inversify"; + +/** + * Base class for a dialog. The generic type parameter `T` is used to specify the possible return values. + * + * The return value of the dialog (i.e. which button has been pressed) is then-able by calling `getResult()`. + * Do NOT await the result inside a Command, since Sprotty executes only one command at a time. + */ +@injectable() +export abstract class AbstractDialog extends AbstractUIExtension { + + public constructor() { + super(); + } + + containerClass(): string { + return "dialog-container"; + } + + override hide() { + super.hide(); + this.containerElement.classList.add("hidden"); + } + + override show(root: Readonly, ...contextElementIds: string[]): void { + super.show(root, ...contextElementIds) + this.containerElement.classList.remove("hidden"); + } + + protected initializeContents(containerElement: HTMLElement): void { + const dialog = document.createElement("div"); + dialog.classList.add("dialog"); + containerElement.appendChild(dialog); + + dialog.appendChild(this.initializeText()); + + const actions = this.initializeButtons() + actions.forEach(button => { + button.addEventListener("click", () => { + this.hide(); + }) + dialog.appendChild(button) + }) + } + + protected abstract initializeText(): HTMLElement; + protected abstract initializeButtons(): HTMLButtonElement[]; +} \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/showDialogCommand.ts b/frontend/webEditor/src/uiDialog/showDialogCommand.ts new file mode 100644 index 00000000..172dbd1a --- /dev/null +++ b/frontend/webEditor/src/uiDialog/showDialogCommand.ts @@ -0,0 +1,46 @@ +import { Action } from "sprotty-protocol"; +import { + Command, + CommandExecutionContext, + CommandReturn, + TYPES, +} from "sprotty"; +import { AbstractDialog } from "./index.ts"; +import { inject, injectable } from "inversify"; + +export interface ShowDialogAction extends Action { + dialog: T +} + +export namespace CreateShowDialogAction { + export function create(dialog: T): ShowDialogAction { + return { + kind: ShowDialogCommand.KIND, + dialog + } + } +} + +@injectable() +export class ShowDialogCommand implements Command { + public static readonly KIND = "showDialog" + + constructor( + @inject(TYPES.Action) private readonly action: ShowDialogAction, + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + this.action.dialog.show(context.root) + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + this.action.dialog.show(context.root) + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + this.action.dialog.hide(); + return context.root; + } +} \ No newline at end of file From 94ee721b1738b36e5781a07e651315a90a461491 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Thu, 22 Jan 2026 17:20:46 +0100 Subject: [PATCH 08/23] Improve dialog and assignment --- frontend/webEditor/package-lock.json | 14 ++ frontend/webEditor/package.json | 1 + frontend/webEditor/src/index.ts | 2 - .../src/labelingProcess/di.config.ts | 6 +- .../{uiDialog => labelingProcess}/dialog.css | 31 +++- .../src/labelingProcess/excludesDialog.ts | 104 +++++++++---- ...ner.ts => labelingProcessMouseListener.ts} | 10 +- .../outputPortAssignmentCommand.ts | 16 +- .../threatModelingAssignmentCommand.ts | 141 ++++++++++++++++++ frontend/webEditor/src/uiDialog/di.config.ts | 7 - frontend/webEditor/src/uiDialog/index.ts | 50 ------- .../src/uiDialog/showDialogCommand.ts | 46 ------ 12 files changed, 279 insertions(+), 149 deletions(-) rename frontend/webEditor/src/{uiDialog => labelingProcess}/dialog.css (52%) rename frontend/webEditor/src/labelingProcess/{ClickToAssignMouseListener.ts => labelingProcessMouseListener.ts} (82%) create mode 100644 frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts delete mode 100644 frontend/webEditor/src/uiDialog/di.config.ts delete mode 100644 frontend/webEditor/src/uiDialog/index.ts delete mode 100644 frontend/webEditor/src/uiDialog/showDialogCommand.ts diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index 250ba4cf..f1cf310a 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -17,6 +17,7 @@ "husky": "^9.1.7", "inversify": "^6.2.2", "lint-staged": "^16.2.7", + "marked": "^17.0.1", "monaco-editor": "^0.52.2", "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", @@ -2266,6 +2267,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 2b2dc125..3c90f008 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -16,6 +16,7 @@ "husky": "^9.1.7", "inversify": "^6.2.2", "lint-staged": "^16.2.7", + "marked": "^17.0.1", "monaco-editor": "^0.52.2", "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index e0e6fd84..6305d770 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -26,7 +26,6 @@ import { assignmentModule } from "./assignment/di.config"; import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; import { loadingIndicatorModule } from "./loadingIndicator/di.config"; import { labelingProcessModule } from "./labelingProcess/di.config.ts"; -import { uiDialogModule } from "./uiDialog/di.config.ts"; const container = new Container(); @@ -52,7 +51,6 @@ container.load( settingsModule, labelingProcessModule, toolPaletteModule, - uiDialogModule, constraintModule, assignmentModule, editorModeOverwritesModule, diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts index ef7dac3c..b1207c70 100644 --- a/frontend/webEditor/src/labelingProcess/di.config.ts +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -4,8 +4,9 @@ import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; import { EDITOR_TYPES } from "../editorTypes.ts"; import { OutputPortAssignmentCommand } from "./outputPortAssignmentCommand.ts"; -import { ClickToAssignMouseListener } from "./ClickToAssignMouseListener.ts"; +import { LabelingProcessMouseListener } from "./labelingProcessMouseListener.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; +import { ThreatModelingAssignmentCommand } from "./threatModelingAssignmentCommand.ts"; export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(LabelingProcessUi).toSelf().inSingletonScope(); @@ -15,8 +16,9 @@ export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(ExcludesDialog).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(ExcludesDialog); - bind(TYPES.MouseListener).to(ClickToAssignMouseListener).inSingletonScope(); + bind(TYPES.MouseListener).to(LabelingProcessMouseListener).inSingletonScope(); configureCommand({bind, isBound}, LabelingProcessCommand) + configureCommand({bind, isBound}, ThreatModelingAssignmentCommand); configureCommand({bind, isBound}, OutputPortAssignmentCommand); }) \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/dialog.css b/frontend/webEditor/src/labelingProcess/dialog.css similarity index 52% rename from frontend/webEditor/src/uiDialog/dialog.css rename to frontend/webEditor/src/labelingProcess/dialog.css index c8c9aab1..3ef619ef 100644 --- a/frontend/webEditor/src/uiDialog/dialog.css +++ b/frontend/webEditor/src/labelingProcess/dialog.css @@ -11,14 +11,12 @@ align-items: center; } -.dialog-container.hidden { - display: none; -} - .dialog { z-index: 101; position: relative; + max-width: 50%; + background-color: var(--color-background); border-radius: 5px; padding: 16px; @@ -28,4 +26,29 @@ justify-content: center; align-items: center; gap: 4px; +} + +.dialog-text { + text-align: center; +} + +.dialog-buttons { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 4px; +} + +.dialog-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: fit-content; + cursor: pointer; } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/excludesDialog.ts b/frontend/webEditor/src/labelingProcess/excludesDialog.ts index fa283c9c..78af2dc2 100644 --- a/frontend/webEditor/src/labelingProcess/excludesDialog.ts +++ b/frontend/webEditor/src/labelingProcess/excludesDialog.ts @@ -1,46 +1,100 @@ -import { AbstractDialog } from "../uiDialog"; import { ThreatModelingLabelType, ThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; +import { AbstractUIExtension, IActionDispatcher, TYPES } from "sprotty"; +import "./dialog.css"; +import { inject } from "inversify"; +import { marked } from "marked"; +import { Action } from "sprotty-protocol"; -export class ExcludesDialog extends AbstractDialog { +export type ExcludesDialogData = { + previousLabelAssignments: { labelType: ThreatModelingLabelType; labelTypeValue: ThreatModelingLabelTypeValue }[]; + newLabelAssignment: { labelType: ThreatModelingLabelType; labelTypeValue: ThreatModelingLabelTypeValue }; + confirmAction: Action +}; - private contentContainer: HTMLDivElement +export class ExcludesDialog extends AbstractUIExtension { + protected textContainer: HTMLDivElement = document.createElement("div"); + protected buttonContainer: HTMLDivElement = document.createElement("div"); - constructor( - ) { + private state?: ExcludesDialogData; + + constructor(@inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { super(); - this.contentContainer = document.createElement("div"); } id(): string { return "excludes-collision-dialog"; } - public setContent( - previousLabelAssignments: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue}[], - newLabelAssignment: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue}, - ) { - this.contentContainer.innerText = "The labels " - + previousLabelAssignments - .map(assignment => `${assignment.labelType.name}.${assignment.labelTypeValue.text}`) - .join(", ") - + " and " - + `${newLabelAssignment.labelType.name}.${newLabelAssignment.labelTypeValue.text}` - + " cannot be assigned at the same time, since they exclude each other." + containerClass(): string { + return "dialog-container"; + } + + protected initializeContents(containerElement: HTMLElement): void { + const dialog = document.createElement("div"); + dialog.classList.add("dialog"); + containerElement.appendChild(dialog); + + this.textContainer.classList.add("dialog-text"); + dialog.appendChild(this.textContainer); + + this.buttonContainer.classList.add("dialog-buttons"); + dialog.appendChild(this.buttonContainer); + + this.update(); } - protected initializeText(): HTMLElement { - return this.contentContainer + public update(state?: ExcludesDialogData) { + this.state = state; + this.updateText(); + this.updateButtons(); } - protected initializeButtons(): HTMLButtonElement[] { - const keepPreviousLabelButton = document.createElement("button") + private updateText(): void { + if (!this.state) { + this.textContainer.innerText = "Something went wrong: This dialog has no state."; + return; + } + + this.textContainer.innerHTML = marked.parse( + "This element already has the labels " + + this.state.previousLabelAssignments + .map((assignment) => `**${assignment.labelType.name}.${assignment.labelTypeValue.text}**`) + .join(", ") + + " assigned to it.\n" + + "The label " + + `**${this.state.newLabelAssignment.labelType.name}.${this.state.newLabelAssignment.labelTypeValue.text}**` + + " cannot be assigned at the same time, since they exclude each other.", + { async: false }, + ); + } + + private updateButtons(): void { + if (!this.state) { + const closeButton = document.createElement("button"); + closeButton.classList.add("dialog-button"); + closeButton.innerText = "Close"; + closeButton.addEventListener("click", () => this.hide()); + this.buttonContainer.replaceChildren(closeButton); + return; + } + + const keepPreviousLabelButton = document.createElement("button"); + keepPreviousLabelButton.classList.add("dialog-button"); keepPreviousLabelButton.innerText = `Keep previous labels`; - keepPreviousLabelButton.classList.add("labeling-process-button") + keepPreviousLabelButton.addEventListener("click", () => this.hide()); const overwriteWithNewLabelButton = document.createElement("button"); - overwriteWithNewLabelButton.innerText = `Replace with new label`; - overwriteWithNewLabelButton.classList.add("labeling-process-button") + overwriteWithNewLabelButton.classList.add("dialog-button"); + overwriteWithNewLabelButton.innerText = + "Replace with " + + `${this.state.newLabelAssignment.labelType.name}.${this.state.newLabelAssignment.labelTypeValue.text}`; + + const confirmAction = this.state.confirmAction + overwriteWithNewLabelButton.addEventListener("click", () => { + this.hide(); + this.actionDispatcher.dispatch(confirmAction) + }); - return [keepPreviousLabelButton, overwriteWithNewLabelButton]; + this.buttonContainer.replaceChildren(keepPreviousLabelButton, overwriteWithNewLabelButton); } } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts b/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts similarity index 82% rename from frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts rename to frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts index e94582e5..99232e3d 100644 --- a/frontend/webEditor/src/labelingProcess/ClickToAssignMouseListener.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts @@ -5,10 +5,10 @@ import { inject } from "inversify"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { AddLabelToOutputPortAction } from "./outputPortAssignmentCommand.ts"; import { containsDfdLabels } from "../labels/feature"; -import { AddLabelAssignmentAction } from "../labels/assignmentCommand.ts"; import { getParentWithDfdLabels } from "../labels/dragAndDrop.ts"; +import { AddThreatModelingLabelToNodeAction } from "./threatModelingAssignmentCommand.ts"; -export class ClickToAssignMouseListener extends MouseListener { +export class LabelingProcessMouseListener extends MouseListener { constructor( @inject(LabelingProcessUi) private readonly labelingProcessUi: LabelingProcessUi @@ -31,10 +31,8 @@ export class ClickToAssignMouseListener extends MouseListener { if (!dfdLabelElement) return [] if (containsDfdLabels(dfdLabelElement)) { if (!(dfdLabelElement instanceof SNodeImpl)) return []; - return [AddLabelAssignmentAction.create( - processState.activeLabel, - dfdLabelElement - )] + + return [AddThreatModelingLabelToNodeAction.create(dfdLabelElement)] } return [] diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 38c18e53..981a68ec 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -3,7 +3,6 @@ import { Command, CommandExecutionContext, CommandReturn, - IActionDispatcher, TYPES, } from "sprotty"; import { inject, injectable } from "inversify"; @@ -17,7 +16,6 @@ import { import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; -import { CreateShowDialogAction } from "../uiDialog/showDialogCommand.ts"; interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { @@ -33,7 +31,7 @@ export namespace AddLabelToOutputPortAction { return { kind: OutputPortAssignmentCommand.KIND, element, - collisionMode: collisionMode ?? 'overwrite' + collisionMode: collisionMode ?? 'askUser' }; } } @@ -49,7 +47,6 @@ export class OutputPortAssignmentCommand implements Command { @inject(TYPES.Action) private readonly action: ThreatModelingLabelAssignmentToOutputPortAction, @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, @inject(LabelingProcessUi) private readonly labelingProcessUI: LabelingProcessUi, - @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, @inject(ExcludesDialog) private readonly excludesDialog: ExcludesDialog ) {} @@ -80,8 +77,13 @@ export class OutputPortAssignmentCommand implements Command { } if (this.action.collisionMode === "askUser") { - this.actionDispatcher.dispatch(CreateShowDialogAction.create(this.excludesDialog)) - //TODO add actions on button presses + this.excludesDialog.update({ + previousLabelAssignments: collisions, + newLabelAssignment: { labelType, labelTypeValue }, + confirmAction: AddLabelToOutputPortAction.create(this.action.element, "overwrite") + }) + this.excludesDialog.show(context.root); + return context.root } @@ -163,7 +165,7 @@ function findAllCollisions( } } - //TODO what about multiple entries for the same collision?? + //TODO currently finds multiple entries for the same collision?? return collisions; } diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts new file mode 100644 index 00000000..7d8ffc49 --- /dev/null +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts @@ -0,0 +1,141 @@ +import { Action } from "sprotty-protocol"; +import { injectable, inject } from "inversify"; +import { Command, CommandExecutionContext, CommandReturn, IActionDispatcher, SNodeImpl, TYPES } from "sprotty"; +import { ContainsDfdLabels } from "../labels/feature.ts"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; +import { LabelingProcessUi } from "./labelingProcessUi.ts"; +import { ExcludesDialog } from "./excludesDialog.ts"; +import { + isThreatModelingLabelType, + isThreatModelingLabelTypeValue, + ThreatModelingLabelType, + ThreatModelingLabelTypeValue, +} from "../labels/ThreatModelingLabelType.ts"; +import { AddLabelAssignmentAction, RemoveLabelAssignmentAction } from "../labels/assignmentCommand.ts"; + +interface ThreatModelingLabelAssignmentToNodeAction extends Action { + element: ContainsDfdLabels & SNodeImpl; + collisionMode: 'overwrite' | 'askUser' +} + +export namespace AddThreatModelingLabelToNodeAction { + export function create( + element: ContainsDfdLabels & SNodeImpl, + collisionMode?: 'overwrite' | 'askUser' + ): ThreatModelingLabelAssignmentToNodeAction { + return { + kind: ThreatModelingAssignmentCommand.KIND, + element, + collisionMode: collisionMode ?? 'askUser' + }; + } +} + +@injectable() +export class ThreatModelingAssignmentCommand implements Command { + public static readonly KIND = "threatModeling-addLabelToNode"; + + constructor( + @inject(TYPES.Action) private readonly action: ThreatModelingLabelAssignmentToNodeAction, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(LabelingProcessUi) private readonly labelingProcessUI: LabelingProcessUi, + @inject(ExcludesDialog) private readonly excludesDialog: ExcludesDialog, + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + const labelProcessState = this.labelingProcessUI.getState() + if (labelProcessState.state !== "inProgress") return context.root; + + const { labelType, labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(labelProcessState.activeLabel) + if (!labelType || !labelTypeValue) return context.root; + + if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { + this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( + labelProcessState.activeLabel, + this.action.element, + )) + return context.root; + } + + const collisions = this.action.element.labels + .map(label => this.labelTypeRegistry.resolveLabelAssignment(label)) + .filter(assignedLabel => { + if (!assignedLabel.labelType + || !assignedLabel.labelTypeValue + || !isThreatModelingLabelType(assignedLabel.labelType) + || !isThreatModelingLabelTypeValue(assignedLabel.labelTypeValue) + ) { + return false; + } + + // Does a previously assigned label exclude the new label? + if (!assignedLabel.labelTypeValue.excludes.some( + (exclude) => + exclude.labelTypeId === labelType.id + && exclude.labelTypeValueId === labelTypeValue.id + )) { + return true; + } + + // Does the new label exclude the previously assigned label? + if (labelTypeValue.excludes.some( + (exclude) => + exclude.labelTypeId === assignedLabel.labelType?.id + && exclude.labelTypeValueId === assignedLabel.labelTypeValue?.id + )) { + return true; + } + + return false; + }) as { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] + // ^ Assignments that are partial or are of the wrong type are filtered out above. + // Typescript does not recognize that the filter ensures that the array only contains the correct types. + // Therefore, we need to cast the array to the correct type. + + console.error(collisions) + + if (collisions.length == 0) { + this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( + labelProcessState.activeLabel, + this.action.element, + )) + return context.root; + } + + if (this.action.collisionMode === "askUser") { + this.excludesDialog.update({ + previousLabelAssignments: collisions, + newLabelAssignment: { labelType, labelTypeValue }, + confirmAction: AddThreatModelingLabelToNodeAction.create(this.action.element, "overwrite") + }) + this.excludesDialog.show(context.root); + + return context.root + } + + //this.action.collisionMode === "overwrite" + for (const collision of collisions) { + this.actionDispatcher.dispatch(RemoveLabelAssignmentAction.create( + { labelTypeId: collision.labelType.id, labelTypeValueId: collision.labelTypeValue.id }, + this.action.element, + )) + } + this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( + labelProcessState.activeLabel, + this.action.element, + )) + + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.action.collisionMode === "askUser") return context.root; + + return context.root; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/di.config.ts b/frontend/webEditor/src/uiDialog/di.config.ts deleted file mode 100644 index 8e11d307..00000000 --- a/frontend/webEditor/src/uiDialog/di.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ContainerModule } from "inversify"; -import { configureCommand } from "sprotty"; -import { ShowDialogCommand } from "./showDialogCommand.ts"; - -export const uiDialogModule = new ContainerModule((bind, _, isBound) => { - configureCommand({bind, isBound}, ShowDialogCommand); -}) \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/index.ts b/frontend/webEditor/src/uiDialog/index.ts deleted file mode 100644 index 4ccc6248..00000000 --- a/frontend/webEditor/src/uiDialog/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AbstractUIExtension, SModelRootImpl } from "sprotty"; -import './dialog.css' -import { injectable } from "inversify"; - -/** - * Base class for a dialog. The generic type parameter `T` is used to specify the possible return values. - * - * The return value of the dialog (i.e. which button has been pressed) is then-able by calling `getResult()`. - * Do NOT await the result inside a Command, since Sprotty executes only one command at a time. - */ -@injectable() -export abstract class AbstractDialog extends AbstractUIExtension { - - public constructor() { - super(); - } - - containerClass(): string { - return "dialog-container"; - } - - override hide() { - super.hide(); - this.containerElement.classList.add("hidden"); - } - - override show(root: Readonly, ...contextElementIds: string[]): void { - super.show(root, ...contextElementIds) - this.containerElement.classList.remove("hidden"); - } - - protected initializeContents(containerElement: HTMLElement): void { - const dialog = document.createElement("div"); - dialog.classList.add("dialog"); - containerElement.appendChild(dialog); - - dialog.appendChild(this.initializeText()); - - const actions = this.initializeButtons() - actions.forEach(button => { - button.addEventListener("click", () => { - this.hide(); - }) - dialog.appendChild(button) - }) - } - - protected abstract initializeText(): HTMLElement; - protected abstract initializeButtons(): HTMLButtonElement[]; -} \ No newline at end of file diff --git a/frontend/webEditor/src/uiDialog/showDialogCommand.ts b/frontend/webEditor/src/uiDialog/showDialogCommand.ts deleted file mode 100644 index 172dbd1a..00000000 --- a/frontend/webEditor/src/uiDialog/showDialogCommand.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Action } from "sprotty-protocol"; -import { - Command, - CommandExecutionContext, - CommandReturn, - TYPES, -} from "sprotty"; -import { AbstractDialog } from "./index.ts"; -import { inject, injectable } from "inversify"; - -export interface ShowDialogAction extends Action { - dialog: T -} - -export namespace CreateShowDialogAction { - export function create(dialog: T): ShowDialogAction { - return { - kind: ShowDialogCommand.KIND, - dialog - } - } -} - -@injectable() -export class ShowDialogCommand implements Command { - public static readonly KIND = "showDialog" - - constructor( - @inject(TYPES.Action) private readonly action: ShowDialogAction, - ) {} - - execute(context: CommandExecutionContext): CommandReturn { - this.action.dialog.show(context.root) - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - this.action.dialog.show(context.root) - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - this.action.dialog.hide(); - return context.root; - } -} \ No newline at end of file From 2ddca8d6e72e2d0da5413f2539f4293e09858317 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 14:58:17 +0100 Subject: [PATCH 09/23] Fix threat modeling label assignments to nodes --- .../src/labelingProcess/excludesDialog.ts | 4 ++ .../threatModelingAssignmentCommand.ts | 69 +++++++++---------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/excludesDialog.ts b/frontend/webEditor/src/labelingProcess/excludesDialog.ts index 78af2dc2..aefa4750 100644 --- a/frontend/webEditor/src/labelingProcess/excludesDialog.ts +++ b/frontend/webEditor/src/labelingProcess/excludesDialog.ts @@ -44,6 +44,10 @@ export class ExcludesDialog extends AbstractUIExtension { } public update(state?: ExcludesDialogData) { + if (!this.containerElement) { + if (!this.initialize()) return; + } + this.state = state; this.updateText(); this.updateButtons(); diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts index 7d8ffc49..3f44bcc8 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts @@ -58,44 +58,23 @@ export class ThreatModelingAssignmentCommand implements Command { return context.root; } - const collisions = this.action.element.labels + const possibleCollisions = this.action.element.labels .map(label => this.labelTypeRegistry.resolveLabelAssignment(label)) - .filter(assignedLabel => { - if (!assignedLabel.labelType - || !assignedLabel.labelTypeValue - || !isThreatModelingLabelType(assignedLabel.labelType) - || !isThreatModelingLabelTypeValue(assignedLabel.labelTypeValue) - ) { - return false; - } - - // Does a previously assigned label exclude the new label? - if (!assignedLabel.labelTypeValue.excludes.some( - (exclude) => - exclude.labelTypeId === labelType.id - && exclude.labelTypeValueId === labelTypeValue.id - )) { - return true; - } - - // Does the new label exclude the previously assigned label? - if (labelTypeValue.excludes.some( - (exclude) => - exclude.labelTypeId === assignedLabel.labelType?.id - && exclude.labelTypeValueId === assignedLabel.labelTypeValue?.id - )) { - return true; - } - - return false; - }) as { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] - // ^ Assignments that are partial or are of the wrong type are filtered out above. - // Typescript does not recognize that the filter ensures that the array only contains the correct types. - // Therefore, we need to cast the array to the correct type. - + .filter((label) : label is Required<{ labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }> => + label.labelType !== undefined + && label.labelTypeValue !== undefined + ) + .filter(label => + isThreatModelingLabelType(label.labelType) + && isThreatModelingLabelTypeValue(label.labelTypeValue) + ) + const collisions = findCollisions({ labelType, labelTypeValue }, possibleCollisions ) + + console.error(this.action.element.labels) + console.error(possibleCollisions) console.error(collisions) - if (collisions.length == 0) { + if (collisions .length == 0) { this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( labelProcessState.activeLabel, this.action.element, @@ -105,7 +84,7 @@ export class ThreatModelingAssignmentCommand implements Command { if (this.action.collisionMode === "askUser") { this.excludesDialog.update({ - previousLabelAssignments: collisions, + previousLabelAssignments: collisions , newLabelAssignment: { labelType, labelTypeValue }, confirmAction: AddThreatModelingLabelToNodeAction.create(this.action.element, "overwrite") }) @@ -115,7 +94,7 @@ export class ThreatModelingAssignmentCommand implements Command { } //this.action.collisionMode === "overwrite" - for (const collision of collisions) { + for (const collision of collisions ) { this.actionDispatcher.dispatch(RemoveLabelAssignmentAction.create( { labelTypeId: collision.labelType.id, labelTypeValueId: collision.labelTypeValue.id }, this.action.element, @@ -138,4 +117,20 @@ export class ThreatModelingAssignmentCommand implements Command { return context.root; } +} + +function findCollisions( + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, + assigned: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] +): { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] { + return assigned.filter(existing => + candidate.labelTypeValue.excludes.some(exclude => + exclude.labelTypeId === existing.labelType.id + && exclude.labelTypeValueId === existing.labelTypeValue.id + ) + || existing.labelTypeValue.excludes.some(exclude => + exclude.labelTypeId === candidate.labelType.id + && exclude.labelTypeValueId === candidate.labelTypeValue.id + ) + ) } \ No newline at end of file From c5066ea2deffb4587ee19a24ea08d18c90130adf Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 16:19:43 +0100 Subject: [PATCH 10/23] Add additional information on hover --- .../src/labelingProcess/labelingProcessUI.css | 65 +++++++++++++++---- .../src/labelingProcess/labelingProcessUi.ts | 37 +++++++++-- .../threatModelingAssignmentCommand.ts | 4 -- .../src/labels/ThreatModelingLabelType.ts | 4 +- 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index f1f64f81..be67c391 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -16,17 +16,56 @@ /* Make text of the elements non-selectable */ -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ user-select: none; -} - -.labeling-process-button { - background-color: green; - color: white; - border: none; - border-radius: 8px; - padding: 5px 10px; - text-align: center; - text-decoration: none; - display: inline-block; - width: fit-content; - cursor: pointer; + + .labeling-process-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: fit-content; + cursor: pointer; + } + + .additional-information-icon { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); + background-repeat: no-repeat; + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; + margin-left: 4px; + margin-right: 4px; + position: relative; + cursor: pointer; + } + + .additional-information-container { + position: absolute; + top: 120%; + left: 50%; + transform: translateX(-50%); + z-index: 50; + + background-color: var(--color-background); + color: var(--color-foreground); + padding: 4px 16px; + border: 2px solid var(--color-primary); + border-radius: 10px; + font-size: 12px; + white-space: nowrap; + + opacity: 0; + pointer-events: none; /* prevents flicker */ + } + + .additional-information-icon:hover .additional-information-container { + opacity: 1; + } } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index 272380eb..49ba1d4b 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -12,7 +12,8 @@ import { AnalyzeAction } from "../serialize/analyze.ts"; import { SelectConstraintsAction } from "../constraint/selection.ts"; import { ConstraintRegistry } from "../constraint/constraintRegistry.ts"; import { SaveThreatsTableAction } from "../serialize/saveThreatsTable.ts"; -import { isThreatModelingLabelType } from "../labels/ThreatModelingLabelType.ts"; +import { isThreatModelingLabelType, isThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; +import { marked } from "marked"; export type LabelingProcessState = { state: 'pending' } @@ -70,12 +71,19 @@ export class LabelingProcessUi extends AbstractUIExtension { } else { let targetElement = "" if (isThreatModelingLabelType(labelType)) { - targetElement = labelType.intendedFor === "Vertex" ? "node" : "output pin" + targetElement = labelType.intendedFor === "Vertex" ? "a node" : "an output pin" } else { - targetElement = "node or output pin" + targetElement = "a node or output pin" } - text.innerText = `Right click to assign ${labelType.name}.${labelTypeValue.text} to a ${targetElement}` + const labelHTML = document.createElement("strong") + labelHTML.innerText = `${labelType.name}.${labelTypeValue.text}` + + text.append( + `Right click ${targetElement} to assign `, + labelHTML, + this.generateAdditionalInformation() ?? '', + ) } const nextStepButton = document.createElement('button') @@ -115,6 +123,27 @@ export class LabelingProcessUi extends AbstractUIExtension { this.containerElement.replaceChildren(text, finalStepsButton) } + private generateAdditionalInformation(): HTMLElement | undefined { + if (this.state.state !== "inProgress") return; + + const { labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(this.state.activeLabel) + if (!labelTypeValue + || !isThreatModelingLabelTypeValue(labelTypeValue) + || !labelTypeValue.additionalInformation + ) return; + + const icon = document.createElement('div') + icon.classList.add('additional-information-icon') + + const container = document.createElement('div') + container.classList.add('additional-information-container') + + icon.appendChild(container) + container.innerHTML = marked.parse(labelTypeValue.additionalInformation, { async: false }) + + return icon; + } + public getState(): LabelingProcessState { return this.state; } diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts index 3f44bcc8..46113383 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts @@ -70,10 +70,6 @@ export class ThreatModelingAssignmentCommand implements Command { ) const collisions = findCollisions({ labelType, labelTypeValue }, possibleCollisions ) - console.error(this.action.element.labels) - console.error(possibleCollisions) - console.error(collisions) - if (collisions .length == 0) { this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( labelProcessState.activeLabel, diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts index 12980ce4..484041ce 100644 --- a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -6,8 +6,7 @@ export interface ThreatModelingLabelType extends LabelType { export interface ThreatModelingLabelTypeValue extends LabelTypeValue { excludes: LabelAssignment[] - //defaultPinBehavior: string, - additionalInformation: string[] + additionalInformation?: string } export function isThreatModelingLabelType(labelType: LabelType): labelType is ThreatModelingLabelType { @@ -16,5 +15,4 @@ export function isThreatModelingLabelType(labelType: LabelType): labelType is Th export function isThreatModelingLabelTypeValue(labelTypeValue: LabelTypeValue): labelTypeValue is ThreatModelingLabelTypeValue { return "excludes" in labelTypeValue - && "additionalInformation" in labelTypeValue } \ No newline at end of file From fd12a1e579a565a6eafe6d810009056eaf8423f8 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 16:57:12 +0100 Subject: [PATCH 11/23] Fix dark mode colors for additional information --- .../src/labelingProcess/labelingProcessUI.css | 12 ++++++------ .../src/labelingProcess/labelingProcessUi.ts | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index be67c391..500adfee 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -31,6 +31,11 @@ } .additional-information-icon { + position: relative; + cursor: pointer; + } + + .additional-information-icon::before { content: ""; background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); background-repeat: no-repeat; @@ -42,8 +47,6 @@ vertical-align: text-top; margin-left: 4px; margin-right: 4px; - position: relative; - cursor: pointer; } .additional-information-container { @@ -53,11 +56,8 @@ transform: translateX(-50%); z-index: 50; - background-color: var(--color-background); - color: var(--color-foreground); padding: 4px 16px; - border: 2px solid var(--color-primary); - border-radius: 10px; + border: 1px solid var(--color-foreground); font-size: 12px; white-space: nowrap; diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index 49ba1d4b..ddadc487 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -83,6 +83,7 @@ export class LabelingProcessUi extends AbstractUIExtension { `Right click ${targetElement} to assign `, labelHTML, this.generateAdditionalInformation() ?? '', + ' to it.' ) } @@ -132,11 +133,11 @@ export class LabelingProcessUi extends AbstractUIExtension { || !labelTypeValue.additionalInformation ) return; - const icon = document.createElement('div') + const icon = document.createElement('span') icon.classList.add('additional-information-icon') const container = document.createElement('div') - container.classList.add('additional-information-container') + container.classList.add('additional-information-container', 'ui-float') icon.appendChild(container) container.innerHTML = marked.parse(labelTypeValue.additionalInformation, { async: false }) From b7678ef05fff85e1788dbc5f6e190ee8fc9503cb Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 17:05:04 +0100 Subject: [PATCH 12/23] Reset labeling process when loading a new threat modeling file --- .../labelingProcess/labelingProcessCommand.ts | 40 +++++++++++-------- .../src/labelingProcess/labelingProcessUi.ts | 1 + .../src/serialize/loadThreatModelingFile.ts | 18 ++++++--- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index f34f02f7..d0540aca 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -18,6 +18,15 @@ export interface LabelingProcessAction extends Action { state: LabelingProcessState } +export namespace ResetLabelingProcessAction { + export function create(): LabelingProcessAction { + return { + kind: LabelingProcessCommand.KIND, + state: { state: 'pending' } + } + } +} + export namespace BeginLabelingProcessAction { export function create( labelTypeRegistry: LabelTypeRegistry @@ -109,22 +118,21 @@ export class LabelingProcessCommand implements Command { } highlightShapes(context: CommandExecutionContext) { - if (this.action.state.state !== "inProgress") return; - - const { labelType } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) - if (!labelType) return; - - let nodeColor = "" - let outputPortColor = "" - if (!isThreatModelingLabelType(labelType)) { - nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR - outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR - } else if (labelType.intendedFor === "Vertex") { - nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR - outputPortColor = DfdOutputPortImpl.PORT_COLOR - } else { - nodeColor = DfdNodeImpl.NODE_COLOR - outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + let nodeColor = DfdNodeImpl.NODE_COLOR + let outputPortColor = DfdOutputPortImpl.PORT_COLOR + + if (this.action.state.state === "inProgress") { + const { labelType } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) + if (!labelType) return; + + if (!isThreatModelingLabelType(labelType)) { + nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR + outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + } else if (labelType.intendedFor === "Vertex") { + nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR + } else { + outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + } } getAllElements(context.root.children) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index ddadc487..2709faff 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -58,6 +58,7 @@ export class LabelingProcessUi extends AbstractUIExtension { private showPendingContents(): void { this.containerElement.classList.remove("ui-float") + this.containerElement.replaceChildren('') } private showInProgressContents(): void { diff --git a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts index a2c310f0..f931f524 100644 --- a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts +++ b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts @@ -1,7 +1,8 @@ import { + ActionDispatcher, Command, CommandExecutionContext, - CommandReturn, + CommandReturn, IActionDispatcher, ILogger, ISnapper, SModelElementImpl, @@ -22,6 +23,7 @@ import { getAllElements } from "../labels/assignmentCommand.ts"; import { ContainsDfdLabels, containsDfdLabels } from "../labels/feature.ts"; import { snapPortsOfNode } from "../diagram/ports/portSnapper.ts"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; +import { ResetLabelingProcessAction } from "../labelingProcess/labelingProcessCommand.ts"; // Replaces the type of the `values` of a `LabelType` with a subclass of `LabelTypeValue` type OverwriteLabelTypeValueType = Omit & { values: S[] } @@ -55,11 +57,12 @@ export class LoadThreatModelingFileCommand extends Command { constructor( @inject(TYPES.Action) _: Action, - @inject(TYPES.ILogger) private logger: ILogger, - @inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry, - @inject(LoadingIndicator) private loadingIndicator: LoadingIndicator, - @inject(TYPES.ISnapper) private snapper: ISnapper + @inject(TYPES.ILogger) private readonly logger: ILogger, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(LoadingIndicator) private readonly loadingIndicator: LoadingIndicator, + @inject(TYPES.ISnapper) private readonly snapper: ISnapper, + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, ) { super(); } @@ -127,6 +130,9 @@ export class LoadThreatModelingFileCommand extends Command { this.constraintRegistry.setConstraintsFromArray(newConstraints); this.logger.info(this, "Constraints loaded successfully"); + //Reset labeling process + this.actionDispatcher.dispatch(ResetLabelingProcessAction.create()) + this.loadingIndicator.hide(); return context.root; } From c4975a1bc22a258dfdc7e9aa859ecc9c8e9b5b5d Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 20:10:42 +0100 Subject: [PATCH 13/23] Clean up some code --- .../outputPortAssignmentCommand.ts | 37 +++++++++---------- .../src/serialize/loadThreatModelingFile.ts | 1 - 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 981a68ec..39f32400 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -67,10 +67,10 @@ export class OutputPortAssignmentCommand implements Command { let lines = this.previousBehavior .split("\n") .map(line => line.trim()); - const collisions = findAllCollisions(lines, labelType, labelTypeValue, this.labelTypeRegistry) + const collisions = findAllCollisions(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) if (collisions.length == 0) { - lines = addLabelAssignment(lines, labelType, labelTypeValue, this.labelTypeRegistry) + lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) this.newBehavior = lines.join("\n") this.action.element.setBehavior(this.newBehavior); return context.root; @@ -89,9 +89,9 @@ export class OutputPortAssignmentCommand implements Command { //this.action.collisionMode === "overwrite" for (const collision of collisions) { - lines = removeLabelAssignment(lines, collision.labelType, collision.labelTypeValue) + lines = removeLabelAssignment(lines, { labelType: collision.labelType, labelTypeValue: collision.labelTypeValue }) } - lines = addLabelAssignment(lines, labelType, labelTypeValue, this.labelTypeRegistry) + lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) this.newBehavior = lines.join("\n") this.action.element.setBehavior(this.newBehavior); @@ -115,17 +115,16 @@ export class OutputPortAssignmentCommand implements Command { function findAllCollisions( portBehavior: string[], - newLabelType: ThreatModelingLabelType, - newLabelTypeValue: ThreatModelingLabelTypeValue, + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, labelTypeRegistry: LabelTypeRegistry ): { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] { - const collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] = []; + const collisions: Set<{ labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }> = new Set(); for (let i = 0; i < portBehavior.length; i++) { const line = portBehavior[i] //Search for a previous assignment that excludes the new assignment - if (line.match(`unset ${newLabelType.name}.${newLabelTypeValue.text}`)) { + if (line.match(`unset ${candidate.labelType.name}.${candidate.labelTypeValue.text}`)) { //Searches for the previous `set` assignment //Assumes that each `set` assignment is directly followed by their `unset` (`exclude`) assignments for (let j = i; j >= 0; j--) { @@ -144,13 +143,13 @@ function findAllCollisions( if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) continue; - collisions.push({ labelType, labelTypeValue }) + collisions.add({ labelType, labelTypeValue }) } } } //Search for a previous assignment that is excluded by the new assignment - for (const exclude of newLabelTypeValue.excludes) { + for (const exclude of candidate.labelTypeValue.excludes) { const { labelType, labelTypeValue } = labelTypeRegistry.resolveLabelAssignment(exclude); if ( !labelType @@ -160,13 +159,13 @@ function findAllCollisions( ) continue; if (line.match(`set ${labelType.name}.${labelTypeValue.text}`)) { - collisions.push({ labelType, labelTypeValue }); + collisions.add({ labelType, labelTypeValue }); } } } - //TODO currently finds multiple entries for the same collision?? - return collisions; + collisions.delete(candidate) + return Array.from(collisions); } /** @@ -174,12 +173,11 @@ function findAllCollisions( */ function addLabelAssignment( portBehavior: string[], - labelType: ThreatModelingLabelType, - labelTypeValue: ThreatModelingLabelTypeValue, + toAdd: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, labelTypeRegistry: LabelTypeRegistry ): string[] { - const setAssignment = `set ${labelType.name}.${labelTypeValue.text}` - const unsetAssignments: string[] = labelTypeValue.excludes.map((exclude) => { + const setAssignment = `set ${toAdd.labelType.name}.${toAdd.labelTypeValue.text}` + const unsetAssignments: string[] = toAdd.labelTypeValue.excludes.map((exclude) => { const { labelType, labelTypeValue } = labelTypeRegistry.resolveLabelAssignment(exclude) if ( !labelType || !labelTypeValue ) return ""; return `unset ${labelType.name}.${labelTypeValue.text}` @@ -193,13 +191,12 @@ function addLabelAssignment( */ function removeLabelAssignment( portBehavior: string[], - labelType: ThreatModelingLabelType, - labelTypeValue: ThreatModelingLabelTypeValue + toRemove: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, ): string[] { let removing = false; return portBehavior.filter(line => { - if (line === `set ${labelType.name}.${labelTypeValue.text}`) { + if (line === `set ${toRemove.labelType.name}.${toRemove.labelTypeValue.text}`) { removing = true; return false; } diff --git a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts index f931f524..b02236ea 100644 --- a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts +++ b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts @@ -1,5 +1,4 @@ import { - ActionDispatcher, Command, CommandExecutionContext, CommandReturn, IActionDispatcher, From 4e6e1eebedc36c51d243846f918b210ea24cda4c Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 21:19:37 +0100 Subject: [PATCH 14/23] Add color to assigned elements; Add information about colors --- .../labelingProcess/labelingProcessCommand.ts | 9 +++-- .../src/labelingProcess/labelingProcessUI.css | 6 ++++ .../src/labelingProcess/labelingProcessUi.ts | 36 +++++++++++++++++++ .../outputPortAssignmentCommand.ts | 8 +++++ .../threatModelingAssignmentCommand.ts | 10 ++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index d0540aca..760c21aa 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -84,7 +84,6 @@ export namespace CompleteLabelingProcessAction { export class LabelingProcessCommand implements Command { public static readonly KIND = "labelingProcess" - public static readonly HIGHLIGHT_COLOR = '#00FF00' private previousState?: LabelingProcessState = undefined; @@ -126,12 +125,12 @@ export class LabelingProcessCommand implements Command { if (!labelType) return; if (!isThreatModelingLabelType(labelType)) { - nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR - outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + nodeColor = LabelingProcessUi.ASSIGNABLE_COLOR + outputPortColor = LabelingProcessUi.ASSIGNABLE_COLOR } else if (labelType.intendedFor === "Vertex") { - nodeColor = LabelingProcessCommand.HIGHLIGHT_COLOR + nodeColor = LabelingProcessUi.ASSIGNABLE_COLOR } else { - outputPortColor = LabelingProcessCommand.HIGHLIGHT_COLOR + outputPortColor = LabelingProcessUi.ASSIGNABLE_COLOR } } diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css index 500adfee..4c899886 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUI.css +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUI.css @@ -68,4 +68,10 @@ .additional-information-icon:hover .additional-information-container { opacity: 1; } + + .additional-information-colors-explanation { + display: flex; + flex-direction: column; + gap: 2px; + } } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index 2709faff..c72256bb 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -24,6 +24,12 @@ export type LabelingProcessState export class LabelingProcessUi extends AbstractUIExtension { static readonly ID = "labeling-process-ui"; + static readonly ASSIGNABLE_COLOR = "#00ff00" + static readonly ALREADY_ASSIGNED_COLOR = "#ffff00" + static readonly COLLISION_COLOR = "#ff0000" + // ^ The colors are defined here, but the UI elements are colored during the 'LabelingProcessCommand' and the + // respective AssignmentCommand + private state: LabelingProcessState; constructor( @@ -140,8 +146,18 @@ export class LabelingProcessUi extends AbstractUIExtension { const container = document.createElement('div') container.classList.add('additional-information-container', 'ui-float') + const explanation = document.createElement('div') + explanation.classList.add('additional-information-colors-explanation') + explanation.append( + 'Colors: ', + createColorLabel(LabelingProcessUi.ASSIGNABLE_COLOR, 'Label is assignable'), + createColorLabel(LabelingProcessUi.ALREADY_ASSIGNED_COLOR, 'Label is already assigned'), + createColorLabel(LabelingProcessUi.COLLISION_COLOR, 'Label will collide'), + ) + icon.appendChild(container) container.innerHTML = marked.parse(labelTypeValue.additionalInformation, { async: false }) + container.appendChild(explanation) return icon; } @@ -154,4 +170,24 @@ export class LabelingProcessUi extends AbstractUIExtension { this.state = state; this.updateContents(); } +} + +function createColorLabel(color: string, text: string): HTMLElement { + const container = document.createElement('span') + container.style.display = 'flex' + container.style.flexDirection = 'row' + container.style.gap = '4px' + + const box = document.createElement('div') + box.style.background = color + box.style.width = '12px' + box.style.height = '12px' + box.style.display = 'inline-block' + box.style.verticalAlign = 'middle' + + const label = document.createElement('span') + label.innerText = text + + container.append(box, label) + return container } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 39f32400..1888501b 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -61,6 +61,7 @@ export class OutputPortAssignmentCommand implements Command { if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) return context.root; } @@ -69,10 +70,16 @@ export class OutputPortAssignmentCommand implements Command { .map(line => line.trim()); const collisions = findAllCollisions(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) + console.error(this.previousBehavior) + console.error(`${labelType.name}.${labelTypeValue.text}`) + console.error(labelTypeValue.excludes) + console.error(collisions) + if (collisions.length == 0) { lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) this.newBehavior = lines.join("\n") this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) return context.root; } @@ -94,6 +101,7 @@ export class OutputPortAssignmentCommand implements Command { lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) this.newBehavior = lines.join("\n") this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) return context.root } diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts index 46113383..ed4c973e 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts @@ -12,6 +12,7 @@ import { ThreatModelingLabelTypeValue, } from "../labels/ThreatModelingLabelType.ts"; import { AddLabelAssignmentAction, RemoveLabelAssignmentAction } from "../labels/assignmentCommand.ts"; +import { DfdNodeImpl } from "../diagram/nodes/common.ts"; interface ThreatModelingLabelAssignmentToNodeAction extends Action { element: ContainsDfdLabels & SNodeImpl; @@ -55,6 +56,9 @@ export class ThreatModelingAssignmentCommand implements Command { labelProcessState.activeLabel, this.action.element, )) + if (this.action.element instanceof DfdNodeImpl) { + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } return context.root; } @@ -75,6 +79,9 @@ export class ThreatModelingAssignmentCommand implements Command { labelProcessState.activeLabel, this.action.element, )) + if (this.action.element instanceof DfdNodeImpl) { + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } return context.root; } @@ -100,6 +107,9 @@ export class ThreatModelingAssignmentCommand implements Command { labelProcessState.activeLabel, this.action.element, )) + if (this.action.element instanceof DfdNodeImpl) { + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } return context.root; } From ba56eab28197f76d760f76ea0bfddb69247f586d Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Fri, 23 Jan 2026 21:33:25 +0100 Subject: [PATCH 15/23] Refactor complex commands --- .../outputPortAssignmentCommand.ts | 74 ++++++++++----- .../threatModelingAssignmentCommand.ts | 91 +++++++++++-------- 2 files changed, 105 insertions(+), 60 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts index 1888501b..0bf62161 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts @@ -16,6 +16,7 @@ import { import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; +import { LabelType, LabelTypeValue } from "../labels/LabelType.ts"; interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { @@ -59,9 +60,7 @@ export class OutputPortAssignmentCommand implements Command { this.previousBehavior = this.action.element.getBehavior() if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { - this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` - this.action.element.setBehavior(this.newBehavior); - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + this.handleNonThreatModelingCase(labelType, labelTypeValue); return context.root; } @@ -76,33 +75,16 @@ export class OutputPortAssignmentCommand implements Command { console.error(collisions) if (collisions.length == 0) { - lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) - this.newBehavior = lines.join("\n") - this.action.element.setBehavior(this.newBehavior); - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + this.handleSimpleCase(lines, { labelType, labelTypeValue }) return context.root; } if (this.action.collisionMode === "askUser") { - this.excludesDialog.update({ - previousLabelAssignments: collisions, - newLabelAssignment: { labelType, labelTypeValue }, - confirmAction: AddLabelToOutputPortAction.create(this.action.element, "overwrite") - }) - this.excludesDialog.show(context.root); - + this.handleAskUser({ labelType, labelTypeValue }, collisions, context) return context.root } - //this.action.collisionMode === "overwrite" - for (const collision of collisions) { - lines = removeLabelAssignment(lines, { labelType: collision.labelType, labelTypeValue: collision.labelTypeValue }) - } - lines = addLabelAssignment(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) - this.newBehavior = lines.join("\n") - this.action.element.setBehavior(this.newBehavior); - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) - + this.handleOverwrite(lines, { labelType, labelTypeValue }, collisions) return context.root } @@ -119,6 +101,52 @@ export class OutputPortAssignmentCommand implements Command { this.action.element.setBehavior(this.previousBehavior); return context.root; } + + private handleNonThreatModelingCase( + labelType: LabelType, + labelTypeValue: LabelTypeValue, + ) { + this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` + this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } + + private handleSimpleCase( + lines: string[], + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, + ) { + lines = addLabelAssignment(lines, candidate, this.labelTypeRegistry) + this.newBehavior = lines.join("\n") + this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } + + private handleAskUser( + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, + collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[], + context: CommandExecutionContext + ) { + this.excludesDialog.update({ + previousLabelAssignments: collisions, + newLabelAssignment: candidate, + confirmAction: AddLabelToOutputPortAction.create(this.action.element, "overwrite") + }) + this.excludesDialog.show(context.root); + } + + private handleOverwrite( + lines: string[], + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, + collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[], + ) { + for (const collision of collisions) { + lines = removeLabelAssignment(lines, { labelType: collision.labelType, labelTypeValue: collision.labelTypeValue }) + } + lines = addLabelAssignment(lines, candidate, this.labelTypeRegistry) + this.newBehavior = lines.join("\n") + this.action.element.setBehavior(this.newBehavior); + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } } function findAllCollisions( diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts index ed4c973e..66cc2f18 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts @@ -3,7 +3,7 @@ import { injectable, inject } from "inversify"; import { Command, CommandExecutionContext, CommandReturn, IActionDispatcher, SNodeImpl, TYPES } from "sprotty"; import { ContainsDfdLabels } from "../labels/feature.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; -import { LabelingProcessUi } from "./labelingProcessUi.ts"; +import { LabelingProcessState, LabelingProcessUi } from "./labelingProcessUi.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; import { isThreatModelingLabelType, @@ -52,13 +52,7 @@ export class ThreatModelingAssignmentCommand implements Command { if (!labelType || !labelTypeValue) return context.root; if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { - this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( - labelProcessState.activeLabel, - this.action.element, - )) - if (this.action.element instanceof DfdNodeImpl) { - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) - } + this.handleSimpleCase(labelProcessState) return context.root; } @@ -74,30 +68,65 @@ export class ThreatModelingAssignmentCommand implements Command { ) const collisions = findCollisions({ labelType, labelTypeValue }, possibleCollisions ) - if (collisions .length == 0) { - this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( - labelProcessState.activeLabel, - this.action.element, - )) - if (this.action.element instanceof DfdNodeImpl) { - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) - } + if (collisions.length == 0) { + this.handleSimpleCase(labelProcessState) return context.root; } if (this.action.collisionMode === "askUser") { - this.excludesDialog.update({ - previousLabelAssignments: collisions , - newLabelAssignment: { labelType, labelTypeValue }, - confirmAction: AddThreatModelingLabelToNodeAction.create(this.action.element, "overwrite") - }) - this.excludesDialog.show(context.root); - + this.handleAskUser( + { labelType, labelTypeValue }, + collisions, + context + ) return context.root } - //this.action.collisionMode === "overwrite" - for (const collision of collisions ) { + this.handleOverwrite(labelProcessState, collisions) + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.action.collisionMode === "askUser") return context.root; + + return context.root; + } + + private handleSimpleCase( + labelProcessState: LabelingProcessState & { state: "inProgress" }, + ) { + this.actionDispatcher.dispatch(AddLabelAssignmentAction.create( + labelProcessState.activeLabel, + this.action.element, + )) + if (this.action.element instanceof DfdNodeImpl) { + this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + } + } + + private handleAskUser( + candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, + collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[], + context: CommandExecutionContext + ) { + this.excludesDialog.update({ + previousLabelAssignments: collisions , + newLabelAssignment: candidate, + confirmAction: AddThreatModelingLabelToNodeAction.create(this.action.element, "overwrite") + }) + + this.excludesDialog.show(context.root); + } + + private handleOverwrite( + labelProcessState: LabelingProcessState & { state: "inProgress" }, + collisions: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] + ) { + for (const collision of collisions) { this.actionDispatcher.dispatch(RemoveLabelAssignmentAction.create( { labelTypeId: collision.labelType.id, labelTypeValueId: collision.labelTypeValue.id }, this.action.element, @@ -110,18 +139,6 @@ export class ThreatModelingAssignmentCommand implements Command { if (this.action.element instanceof DfdNodeImpl) { this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) } - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - if (this.action.collisionMode === "askUser") return context.root; - - return context.root; } } From fd5cad423694349798e4a72f0a2306bb7b667162 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Sun, 25 Jan 2026 11:58:35 +0100 Subject: [PATCH 16/23] Clean up code; Fix bug where adding behavior to empty behavior resulted in invalid behavior --- .../src/labelingProcess/di.config.ts | 8 ++--- .../labelingProcessMouseListener.ts | 4 +-- ...> threatModelingLabelAssignmentCommand.ts} | 15 +++------ ...lingLabelAssignmentToOutputPortCommand.ts} | 31 +++++++++++-------- 4 files changed, 29 insertions(+), 29 deletions(-) rename frontend/webEditor/src/labelingProcess/{threatModelingAssignmentCommand.ts => threatModelingLabelAssignmentCommand.ts} (94%) rename frontend/webEditor/src/labelingProcess/{outputPortAssignmentCommand.ts => threatModelingLabelAssignmentToOutputPortCommand.ts} (91%) diff --git a/frontend/webEditor/src/labelingProcess/di.config.ts b/frontend/webEditor/src/labelingProcess/di.config.ts index b1207c70..fe480af0 100644 --- a/frontend/webEditor/src/labelingProcess/di.config.ts +++ b/frontend/webEditor/src/labelingProcess/di.config.ts @@ -3,10 +3,10 @@ import { configureCommand, TYPES } from "sprotty"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelingProcessCommand } from "./labelingProcessCommand.ts"; import { EDITOR_TYPES } from "../editorTypes.ts"; -import { OutputPortAssignmentCommand } from "./outputPortAssignmentCommand.ts"; +import { ThreatModelingLabelAssignmentToOutputPortCommand } from "./threatModelingLabelAssignmentToOutputPortCommand.ts"; import { LabelingProcessMouseListener } from "./labelingProcessMouseListener.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; -import { ThreatModelingAssignmentCommand } from "./threatModelingAssignmentCommand.ts"; +import { ThreatModelingLabelAssignmentCommand } from "./threatModelingLabelAssignmentCommand.ts"; export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(LabelingProcessUi).toSelf().inSingletonScope(); @@ -19,6 +19,6 @@ export const labelingProcessModule = new ContainerModule((bind, _, isBound) => { bind(TYPES.MouseListener).to(LabelingProcessMouseListener).inSingletonScope(); configureCommand({bind, isBound}, LabelingProcessCommand) - configureCommand({bind, isBound}, ThreatModelingAssignmentCommand); - configureCommand({bind, isBound}, OutputPortAssignmentCommand); + configureCommand({bind, isBound}, ThreatModelingLabelAssignmentCommand); + configureCommand({bind, isBound}, ThreatModelingLabelAssignmentToOutputPortCommand); }) \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts b/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts index 99232e3d..dcadb7bb 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessMouseListener.ts @@ -3,10 +3,10 @@ import { Action } from "sprotty-protocol/lib/actions"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; import { inject } from "inversify"; import { LabelingProcessUi } from "./labelingProcessUi.ts"; -import { AddLabelToOutputPortAction } from "./outputPortAssignmentCommand.ts"; +import { AddLabelToOutputPortAction } from "./threatModelingLabelAssignmentToOutputPortCommand.ts"; import { containsDfdLabels } from "../labels/feature"; import { getParentWithDfdLabels } from "../labels/dragAndDrop.ts"; -import { AddThreatModelingLabelToNodeAction } from "./threatModelingAssignmentCommand.ts"; +import { AddThreatModelingLabelToNodeAction } from "./threatModelingLabelAssignmentCommand.ts"; export class LabelingProcessMouseListener extends MouseListener { diff --git a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts similarity index 94% rename from frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts rename to frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts index 66cc2f18..e960cce2 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts @@ -25,7 +25,7 @@ export namespace AddThreatModelingLabelToNodeAction { collisionMode?: 'overwrite' | 'askUser' ): ThreatModelingLabelAssignmentToNodeAction { return { - kind: ThreatModelingAssignmentCommand.KIND, + kind: ThreatModelingLabelAssignmentCommand.KIND, element, collisionMode: collisionMode ?? 'askUser' }; @@ -33,7 +33,7 @@ export namespace AddThreatModelingLabelToNodeAction { } @injectable() -export class ThreatModelingAssignmentCommand implements Command { +export class ThreatModelingLabelAssignmentCommand implements Command { public static readonly KIND = "threatModeling-addLabelToNode"; constructor( @@ -70,19 +70,16 @@ export class ThreatModelingAssignmentCommand implements Command { if (collisions.length == 0) { this.handleSimpleCase(labelProcessState) - return context.root; - } - - if (this.action.collisionMode === "askUser") { + } else if (this.action.collisionMode === "askUser") { this.handleAskUser( { labelType, labelTypeValue }, collisions, context ) - return context.root + } else { + this.handleOverwrite(labelProcessState, collisions) } - this.handleOverwrite(labelProcessState, collisions) return context.root; } @@ -91,8 +88,6 @@ export class ThreatModelingAssignmentCommand implements Command { } undo(context: CommandExecutionContext): CommandReturn { - if (this.action.collisionMode === "askUser") return context.root; - return context.root; } diff --git a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts similarity index 91% rename from frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts rename to frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts index 0bf62161..7423f208 100644 --- a/frontend/webEditor/src/labelingProcess/outputPortAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts @@ -30,7 +30,7 @@ export namespace AddLabelToOutputPortAction { collisionMode?: 'overwrite' | 'askUser' ): ThreatModelingLabelAssignmentToOutputPortAction { return { - kind: OutputPortAssignmentCommand.KIND, + kind: ThreatModelingLabelAssignmentToOutputPortCommand.KIND, element, collisionMode: collisionMode ?? 'askUser' }; @@ -38,7 +38,7 @@ export namespace AddLabelToOutputPortAction { } @injectable() -export class OutputPortAssignmentCommand implements Command { +export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command { public static readonly KIND = "addLabelToOutputPort"; private previousBehavior?: string @@ -59,15 +59,19 @@ export class OutputPortAssignmentCommand implements Command { if (!labelType || !labelTypeValue) return context.root; this.previousBehavior = this.action.element.getBehavior() + .trim() + .split("\n") + .filter(line => line !== "") + .join("\n") if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { this.handleNonThreatModelingCase(labelType, labelTypeValue); return context.root; } - let lines = this.previousBehavior + const lines = this.previousBehavior .split("\n") .map(line => line.trim()); - const collisions = findAllCollisions(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) + const collisions = findCollisions(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) console.error(this.previousBehavior) console.error(`${labelType.name}.${labelTypeValue.text}`) @@ -76,27 +80,28 @@ export class OutputPortAssignmentCommand implements Command { if (collisions.length == 0) { this.handleSimpleCase(lines, { labelType, labelTypeValue }) - return context.root; - } - - if (this.action.collisionMode === "askUser") { + } else if (this.action.collisionMode === "askUser") { this.handleAskUser({ labelType, labelTypeValue }, collisions, context) - return context.root + } else { + this.handleOverwrite(lines, { labelType, labelTypeValue }, collisions) } - this.handleOverwrite(lines, { labelType, labelTypeValue }, collisions) return context.root } redo(context: CommandExecutionContext): CommandReturn { - if (!this.newBehavior) return context.root; + if (!this.newBehavior + || this.action.collisionMode === "askUser" + ) return context.root; this.action.element.setBehavior(this.newBehavior); return context.root; } undo(context: CommandExecutionContext): CommandReturn { - if (!this.previousBehavior) return context.root; + if (!this.previousBehavior + || this.action.collisionMode === "askUser" + ) return context.root; this.action.element.setBehavior(this.previousBehavior); return context.root; @@ -149,7 +154,7 @@ export class OutputPortAssignmentCommand implements Command { } } -function findAllCollisions( +function findCollisions( portBehavior: string[], candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, labelTypeRegistry: LabelTypeRegistry From 57033cefce7ba0d2a06cf16f6292ae60ec72671e Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Sun, 25 Jan 2026 15:32:43 +0100 Subject: [PATCH 17/23] Color elements when collisions are detected during labeling process --- .../labelingProcess/labelingProcessCommand.ts | 74 +++++++++++++++---- .../threatModelingLabelAssignmentCommand.ts | 2 +- ...elingLabelAssignmentToOutputPortCommand.ts | 2 +- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts index 760c21aa..cdea0bce 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessCommand.ts @@ -9,10 +9,16 @@ import { Action } from "sprotty-protocol"; import { LabelingProcessState, LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; import { LabelAssignment } from "../labels/LabelType.ts"; -import { isThreatModelingLabelType } from "../labels/ThreatModelingLabelType.ts"; +import { + isThreatModelingLabelType, + isThreatModelingLabelTypeValue, + ThreatModelingLabelType, ThreatModelingLabelTypeValue, +} from "../labels/ThreatModelingLabelType.ts"; import { getAllElements } from "../labels/assignmentCommand.ts"; import { DfdNodeImpl } from "../diagram/nodes/common.ts"; import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort.tsx"; +import { findCollisions as findNodeCollisions } from "./threatModelingLabelAssignmentCommand.ts"; +import { findCollisions as findOutputPortCollisions } from "./threatModelingLabelAssignmentToOutputPortCommand.ts"; export interface LabelingProcessAction extends Action { state: LabelingProcessState @@ -117,30 +123,66 @@ export class LabelingProcessCommand implements Command { } highlightShapes(context: CommandExecutionContext) { - let nodeColor = DfdNodeImpl.NODE_COLOR - let outputPortColor = DfdOutputPortImpl.PORT_COLOR - - if (this.action.state.state === "inProgress") { - const { labelType } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) - if (!labelType) return; - - if (!isThreatModelingLabelType(labelType)) { - nodeColor = LabelingProcessUi.ASSIGNABLE_COLOR - outputPortColor = LabelingProcessUi.ASSIGNABLE_COLOR - } else if (labelType.intendedFor === "Vertex") { - nodeColor = LabelingProcessUi.ASSIGNABLE_COLOR + if (this.action.state.state !== "inProgress") return; + + const { labelType, labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(this.action.state.activeLabel) + if (!labelType || !labelTypeValue) return; + + + const applyColorToNode = (node: DfdNodeImpl) => { + if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { + node.setColor(LabelingProcessUi.ASSIGNABLE_COLOR) + return; + } + + if (labelType.intendedFor !== "Vertex") { + node.setColor(DfdNodeImpl.NODE_COLOR) + return; + } + + const assignedLabels = node.labels + .map(label => this.labelTypeRegistry.resolveLabelAssignment(label)) + .filter((label) : label is Required<{ labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }> => + label.labelType !== undefined + && label.labelTypeValue !== undefined + ) + .filter(label => + isThreatModelingLabelType(label.labelType) + && isThreatModelingLabelTypeValue(label.labelTypeValue) + ) + if (findNodeCollisions({ labelType, labelTypeValue }, assignedLabels).length === 0) { + node.setColor(LabelingProcessUi.ASSIGNABLE_COLOR) + } else { + node.setColor(LabelingProcessUi.COLLISION_COLOR) + } + } + + const applyColorToOutputPort = (port: DfdOutputPortImpl) => { + if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { + port.setColor(LabelingProcessUi.ASSIGNABLE_COLOR) + return; + } + + if (labelType.intendedFor !== "Flow") { + port.setColor(DfdOutputPortImpl.PORT_COLOR) + return; + } + + const behavior = port.getBehavior().split("\n") + if (findOutputPortCollisions(behavior, { labelType, labelTypeValue }, this.labelTypeRegistry).length === 0) { + port.setColor(LabelingProcessUi.ASSIGNABLE_COLOR) } else { - outputPortColor = LabelingProcessUi.ASSIGNABLE_COLOR + port.setColor(LabelingProcessUi.COLLISION_COLOR) } } getAllElements(context.root.children) .filter((element) => element instanceof DfdNodeImpl) - .forEach(node => node.setColor(nodeColor)) + .forEach(applyColorToNode) getAllElements(context.root.children) .filter((element) => element instanceof DfdOutputPortImpl) - .forEach(port => port.setColor(outputPortColor)) + .forEach(applyColorToOutputPort) } } \ No newline at end of file diff --git a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts index e960cce2..e671e137 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentCommand.ts @@ -137,7 +137,7 @@ export class ThreatModelingLabelAssignmentCommand implements Command { } } -function findCollisions( +export function findCollisions( candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, assigned: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] ): { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] { diff --git a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts index 7423f208..f349bf82 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts @@ -154,7 +154,7 @@ export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command } } -function findCollisions( +export function findCollisions( portBehavior: string[], candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, labelTypeRegistry: LabelTypeRegistry From 5b5e4f215fe4712f387cdc38c4762dba6d1ac825 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Sun, 25 Jan 2026 16:26:35 +0100 Subject: [PATCH 18/23] Remove duplicates from collision computation on output ports --- ...elingLabelAssignmentToOutputPortCommand.ts | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts index f349bf82..fef9a99c 100644 --- a/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts +++ b/frontend/webEditor/src/labelingProcess/threatModelingLabelAssignmentToOutputPortCommand.ts @@ -16,7 +16,6 @@ import { import { LabelingProcessUi } from "./labelingProcessUi.ts"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry.ts"; import { ExcludesDialog } from "./excludesDialog.ts"; -import { LabelType, LabelTypeValue } from "../labels/LabelType.ts"; interface ThreatModelingLabelAssignmentToOutputPortAction extends Action { @@ -58,13 +57,12 @@ export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command const { labelType, labelTypeValue } = this.labelTypeRegistry.resolveLabelAssignment(labelProcessState.activeLabel) if (!labelType || !labelTypeValue) return context.root; - this.previousBehavior = this.action.element.getBehavior() - .trim() - .split("\n") - .filter(line => line !== "") - .join("\n") + this.previousBehavior = this.action.element.getBehavior().trim() if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) { - this.handleNonThreatModelingCase(labelType, labelTypeValue); + this.applyNewBehavior([ + `${this.previousBehavior}`, + `set ${labelType.name}.${labelTypeValue.text}` + ]) return context.root; } @@ -73,11 +71,6 @@ export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command .map(line => line.trim()); const collisions = findCollisions(lines, { labelType, labelTypeValue }, this.labelTypeRegistry) - console.error(this.previousBehavior) - console.error(`${labelType.name}.${labelTypeValue.text}`) - console.error(labelTypeValue.excludes) - console.error(collisions) - if (collisions.length == 0) { this.handleSimpleCase(lines, { labelType, labelTypeValue }) } else if (this.action.collisionMode === "askUser") { @@ -107,23 +100,12 @@ export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command return context.root; } - private handleNonThreatModelingCase( - labelType: LabelType, - labelTypeValue: LabelTypeValue, - ) { - this.newBehavior = `${this.previousBehavior}\nset ${labelType.name}.${labelTypeValue.text}` - this.action.element.setBehavior(this.newBehavior); - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) - } - private handleSimpleCase( lines: string[], candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, ) { lines = addLabelAssignment(lines, candidate, this.labelTypeRegistry) - this.newBehavior = lines.join("\n") - this.action.element.setBehavior(this.newBehavior); - this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) + this.applyNewBehavior(lines) } private handleAskUser( @@ -148,7 +130,13 @@ export class ThreatModelingLabelAssignmentToOutputPortCommand implements Command lines = removeLabelAssignment(lines, { labelType: collision.labelType, labelTypeValue: collision.labelTypeValue }) } lines = addLabelAssignment(lines, candidate, this.labelTypeRegistry) - this.newBehavior = lines.join("\n") + this.applyNewBehavior(lines) + } + + private applyNewBehavior(lines: string[]) { + this.newBehavior = lines + .filter(line => line !== '') + .join("\n") this.action.element.setBehavior(this.newBehavior); this.action.element.setColor(LabelingProcessUi.ALREADY_ASSIGNED_COLOR) } @@ -159,17 +147,27 @@ export function findCollisions( candidate: { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }, labelTypeRegistry: LabelTypeRegistry ): { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }[] { - const collisions: Set<{ labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue }> = new Set(); + //Prevents duplicate entries. + //Native JS Sets cannot compare { labelType, labelTypeValue } correctly, therefore this complex solution is required. + const collisions = new Map< + string, + { labelType: ThreatModelingLabelType, labelTypeValue: ThreatModelingLabelTypeValue } + >(); + const computeCompositeKey = ( + labelType: ThreatModelingLabelType, + labelTypeValue: ThreatModelingLabelTypeValue + ) => `${labelType.id}.${labelTypeValue.id}` for (let i = 0; i < portBehavior.length; i++) { const line = portBehavior[i] //Search for a previous assignment that excludes the new assignment - if (line.match(`unset ${candidate.labelType.name}.${candidate.labelTypeValue.text}`)) { + if (line.trim() === `unset ${candidate.labelType.name}.${candidate.labelTypeValue.text}`) { //Searches for the previous `set` assignment - //Assumes that each `set` assignment is directly followed by their `unset` (`exclude`) assignments + //Assumes that each `set` assignment is directly followed by its `unset` assignments (based on it's + //'excludes' property) for (let j = i; j >= 0; j--) { - if (portBehavior[j].match(`set`)) { + if (portBehavior[j].startsWith(`set`)) { const parts = portBehavior[j].split(" ") const label = parts[1] const [ labelTypeName, labelTypeValueText ] = label.split(".") @@ -184,7 +182,10 @@ export function findCollisions( if (!isThreatModelingLabelType(labelType) || !isThreatModelingLabelTypeValue(labelTypeValue)) continue; - collisions.add({ labelType, labelTypeValue }) + collisions.set( + computeCompositeKey(labelType, labelTypeValue), + { labelType, labelTypeValue } + ) } } } @@ -199,14 +200,16 @@ export function findCollisions( || !isThreatModelingLabelTypeValue(labelTypeValue) ) continue; - if (line.match(`set ${labelType.name}.${labelTypeValue.text}`)) { - collisions.add({ labelType, labelTypeValue }); + if (line.trim() === `set ${labelType.name}.${labelTypeValue.text}`) { + collisions.set( + computeCompositeKey(labelType, labelTypeValue), + { labelType, labelTypeValue } + ); } } } - collisions.delete(candidate) - return Array.from(collisions); + return Array.from(collisions.values()); } /** From ba81d8a387f1f3179d5e8f912f1a4ecc5999ff9a Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Mon, 26 Jan 2026 13:09:32 +0100 Subject: [PATCH 19/23] Fix backend analysis (drawback: labeling process after analysis not possible) --- .../src/labels/ThreatModelingLabelType.ts | 35 +++++++++++++++++++ frontend/webEditor/src/serialize/analyze.ts | 12 +++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts index 484041ce..4b0bda2c 100644 --- a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -15,4 +15,39 @@ export function isThreatModelingLabelType(labelType: LabelType): labelType is Th export function isThreatModelingLabelTypeValue(labelTypeValue: LabelTypeValue): labelTypeValue is ThreatModelingLabelTypeValue { return "excludes" in labelTypeValue +} + +/** + * Transforms a `ThreatModelingLabelType` object to an object than can the backend can handle by removing additional + * attributes. + * @param labelType The `ThreatModelingLabelType` to transform + * @param recursive Whether the values of the `LabelType` should also be transformed into `LabelTypeValue` objects that + * can be sent to the backend + */ +export function threatModelingLabelTypeToBackendPayload( + labelType: ThreatModelingLabelType, + recursive: boolean +): LabelType { + const { intendedFor, values, ...defaultAttributes } = labelType + + let transformedValues = values + if (recursive) { + transformedValues = values.map(value => + isThreatModelingLabelTypeValue(value) + ? threatModelingLabelTypeValueToBackendPayload(value) + : value + ) + } + + return { ...defaultAttributes, values: transformedValues } +} + +/** + * Transforms a `ThreatModelingLabelTypeValue` object to an object than can the backend can handle by removing additional + * attributes. + * @param labelTypeValue The `ThreatModelingLabelTypeValue` to transform + */ +function threatModelingLabelTypeValueToBackendPayload(labelTypeValue: ThreatModelingLabelTypeValue): LabelTypeValue { + const { excludes, additionalInformation, ...defaultAttributes} = labelTypeValue; + return { ...defaultAttributes } } \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts index 6b949abe..58347650 100644 --- a/frontend/webEditor/src/serialize/analyze.ts +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -10,6 +10,10 @@ import { EditorModeController } from "../settings/editorMode"; import { Action } from "sprotty-protocol"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; +import { + isThreatModelingLabelType, + threatModelingLabelTypeToBackendPayload, +} from "../labels/ThreatModelingLabelType.ts"; export namespace AnalyzeAction { export const KIND = "analyze"; @@ -46,11 +50,15 @@ export class AnalyzeCommand extends LoadJsonCommand { protected async getFile(context: CommandExecutionContext): Promise | undefined> { const savedDiagram = { model: context.modelFactory.createSchema(context.root), - labelTypes: this.labelTypeRegistry.getLabelTypes(), + labelTypes: this.labelTypeRegistry.getLabelTypes().map(label => + isThreatModelingLabelType(label) ? + threatModelingLabelTypeToBackendPayload(label, true) + : label + ), constraints: this.constraintRegistry.getConstraintList(), mode: this.editorModeController.get(), version: CURRENT_VERSION, }; return await this.dfdWebSocket.requestDiagram("Json:" + JSON.stringify(savedDiagram)); } -} +} \ No newline at end of file From 8610fbfa93d8d71627bb82d058912caf47c2796b Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Mon, 26 Jan 2026 13:23:27 +0100 Subject: [PATCH 20/23] Integrate LINDDUN threat modeling file into command palette --- .../commandPalette/commandPaletteProvider.ts | 12 +- frontend/webEditor/src/serialize/di.config.ts | 6 +- frontend/webEditor/src/serialize/linddun.json | 399 ++++++++++++++++++ .../src/serialize/loadThreatModelingFile.ts | 15 +- .../loadThreatModelingLinddunFile.ts | 19 + .../serialize/loadThreatModelingUserFile.ts | 14 + 6 files changed, 448 insertions(+), 17 deletions(-) create mode 100644 frontend/webEditor/src/serialize/linddun.json create mode 100644 frontend/webEditor/src/serialize/loadThreatModelingLinddunFile.ts create mode 100644 frontend/webEditor/src/serialize/loadThreatModelingUserFile.ts diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index a46ea535..3ed3fe5f 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -10,8 +10,9 @@ import { LayoutMethod } from "../layout/layoutMethod"; import { LayoutModelAction } from "../layout/command"; import { SaveJsonFileAction } from "../serialize/saveJsonFile"; import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; -import { LoadThreatModelingFileAction } from "../serialize/loadThreatModelingFile.ts"; import { SaveThreatsTableAction } from "../serialize/saveThreatsTable.ts"; +import { LoadThreatModelingUserFileAction } from "../serialize/loadThreatModelingUserFile.ts"; +import { LoadThreatModelingLinddunFileAction } from "../serialize/loadThreatModelingLinddunFile.ts"; /** * Provides possible actions for the command palette. @@ -35,9 +36,14 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct ), new LabeledAction( "Load Threat Modeling File (JSON)", - [LoadThreatModelingFileAction.create(), commitAction], + [LoadThreatModelingUserFileAction.create(), commitAction], "fa-triangle-exclamation" - ) + ), + new LabeledAction( + "Load LINDDUN Threat Modeling File", + [LoadThreatModelingLinddunFileAction.create(), commitAction], + "fa-triangle-exclamation" + ), ], "go-to-file", ), diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index 6c0cac7f..ec48e709 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -8,8 +8,9 @@ import { DfdModelFactory } from "./ModelFactory"; import { SaveJsonFileCommand } from "./saveJsonFile"; import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; import { AnalyzeCommand } from "./analyze"; -import { LoadThreatModelingFileCommand } from "./loadThreatModelingFile.ts"; import { SaveThreatsTableCommand } from "./saveThreatsTable.ts"; +import { LoadThreatModelingUserFileCommand } from "./loadThreatModelingUserFile.ts"; +import { LoadThreatModelingLinddunFileCommand } from "./loadThreatModelingLinddunFile.ts"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -17,7 +18,8 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadJsonFileCommand); configureCommand(context, LoadDfdAndDdFileCommand); configureCommand(context, LoadPalladioFileCommand); - configureCommand(context, LoadThreatModelingFileCommand); + configureCommand(context, LoadThreatModelingUserFileCommand); + configureCommand(context, LoadThreatModelingLinddunFileCommand); configureCommand(context, SaveJsonFileCommand); configureCommand(context, SaveDfdAndDdFileCommand); configureCommand(context, SaveThreatsTableCommand) diff --git a/frontend/webEditor/src/serialize/linddun.json b/frontend/webEditor/src/serialize/linddun.json new file mode 100644 index 00000000..9b2a0895 --- /dev/null +++ b/frontend/webEditor/src/serialize/linddun.json @@ -0,0 +1,399 @@ +{ + "threatKnowledgeName": "LINDDUN", + "threatKnowledgeVersion": "0.3", + "labels": [ + { + "id": "T-RhN06vgx", + "name": "DataForm", + "intendedFor": "Flow", + "values": [ + { + "id": "L-xUrF9mGH", + "text": "Hashed", + "excludes": [ + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-ISc2fMXE" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-XT9WKSWb", + "text": "Encrypted", + "excludes": [ + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-ISc2fMXE" + } + ], + "additionalInformation": "#todo There is a difference by who possesses the encryption key (e.g. symmetrical encryption maintains deniability, asymmetrical encryption does not)\n# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-bQVu96ih", + "text": "Aggregated", + "excludes": [ + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-ISc2fMXE" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-xUrF9mGH" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-cubKHVGd", + "text": "Noisy", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-ISc2fMXE", + "text": "Raw", + "excludes": [ + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-XT9WKSWb" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-ISc2fMXE" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-bQVu96ih" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-xUrF9mGH" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-PRhK8ZRm" + } + ], + "additionalInformation": "# Description\nBasic data, no encryption, no added anything\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-PRhK8ZRm", + "text": "Sanitized", + "excludes": [ + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-ISc2fMXE" + }, + { + "labelTypeId": "T-RhN06vgx", + "labelTypeValueId": "L-xUrF9mGH" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-hqzmK4lj", + "name": "VertexLocation", + "intendedFor": "Vertex", + "values": [ + { + "id": "L-Y9Hi8FwJ", + "text": "User", + "excludes": [ + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-BnqZZq2f" + }, + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-EU7CXdo5" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-BnqZZq2f", + "text": "Organization", + "excludes": [ + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-Y9Hi8FwJ" + }, + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-EU7CXdo5" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-EU7CXdo5", + "text": "External", + "excludes": [ + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-Y9Hi8FwJ" + }, + { + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-BnqZZq2f" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-hDctW1RZ", + "name": "DataSensitivity", + "intendedFor": "Flow", + "values": [ + { + "id": "L-oHu78JWj", + "text": "Public", + "excludes": [], + "additionalInformation": "# Relations\n- ...\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-h1fGHkX7", + "text": "PersonalData", + "excludes": [], + "additionalInformation": "# Relations\n- excludes [[Public]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-4lebOxF1", + "name": "DataIdentifiability", + "intendedFor": "Flow", + "values": [ + { + "id": "L-Vdnn8zIC", + "text": "QuasiIdentifiable", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-4hzWLvca", + "text": "Pseudonym", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-AziCqUMB", + "text": "UniquelyIdentifiable", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-ki3tgyjB", + "text": "Authenticated", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-Qu9VVPG4", + "name": "DataInterveniability", + "intendedFor": "Flow", + "values": [ + { + "id": "L-u7wWmGo9", + "text": "Accessible", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-s2Op4rNS", + "text": "Deletable", + "excludes": [], + "additionalInformation": "# Relations\n- includes [[Rectification Possible]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-7Ix5ydqW", + "text": "None", + "excludes": [ + { + "labelTypeId": "T-Qu9VVPG4", + "labelTypeValueId": "L-0YOIZs6L" + }, + { + "labelTypeId": "T-Qu9VVPG4", + "labelTypeValueId": "L-s2Op4rNS" + }, + { + "labelTypeId": "T-Qu9VVPG4", + "labelTypeValueId": "L-AJf2Su62" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-0YOIZs6L", + "text": "ControlViaPreferences", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-IYBKXU20", + "text": "OptIn", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-uIQJl0lr", + "text": "OptOut", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-AJf2Su62", + "text": "RectificationPossible", + "excludes": [], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-3TGOog3X", + "name": "DataPrecision", + "intendedFor": "Flow", + "values": [ + { + "id": "L-JKy1Otrb", + "text": "ExcessiveDataTypes", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "# Description\n\n# Examples\n- Exact Location if Rough Location is sufficient\n# Elicitation Questions\n\n# Output Pin Behavior\n```\nforward *\nset ...\n```" + }, + { + "id": "L-uoXCDQCw", + "text": "ExcessiveFrequency", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-76VjjaH1", + "text": "ExcessiveVolume", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-I78hTu4G", + "text": "StrictlyNecessary", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-76VjjaH1" + }, + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-uoXCDQCw" + }, + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-JKy1Otrb" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + }, + { + "id": "T-IqL9df0D", + "name": "DataIntegrity", + "intendedFor": "Flow", + "values": [ + { + "id": "L-AcyIOqAF", + "text": "Signed", + "excludes": [], + "additionalInformation": "#todo does Signed make sense like this?\n#todo There is a difference between symmetric and asymmetric Signatures\n\n# Description\nSigning data removes deniability\n\n# Examples\n\n# Elicitation Questions\n# Output Pin Behavior\n```\nforward *\nset ...\n```" + } + ] + }, + { + "id": "T-1ez1Gjk9", + "name": "Observability", + "intendedFor": null, + "values": [ + { + "id": "L-irbu2qs8", + "text": "FullyVisible", + "excludes": [ + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-M5uQ5X3f" + }, + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-nvKZS1U9" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-M5uQ5X3f", + "text": "MetadataOnly", + "excludes": [ + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-irbu2qs8" + }, + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-nvKZS1U9" + } + ], + "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + }, + { + "id": "L-nvKZS1U9", + "text": "NonObservable", + "excludes": [], + "additionalInformation": "# Relations\n- excludes [[Fully Visible]]\n- excludes [[Metadata Only]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + } + ] + } + ], + "constraints": [ + { + "name": "IdentifyingthroughUniqueIdentifier", + "constraint": "data DataIdentifiability.UniquelyIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + }, + { + "name": "DatawithexcessiveVolumeisprocessed", + "constraint": "data DataPrecision.ExcessiveVolume neverFlows vertex VertexLocation.Organization,VertexLocation.External" + }, + { + "name": "DatawithexcessiveFrequencyisprocessed", + "constraint": "data DataPrecision.ExcessiveFrequency neverFlows vertex VertexLocation.Organization,VertexLocation.External" + }, + { + "name": "NonRepudiationthroughSignature", + "constraint": "data DataIntegrity.Signed neverFlows vertex VertexLocation.Organization" + }, + { + "name": "IdentifyingthroughQuasiIdentifier", + "constraint": "data DataIdentifiability.QuasiIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + } + ] +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts index b02236ea..854bb8a2 100644 --- a/frontend/webEditor/src/serialize/loadThreatModelingFile.ts +++ b/frontend/webEditor/src/serialize/loadThreatModelingFile.ts @@ -27,23 +27,14 @@ import { ResetLabelingProcessAction } from "../labelingProcess/labelingProcessCo // Replaces the type of the `values` of a `LabelType` with a subclass of `LabelTypeValue` type OverwriteLabelTypeValueType = Omit & { values: S[] } -type ThreatModelingFileFormat = { +export type ThreatModelingFileFormat = { threatKnowledgeName: string, threatKnowledgeVersion: string, labels: OverwriteLabelTypeValueType[], constraints: Constraint[] } -export namespace LoadThreatModelingFileAction { - export const KIND = "loadThreatModelingFile"; - - export function create(): Action { - return { kind: KIND }; - } -} - -export class LoadThreatModelingFileCommand extends Command { - static readonly KIND = LoadThreatModelingFileAction.KIND; +export abstract class LoadThreatModelingFileCommand extends Command { private fileContent: ThreatModelingFileFormat | undefined; @@ -66,7 +57,7 @@ export class LoadThreatModelingFileCommand extends Command { super(); } - private async getFileContent(): Promise { + protected async getFileContent(): Promise { const file = await chooseFile(["application/json"]); if (!file) return undefined diff --git a/frontend/webEditor/src/serialize/loadThreatModelingLinddunFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingLinddunFile.ts new file mode 100644 index 00000000..52529b42 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadThreatModelingLinddunFile.ts @@ -0,0 +1,19 @@ +import { Action } from "sprotty-protocol"; +import { LoadThreatModelingFileCommand, ThreatModelingFileFormat } from "./loadThreatModelingFile.ts"; +import LINDDUN from './linddun.json' + +export namespace LoadThreatModelingLinddunFileAction { + export const KIND = "LoadThreatModelingLINDDUNFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadThreatModelingLinddunFileCommand extends LoadThreatModelingFileCommand { + static readonly KIND = LoadThreatModelingLinddunFileAction.KIND; + + override async getFileContent(): Promise { + return LINDDUN as ThreatModelingFileFormat; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadThreatModelingUserFile.ts b/frontend/webEditor/src/serialize/loadThreatModelingUserFile.ts new file mode 100644 index 00000000..bb8f2387 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadThreatModelingUserFile.ts @@ -0,0 +1,14 @@ +import { LoadThreatModelingFileCommand } from "./loadThreatModelingFile.ts"; +import { Action } from "sprotty-protocol"; + +export namespace LoadThreatModelingUserFileAction { + export const KIND = "loadThreatModelingUserFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadThreatModelingUserFileCommand extends LoadThreatModelingFileCommand { + static readonly KIND = LoadThreatModelingUserFileAction.KIND; +} \ No newline at end of file From 0e8e0579a91297389c85e69c66ee178cf638422d Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Mon, 26 Jan 2026 20:37:49 +0100 Subject: [PATCH 21/23] Import newer linddun file version --- frontend/webEditor/src/serialize/linddun.json | 137 ++++++++++++------ 1 file changed, 94 insertions(+), 43 deletions(-) diff --git a/frontend/webEditor/src/serialize/linddun.json b/frontend/webEditor/src/serialize/linddun.json index 9b2a0895..abccb2f4 100644 --- a/frontend/webEditor/src/serialize/linddun.json +++ b/frontend/webEditor/src/serialize/linddun.json @@ -1,6 +1,7 @@ { "threatKnowledgeName": "LINDDUN", - "threatKnowledgeVersion": "0.3", + "threatKnowledgeVersion": "0.4", + "xDecafVersion": 1, "labels": [ { "id": "T-RhN06vgx", @@ -16,7 +17,7 @@ "labelTypeValueId": "L-ISc2fMXE" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData transformed using a one-way cryptographic function, typically for comparison or verification.\n\n**Examples:**\n- Hashed passwords\n- Hashed email addresses for matching\n- Hash-based identifiers\n\n**Elicitation questions:**\n- Is the hashing salted or unsalted?\n- Can the original value be reconstructed (directly or via lookup)?\n- Is hashing used consistently across systems?\n\n[[ChatGPT]]" }, { "id": "L-XT9WKSWb", @@ -27,7 +28,7 @@ "labelTypeValueId": "L-ISc2fMXE" } ], - "additionalInformation": "#todo There is a difference by who possesses the encryption key (e.g. symmetrical encryption maintains deniability, asymmetrical encryption does not)\n# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "#todo There is a difference by who possesses the encryption key (e.g. symmetrical encryption maintains deniability, asymmetrical encryption does not)\n\n**Description:**\nData transformed using cryptography so it is unreadable without a decryption key.\n\n**Examples:**\n- Encrypted customer databases at rest\n- TLS-encrypted data in transit\n- Encrypted backups\n\n**Elicitation questions:**\n- Is the data encrypted at rest, in transit, or both?\n- Who controls the encryption keys?\n- Can the system process the data while it remains encrypted?\n\n[[ChatGPT]]" }, { "id": "L-bQVu96ih", @@ -42,13 +43,13 @@ "labelTypeValueId": "L-xUrF9mGH" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData combined across multiple records or individuals so that individual-level details are not directly visible.\n\n**Examples:**\n- Average daily step count across all users\n- Monthly sales totals by region\n- Percentage of users who enabled a feature\n\n**Elicitation questions:**\n- Can individual records be reconstructed from this data?\n- What is the minimum group size represented?\n- Is aggregation done before storage or only at reporting time?\n\n[[ChatGPT]]" }, { "id": "L-cubKHVGd", "text": "Noisy", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData deliberately altered with random noise to reduce accuracy and protect individuals (often used in privacy-preserving techniques).\n\n**Examples:**\n- Differentially private statistics\n- Location data with jitter added\n- Slightly perturbed survey results\n\n**Elicitation questions:**\n- What privacy mechanism introduces the noise?\n- Is the noise reversible or cumulative?\n- What accuracy trade-offs are acceptable?\n\n[[ChatGPT]]" }, { "id": "L-ISc2fMXE", @@ -75,7 +76,7 @@ "labelTypeValueId": "L-PRhK8ZRm" } ], - "additionalInformation": "# Description\nBasic data, no encryption, no added anything\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData in its original, unprocessed form as collected from the source.\n\n**Examples:**\n- Raw sensor readings\n- Original application logs\n- Unfiltered user input\n\n**Elicitation questions:**\n- Is any transformation applied before storage?\n- How long is raw data retained?\n- Who has access to the raw form?\n\n[[ChatGPT]]" }, { "id": "L-PRhK8ZRm", @@ -90,7 +91,7 @@ "labelTypeValueId": "L-xUrF9mGH" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData that has been cleaned or modified to remove or reduce sensitive elements.\n\n**Examples:**\n- Logs with IP addresses removed\n- Datasets with names redacted\n- Masked account numbers\n\n**Elicitation questions:**\n- What fields were removed or masked?\n- Is sanitization irreversible?\n- Is sanitization applied consistently?\n\n[[ChatGPT]]" } ] }, @@ -112,7 +113,7 @@ "labelTypeValueId": "L-EU7CXdo5" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData resides on or is processed directly on the user\u2019s device.\n\n**Examples:**\n- Data stored locally in a mobile app\n- On-device analytics or ML inference\n- Browser-based form data before submission\n\n**Elicitation questions:**\n- Does data leave the user\u2019s device?\n- Is data encrypted at rest on the device?\n- What happens if the device is lost or compromised?\n\n[[ChatGPT]]" }, { "id": "L-BnqZZq2f", @@ -127,7 +128,7 @@ "labelTypeValueId": "L-EU7CXdo5" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData is stored or processed within systems controlled by the organization.\n\n**Examples:**\n- Internal databases\n- Company-managed cloud infrastructure\n- Internal analytics platforms\n\n**Elicitation questions:**\n- Which internal systems store or process the data?\n- Who within the organization can access it?\n- In which countries or regions is it hosted?\n\n[[ChatGPT]]" }, { "id": "L-EU7CXdo5", @@ -142,7 +143,7 @@ "labelTypeValueId": "L-BnqZZq2f" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData is transferred to or processed by third parties outside the organization\u2019s direct control.\n\n**Examples:**\n- Cloud service providers\n- Payment processors\n- Analytics or marketing vendors\n\n**Elicitation questions:**\n- Which third parties receive this data?\n- What contractual or technical safeguards exist?\n- Is data shared in raw, pseudonymized, or aggregated form?\n- Is cross-border transfer involved?\n\n[[ChatGPT]]" } ] }, @@ -154,14 +155,24 @@ { "id": "L-oHu78JWj", "text": "Public", - "excludes": [], - "additionalInformation": "# Relations\n- ...\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + "excludes": [ + { + "labelTypeId": "T-hDctW1RZ", + "labelTypeValueId": "L-h1fGHkX7" + } + ], + "additionalInformation": "**Description:** \nData that is intentionally made available to the general public.\n\n**Examples:**\n- Public social media posts\n- Published reports\n- Open government datasets\n\n**Elicitation questions:**\n- Was this data deliberately made public?\n- Are there reuse or licensing limits?\n- Could context make it sensitive anyway?\n\n[[ChatGPT]]" }, { "id": "L-h1fGHkX7", "text": "PersonalData", - "excludes": [], - "additionalInformation": "# Relations\n- excludes [[Public]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + "excludes": [ + { + "labelTypeId": "T-hDctW1RZ", + "labelTypeValueId": "L-oHu78JWj" + } + ], + "additionalInformation": "**Description:**\nData relating to an identified or identifiable individual.\n\n**Examples:**\n- Names, emails, IP addresses\n- Behavioral profiles\n- Location histories\n\n**Elicitation questions:**\n- Can this data identify a person directly or indirectly?\n- Is it regulated under privacy laws?\n- What harm could result from misuse?\n\n[[ChatGPT]]" } ] }, @@ -174,25 +185,25 @@ "id": "L-Vdnn8zIC", "text": "QuasiIdentifiable", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData that is not directly identifying alone but can identify individuals when combined.\n\n**Examples:**\n- ZIP code + birth date + gender\n- Job title + department\n- Partial location histories\n\n**Elicitation questions:**\n- What other datasets could be combined with this?\n- Has re-identification risk been assessed?\n- Is this data shared externally?\n\n[[ChatGPT]]" }, { "id": "L-4hzWLvca", "text": "Pseudonym", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData linked to an identifier that does not directly reveal identity but can be re-linked.\n\n**Examples:**\n- User IDs instead of names\n- Device IDs\n- Tokenized identifiers\n\n**Elicitation questions:**\n- Where is the mapping between pseudonym and real identity stored?\n- Who can access the re-identification key?\n- Is the pseudonym stable over time?\n\n[[ChatGPT]]" }, { "id": "L-AziCqUMB", "text": "UniquelyIdentifiable", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData that uniquely and directly identifies a person.\n\n**Examples:**\n- Full name + address\n- National ID number\n- Biometric identifiers\n\n**Elicitation questions:**\n- Does this data uniquely identify an individual on its own?\n- Is it legally protected or regulated?\n- Is collection strictly necessary?\n\n[[ChatGPT]]" }, { "id": "L-ki3tgyjB", "text": "Authenticated", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData directly tied to a verified, logged-in identity.\n\n**Examples:**\n- User account profiles\n- Authenticated API usage logs\n- Payment transaction histories\n\n**Elicitation questions:**\n- Does this data require user authentication to generate or access?\n- Is identity verified or merely asserted?\n- Can activity be traced to a specific account?\n\n[[ChatGPT]]" } ] }, @@ -205,13 +216,13 @@ "id": "L-u7wWmGo9", "text": "Accessible", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nIndividuals can access their data.\n\n**Examples:**\n- User dashboards\n- Data access request portals\n- Download-your-data features\n\n**Elicitation questions:**\n- How can users access their data?\n- Are there authentication or rate limits?\n- Is access complete or partial?\n\n[[ChatGPT]]" }, { "id": "L-s2Op4rNS", "text": "Deletable", "excludes": [], - "additionalInformation": "# Relations\n- includes [[Rectification Possible]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nIndividuals can request deletion of their data.\n\n**Examples:**\n- Account deletion workflows\n- \u201cRight to be forgotten\u201d processes\n\n**Elicitation questions:**\n- Is deletion permanent or soft-deleted?\n- Are backups also deleted?\n- What legal exceptions apply?\n\n[[ChatGPT]]" }, { "id": "L-7Ix5ydqW", @@ -242,19 +253,19 @@ "id": "L-IYBKXU20", "text": "OptIn", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData is collected or processed only after explicit consent.\n\n**Examples:**\n- Marketing email subscriptions\n- Location tracking enabled by default off\n\n**Elicitation questions:**\n- What constitutes valid consent?\n- Is consent granular?\n- How is consent recorded?\n\n[[ChatGPT]]" }, { "id": "L-uIQJl0lr", "text": "OptOut", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData is collected by default but individuals can refuse or stop processing.\n\n**Examples:**\n- Analytics cookies with opt-out\n- Default personalization settings\n\n**Elicitation questions:**\n- How easy is it to opt out?\n- Does opt-out fully stop processing?\n- Are users clearly informed?\n\n[[ChatGPT]]" }, { "id": "L-AJf2Su62", "text": "RectificationPossible", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nIndividuals can correct inaccurate or outdated data.\n\n**Examples:**\n- Profile edit features\n- Address update forms\n\n**Elicitation questions:**\n- Which fields can users edit themselves?\n- How are corrections propagated?\n- Are changes logged?\n\n[[ChatGPT]]" } ] }, @@ -272,7 +283,7 @@ "labelTypeValueId": "L-I78hTu4G" } ], - "additionalInformation": "# Description\n\n# Examples\n- Exact Location if Rough Location is sufficient\n# Elicitation Questions\n\n# Output Pin Behavior\n```\nforward *\nset ...\n```" + "additionalInformation": "**Description:** \nUnnecessary categories or attributes are collected.\n\n**Examples:**\n- Collecting gender when not needed\n- Storing device fingerprints unnecessarily\n\n**Elicitation questions:**\n- Why is each data type needed?\n- What happens if this field is removed?\n- Who requested its inclusion?\n\n[[ChatGPT]]" }, { "id": "L-uoXCDQCw", @@ -283,7 +294,7 @@ "labelTypeValueId": "L-I78hTu4G" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nData is collected more often than required.\n\n**Examples:**\n- Continuous location tracking\n- Logging every keystroke\n\n**Elicitation questions:**\n- How often is this data actually used?\n- Can collection be event-based instead?\n- Is sampling sufficient?\n\n[[ChatGPT]]" }, { "id": "L-76VjjaH1", @@ -294,7 +305,7 @@ "labelTypeValueId": "L-I78hTu4G" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description**\nThe total quantity of data collected and stored, considering scale across users, time, and systems. Volume affects storage risk, breach impact, system performance, and feasibility of privacy controls.\n\n**Examples**\n- Millions of daily log entries\n- High-resolution telemetry collected continuously\n- Small profile dataset per user but retained for millions of users\n- Large historical dataset kept for analytics\n\n**Elicitation questions**\n- How much data is collected per user, per event, or per time period?\n- What is the total volume currently stored?\n- How fast does the dataset grow?\n- Is all collected data actively used?\n- Could older data be summarized, sampled, or deleted?\n\n[[ChatGPT]]" }, { "id": "L-I78hTu4G", @@ -311,9 +322,24 @@ { "labelTypeId": "T-3TGOog3X", "labelTypeValueId": "L-JKy1Otrb" + }, + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-mRFUMGpc" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nOnly the minimum data required for the purpose is collected.\n\n**Examples:**\n- Age range instead of birth date\n- Country instead of exact address\n\n**Elicitation questions:**\n- What decision depends on each field?\n- Can this be collected at lower precision?\n- Has minimization been reviewed?\n\n[[ChatGPT]]" + }, + { + "id": "L-mRFUMGpc", + "text": "ExcessiveRetention", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "**Description**\nThe length of time data is stored before it is deleted, anonymized, or irreversibly aggregated. Retention directly affects privacy risk, breach impact, and regulatory compliance.\n\n**Examples**\n- Authentication logs retained for 30 days for security investigations\n- Customer account data retained for the life of the account plus 90 days\n- Transaction records retained for 7 years to meet legal obligations\n- Telemetry data aggregated after 14 days and raw data deleted\n\n**Elicitation questions**\n- What is the defined retention period for this data?\n- Does retention differ between active systems, archives, and backups?\n- What event triggers deletion (time-based, account closure, user request)?\n- Is data deleted, anonymized, or merely archived at the end of retention?\n- Are there legal or contractual reasons for extended retention?\n\n[[ChatGPT]]" } ] }, @@ -325,8 +351,24 @@ { "id": "L-AcyIOqAF", "text": "Signed", - "excludes": [], - "additionalInformation": "#todo does Signed make sense like this?\n#todo There is a difference between symmetric and asymmetric Signatures\n\n# Description\nSigning data removes deniability\n\n# Examples\n\n# Elicitation Questions\n# Output Pin Behavior\n```\nforward *\nset ...\n```" + "excludes": [ + { + "labelTypeId": "T-IqL9df0D", + "labelTypeValueId": "L-bDV34VU2" + } + ], + "additionalInformation": "#todo does Signed make sense like this?\n#todo There is a difference between symmetric and asymmetric Signatures\n\n**Description:** \nData includes a cryptographic signature to verify authenticity and integrity. This also removes deniability.\n\n**Examples:**\n- Digitally signed documents\n- Signed API payloads\n- Code signing certificates\n\n**Elicitation questions:**\n- How is the signature generated and verified?\n- What happens if verification fails?\n- Who controls the signing keys?\n\n[[ChatGPT]]" + }, + { + "id": "L-bDV34VU2", + "text": "Unsigned", + "excludes": [ + { + "labelTypeId": "T-IqL9df0D", + "labelTypeValueId": "L-AcyIOqAF" + } + ], + "additionalInformation": "**Description:** \nData has no cryptographic assurance of origin or integrity.\n\n**Examples:**\n- Plain text files\n- Unsigned logs\n- Unverified data imports\n\n**Elicitation questions:**\n- How do you detect tampering or corruption?\n- Is integrity assumed or enforced elsewhere?\n- Is this acceptable for the data\u2019s purpose?\n\n[[ChatGPT]]" } ] }, @@ -348,7 +390,7 @@ "labelTypeValueId": "L-nvKZS1U9" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nThe data content itself is visible to observers or systems.\n\n**Examples:**\n- Plaintext messages\n- Visible transaction details\n\n**Elicitation questions:**\n- Who can view the full content?\n- Is visibility role-based?\n- Is access logged?\n\n[[ChatGPT]]" }, { "id": "L-M5uQ5X3f", @@ -363,37 +405,46 @@ "labelTypeValueId": "L-nvKZS1U9" } ], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description:** \nOnly contextual information is observable, not the content.\n\n**Examples:**\n- Email headers without body\n- Encrypted message metadata\n- Call timestamps and duration\n\n**Elicitation questions:**\n- What metadata is retained?\n- Can metadata alone reveal sensitive patterns?\n- Is metadata minimized?\n\n[[ChatGPT]]" }, { "id": "L-nvKZS1U9", "text": "NonObservable", - "excludes": [], - "additionalInformation": "# Relations\n- excludes [[Fully Visible]]\n- excludes [[Metadata Only]]\n\n# Description\n\n# Examples\n\n# Elicitation Questions" + "excludes": [ + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-irbu2qs8" + }, + { + "labelTypeId": "T-1ez1Gjk9", + "labelTypeValueId": "L-M5uQ5X3f" + } + ], + "additionalInformation": "**Description:** \nNeither content nor metadata is accessible.\n\n**Examples:**\n- End-to-end encrypted messages with sealed metadata\n- Secure enclaves\n\n**Elicitation questions:**\n- Who (if anyone) can observe anything?\n- Is this by design or limitation?\n- How is misuse detected?\n\n[[ChatGPT]]" } ] } ], "constraints": [ { - "name": "IdentifyingthroughUniqueIdentifier", - "constraint": "data DataIdentifiability.UniquelyIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "name": "DataWithExcessiveFrequencyIsProcessed", + "constraint": "data DataPrecision.ExcessiveFrequency neverFlows vertex VertexLocation.Organization,VertexLocation.External" }, { - "name": "DatawithexcessiveVolumeisprocessed", + "name": "DataWithExcessiveVolumeIsProcessed", "constraint": "data DataPrecision.ExcessiveVolume neverFlows vertex VertexLocation.Organization,VertexLocation.External" }, { - "name": "DatawithexcessiveFrequencyisprocessed", - "constraint": "data DataPrecision.ExcessiveFrequency neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "name": "IdentifyingThroughQuasiIdentifier", + "constraint": "data DataIdentifiability.QuasiIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" }, { - "name": "NonRepudiationthroughSignature", - "constraint": "data DataIntegrity.Signed neverFlows vertex VertexLocation.Organization" + "name": "IdentifyingThroughUniqueIdentifier", + "constraint": "data DataIdentifiability.UniquelyIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" }, { - "name": "IdentifyingthroughQuasiIdentifier", - "constraint": "data DataIdentifiability.QuasiIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "name": "NonRepudiationThroughSignature", + "constraint": "data DataIntegrity.Signed neverFlows vertex VertexLocation.Organization" } ] -} \ No newline at end of file +} From f87cf6d5d77f944fe661abde571aed00d519fc14 Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Wed, 18 Feb 2026 17:01:13 +0100 Subject: [PATCH 22/23] Update linddun.json --- .../src/labelingProcess/labelingProcessUi.ts | 11 +- frontend/webEditor/src/serialize/linddun.json | 389 ++++++++++-------- 2 files changed, 224 insertions(+), 176 deletions(-) diff --git a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts index c72256bb..74550408 100644 --- a/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts +++ b/frontend/webEditor/src/labelingProcess/labelingProcessUi.ts @@ -14,6 +14,7 @@ import { ConstraintRegistry } from "../constraint/constraintRegistry.ts"; import { SaveThreatsTableAction } from "../serialize/saveThreatsTable.ts"; import { isThreatModelingLabelType, isThreatModelingLabelTypeValue } from "../labels/ThreatModelingLabelType.ts"; import { marked } from "marked"; +import { SaveJsonFileAction } from "../serialize/saveJsonFile.ts"; export type LabelingProcessState = { state: 'pending' } @@ -119,13 +120,13 @@ export class LabelingProcessUi extends AbstractUIExtension { const finalStepsButton = document.createElement('button') finalStepsButton.innerText = "Check constraints and download threats" finalStepsButton.classList.add("labeling-process-button") - finalStepsButton.addEventListener('click', () => { - this.actionDispatcher.dispatchAll([ + finalStepsButton.addEventListener('click', async () => { + await this.actionDispatcher.dispatch(SaveJsonFileAction.create()) + await this.actionDispatcher.dispatchAll([ AnalyzeAction.create(), SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), - ]).then(() => - this.actionDispatcher.dispatch(SaveThreatsTableAction.create()) - ) + ]) + await this.actionDispatcher.dispatch(SaveThreatsTableAction.create()) }) this.containerElement.replaceChildren(text, finalStepsButton) diff --git a/frontend/webEditor/src/serialize/linddun.json b/frontend/webEditor/src/serialize/linddun.json index abccb2f4..921fe0db 100644 --- a/frontend/webEditor/src/serialize/linddun.json +++ b/frontend/webEditor/src/serialize/linddun.json @@ -1,123 +1,73 @@ { "threatKnowledgeName": "LINDDUN", - "threatKnowledgeVersion": "0.4", + "threatKnowledgeVersion": "1", "xDecafVersion": 1, "labels": [ { - "id": "T-RhN06vgx", - "name": "DataForm", - "intendedFor": "Flow", + "id": "T-hqzmK4lj", + "name": "VertexLocation", + "intendedFor": "Vertex", "values": [ { - "id": "L-xUrF9mGH", - "text": "Hashed", - "excludes": [ - { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-ISc2fMXE" - } - ], - "additionalInformation": "**Description:** \nData transformed using a one-way cryptographic function, typically for comparison or verification.\n\n**Examples:**\n- Hashed passwords\n- Hashed email addresses for matching\n- Hash-based identifiers\n\n**Elicitation questions:**\n- Is the hashing salted or unsalted?\n- Can the original value be reconstructed (directly or via lookup)?\n- Is hashing used consistently across systems?\n\n[[ChatGPT]]" - }, - { - "id": "L-XT9WKSWb", - "text": "Encrypted", + "id": "L-Y9Hi8FwJ", + "text": "User", "excludes": [ { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-ISc2fMXE" - } - ], - "additionalInformation": "#todo There is a difference by who possesses the encryption key (e.g. symmetrical encryption maintains deniability, asymmetrical encryption does not)\n\n**Description:**\nData transformed using cryptography so it is unreadable without a decryption key.\n\n**Examples:**\n- Encrypted customer databases at rest\n- TLS-encrypted data in transit\n- Encrypted backups\n\n**Elicitation questions:**\n- Is the data encrypted at rest, in transit, or both?\n- Who controls the encryption keys?\n- Can the system process the data while it remains encrypted?\n\n[[ChatGPT]]" - }, - { - "id": "L-bQVu96ih", - "text": "Aggregated", - "excludes": [ + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-BnqZZq2f" + }, { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-ISc2fMXE" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-jdroojuo" }, { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-xUrF9mGH" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-oHu78JWj" } ], - "additionalInformation": "**Description:** \nData combined across multiple records or individuals so that individual-level details are not directly visible.\n\n**Examples:**\n- Average daily step count across all users\n- Monthly sales totals by region\n- Percentage of users who enabled a feature\n\n**Elicitation questions:**\n- Can individual records be reconstructed from this data?\n- What is the minimum group size represented?\n- Is aggregation done before storage or only at reporting time?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nData resides on or is processed directly on the user\u2019s device.\n\n**Examples:**\n- Data stored locally in a mobile app\n- On-device analytics or ML inference\n- Browser-based form data before submission\n\n[[ChatGPT]]" }, { - "id": "L-cubKHVGd", - "text": "Noisy", - "excludes": [], - "additionalInformation": "**Description:** \nData deliberately altered with random noise to reduce accuracy and protect individuals (often used in privacy-preserving techniques).\n\n**Examples:**\n- Differentially private statistics\n- Location data with jitter added\n- Slightly perturbed survey results\n\n**Elicitation questions:**\n- What privacy mechanism introduces the noise?\n- Is the noise reversible or cumulative?\n- What accuracy trade-offs are acceptable?\n\n[[ChatGPT]]" - }, - { - "id": "L-ISc2fMXE", - "text": "Raw", + "id": "L-BnqZZq2f", + "text": "Organization", "excludes": [ { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-XT9WKSWb" - }, - { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-ISc2fMXE" - }, - { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-bQVu96ih" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-Y9Hi8FwJ" }, { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-xUrF9mGH" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-jdroojuo" }, { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-PRhK8ZRm" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-oHu78JWj" } ], - "additionalInformation": "**Description:** \nData in its original, unprocessed form as collected from the source.\n\n**Examples:**\n- Raw sensor readings\n- Original application logs\n- Unfiltered user input\n\n**Elicitation questions:**\n- Is any transformation applied before storage?\n- How long is raw data retained?\n- Who has access to the raw form?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nData is stored or processed within systems controlled by the organization.\n\n**Examples:**\n- Internal databases\n- Company-managed cloud infrastructure\n- Internal analytics platforms\n\n**Elicitation questions:**\n- Which internal systems store or process the data?\n- Who within the organization can access it?\n\n[[ChatGPT]]" }, { - "id": "L-PRhK8ZRm", - "text": "Sanitized", + "id": "L-oHu78JWj", + "text": "Public", "excludes": [ { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-ISc2fMXE" + "labelTypeId": "T-hqzmK4lj", + "labelTypeValueId": "L-Y9Hi8FwJ" }, - { - "labelTypeId": "T-RhN06vgx", - "labelTypeValueId": "L-xUrF9mGH" - } - ], - "additionalInformation": "**Description:** \nData that has been cleaned or modified to remove or reduce sensitive elements.\n\n**Examples:**\n- Logs with IP addresses removed\n- Datasets with names redacted\n- Masked account numbers\n\n**Elicitation questions:**\n- What fields were removed or masked?\n- Is sanitization irreversible?\n- Is sanitization applied consistently?\n\n[[ChatGPT]]" - } - ] - }, - { - "id": "T-hqzmK4lj", - "name": "VertexLocation", - "intendedFor": "Vertex", - "values": [ - { - "id": "L-Y9Hi8FwJ", - "text": "User", - "excludes": [ { "labelTypeId": "T-hqzmK4lj", "labelTypeValueId": "L-BnqZZq2f" }, { "labelTypeId": "T-hqzmK4lj", - "labelTypeValueId": "L-EU7CXdo5" + "labelTypeValueId": "L-jdroojuo" } ], - "additionalInformation": "**Description:** \nData resides on or is processed directly on the user\u2019s device.\n\n**Examples:**\n- Data stored locally in a mobile app\n- On-device analytics or ML inference\n- Browser-based form data before submission\n\n**Elicitation questions:**\n- Does data leave the user\u2019s device?\n- Is data encrypted at rest on the device?\n- What happens if the device is lost or compromised?\n\n[[ChatGPT]]" + "additionalInformation": "**Description**\n- Endpoints that are accessible by anyone. No technical safeguards prevent or limit that access to data.\n\n**Examples**\n- Searching user by phone number can be used to discover/confirm someone else's phone number.\n- The original publication of a company's foundation reveals the home addresses of the founders.\n- Query interfaces to search for people working in specific area, may reveal their location of employment.\n- Posting private messages to a person through a social network in a public forum that may be seen and monitored by many.\n- The lack of encryption enables an external adversary to read the personal data being exchanged with the system.\n\n**Elicitation Questions**\n\n[[LINDDUN (Sion 2025)]]" }, { - "id": "L-BnqZZq2f", - "text": "Organization", + "id": "L-jdroojuo", + "text": "ThirdParty", "excludes": [ { "labelTypeId": "T-hqzmK4lj", @@ -125,25 +75,14 @@ }, { "labelTypeId": "T-hqzmK4lj", - "labelTypeValueId": "L-EU7CXdo5" - } - ], - "additionalInformation": "**Description:** \nData is stored or processed within systems controlled by the organization.\n\n**Examples:**\n- Internal databases\n- Company-managed cloud infrastructure\n- Internal analytics platforms\n\n**Elicitation questions:**\n- Which internal systems store or process the data?\n- Who within the organization can access it?\n- In which countries or regions is it hosted?\n\n[[ChatGPT]]" - }, - { - "id": "L-EU7CXdo5", - "text": "External", - "excludes": [ - { - "labelTypeId": "T-hqzmK4lj", - "labelTypeValueId": "L-Y9Hi8FwJ" + "labelTypeValueId": "L-BnqZZq2f" }, { "labelTypeId": "T-hqzmK4lj", - "labelTypeValueId": "L-BnqZZq2f" + "labelTypeValueId": "L-oHu78JWj" } ], - "additionalInformation": "**Description:** \nData is transferred to or processed by third parties outside the organization\u2019s direct control.\n\n**Examples:**\n- Cloud service providers\n- Payment processors\n- Analytics or marketing vendors\n\n**Elicitation questions:**\n- Which third parties receive this data?\n- What contractual or technical safeguards exist?\n- Is data shared in raw, pseudonymized, or aggregated form?\n- Is cross-border transfer involved?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nData is transferred to or processed by third parties outside the organization\u2019s direct control.\n\n**Examples:**\n- Cloud service providers\n- Payment processors\n- Analytics or marketing vendors\n\n[[ChatGPT]]" } ] }, @@ -153,57 +92,41 @@ "intendedFor": "Flow", "values": [ { - "id": "L-oHu78JWj", - "text": "Public", + "id": "L-h1fGHkX7", + "text": "PersonalData", "excludes": [ { "labelTypeId": "T-hDctW1RZ", - "labelTypeValueId": "L-h1fGHkX7" + "labelTypeValueId": "L-lovys0HK" } ], - "additionalInformation": "**Description:** \nData that is intentionally made available to the general public.\n\n**Examples:**\n- Public social media posts\n- Published reports\n- Open government datasets\n\n**Elicitation questions:**\n- Was this data deliberately made public?\n- Are there reuse or licensing limits?\n- Could context make it sensitive anyway?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:**\nData relating to an identified or identifiable individual.\n\n**Examples:**\n- Names, emails, IP addresses\n- Behavioral profiles\n- Location histories\n\n**Elicitation questions:**\n- Can this data identify a person directly or indirectly?\n- Is it regulated under privacy laws?\n- What harm could result from misuse?\n\n[[ChatGPT]]" }, { - "id": "L-h1fGHkX7", - "text": "PersonalData", + "id": "L-lovys0HK", + "text": "PublicData", "excludes": [ { "labelTypeId": "T-hDctW1RZ", - "labelTypeValueId": "L-oHu78JWj" + "labelTypeValueId": "L-h1fGHkX7" + }, + { + "labelTypeId": "T-hDctW1RZ", + "labelTypeValueId": "L-KQpPJy98" } ], - "additionalInformation": "**Description:**\nData relating to an identified or identifiable individual.\n\n**Examples:**\n- Names, emails, IP addresses\n- Behavioral profiles\n- Location histories\n\n**Elicitation questions:**\n- Can this data identify a person directly or indirectly?\n- Is it regulated under privacy laws?\n- What harm could result from misuse?\n\n[[ChatGPT]]" - } - ] - }, - { - "id": "T-4lebOxF1", - "name": "DataIdentifiability", - "intendedFor": "Flow", - "values": [ - { - "id": "L-Vdnn8zIC", - "text": "QuasiIdentifiable", - "excludes": [], - "additionalInformation": "**Description:** \nData that is not directly identifying alone but can identify individuals when combined.\n\n**Examples:**\n- ZIP code + birth date + gender\n- Job title + department\n- Partial location histories\n\n**Elicitation questions:**\n- What other datasets could be combined with this?\n- Has re-identification risk been assessed?\n- Is this data shared externally?\n\n[[ChatGPT]]" - }, - { - "id": "L-4hzWLvca", - "text": "Pseudonym", - "excludes": [], - "additionalInformation": "**Description:** \nData linked to an identifier that does not directly reveal identity but can be re-linked.\n\n**Examples:**\n- User IDs instead of names\n- Device IDs\n- Tokenized identifiers\n\n**Elicitation questions:**\n- Where is the mapping between pseudonym and real identity stored?\n- Who can access the re-identification key?\n- Is the pseudonym stable over time?\n\n[[ChatGPT]]" - }, - { - "id": "L-AziCqUMB", - "text": "UniquelyIdentifiable", - "excludes": [], - "additionalInformation": "**Description:** \nData that uniquely and directly identifies a person.\n\n**Examples:**\n- Full name + address\n- National ID number\n- Biometric identifiers\n\n**Elicitation questions:**\n- Does this data uniquely identify an individual on its own?\n- Is it legally protected or regulated?\n- Is collection strictly necessary?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nData that is intentionally made available to the general public.\n\n**Examples:**\n- Public social media posts\n- Published reports\n- Open government datasets\n\n**Elicitation questions:**\n- Was this data deliberately made public?\n- Are there reuse or licensing limits?\n- Could context make it sensitive anyway?\n\n[[ChatGPT]]" }, { - "id": "L-ki3tgyjB", - "text": "Authenticated", - "excludes": [], - "additionalInformation": "**Description:** \nData directly tied to a verified, logged-in identity.\n\n**Examples:**\n- User account profiles\n- Authenticated API usage logs\n- Payment transaction histories\n\n**Elicitation questions:**\n- Does this data require user authentication to generate or access?\n- Is identity verified or merely asserted?\n- Can activity be traced to a specific account?\n\n[[ChatGPT]]" + "id": "L-KQpPJy98", + "text": "PersonalDataAboutOtherPeople", + "excludes": [ + { + "labelTypeId": "T-hDctW1RZ", + "labelTypeValueId": "L-lovys0HK" + } + ], + "additionalInformation": "**Description**\n\n**Examples**\n\n**Elicitation Questions**" } ] }, @@ -216,13 +139,7 @@ "id": "L-u7wWmGo9", "text": "Accessible", "excludes": [], - "additionalInformation": "**Description:** \nIndividuals can access their data.\n\n**Examples:**\n- User dashboards\n- Data access request portals\n- Download-your-data features\n\n**Elicitation questions:**\n- How can users access their data?\n- Are there authentication or rate limits?\n- Is access complete or partial?\n\n[[ChatGPT]]" - }, - { - "id": "L-s2Op4rNS", - "text": "Deletable", - "excludes": [], - "additionalInformation": "**Description:** \nIndividuals can request deletion of their data.\n\n**Examples:**\n- Account deletion workflows\n- \u201cRight to be forgotten\u201d processes\n\n**Elicitation questions:**\n- Is deletion permanent or soft-deleted?\n- Are backups also deleted?\n- What legal exceptions apply?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nThe lack of control in accessing the personal data that is being processed.\n\n**Examples:**\n- The data subject is unable to access their personal data held and processed by the service.\n- The sensor data from a wearable is transmitted to a lifestyle tracking app, but the user cannot access statistics and information derived from their data.\n- It is not possible for a data subject to request access, neither directly through the system nor indirectly through a helpdesk.\n\n**Elicitation questions:**\n- Do data subjects lack the ability to access the personal data being collected, processed, stored, or disclosed about them?\n\n[[LINDDUN (Sion 2025)]]" }, { "id": "L-7Ix5ydqW", @@ -247,25 +164,19 @@ "id": "L-0YOIZs6L", "text": "ControlViaPreferences", "excludes": [], - "additionalInformation": "# Description\n\n# Examples\n\n# Elicitation Questions" + "additionalInformation": "**Description**\nThe lack of control of a data subject to set preferences or consent in how personal data is processed (or collected/stored).\n\n**Examples**\n- The data subject is unable to set appropriate preferences on which data is shared and why.\n- The data subject is unable to set their consent preferences for the processing of personal data.\n\n**Elicitation Questions**\n- Does the system enable the data subject to configure which personal data is processed and for what purposes?\n- Can the data subject alter their preferences afterwards?\n\n[[LINDDUN (Sion 2025)]]" }, { - "id": "L-IYBKXU20", - "text": "OptIn", - "excludes": [], - "additionalInformation": "**Description:** \nData is collected or processed only after explicit consent.\n\n**Examples:**\n- Marketing email subscriptions\n- Location tracking enabled by default off\n\n**Elicitation questions:**\n- What constitutes valid consent?\n- Is consent granular?\n- How is consent recorded?\n\n[[ChatGPT]]" - }, - { - "id": "L-uIQJl0lr", - "text": "OptOut", + "id": "L-AJf2Su62", + "text": "RectificationPossible", "excludes": [], - "additionalInformation": "**Description:** \nData is collected by default but individuals can refuse or stop processing.\n\n**Examples:**\n- Analytics cookies with opt-out\n- Default personalization settings\n\n**Elicitation questions:**\n- How easy is it to opt out?\n- Does opt-out fully stop processing?\n- Are users clearly informed?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nThe control (or lack thereof) in rectifying incorrect data or removing personal data that is no longer relevant.\n\n**Examples:**\n- The data subject is unable to correct their personal data.\n- When a data subject deletes their social media account, the account is disabled, but the actual data is not erased.\n- A data subject is unable to update their home address after relocating.\n\n**Elicitation questions:**\n- Do data subjects have the ability to correct or delete their personal data?\n\n[[LINDDUN (Sion 2025)]]" }, { - "id": "L-AJf2Su62", - "text": "RectificationPossible", + "id": "L-W2Cqnyye", + "text": "Awareness", "excludes": [], - "additionalInformation": "**Description:** \nIndividuals can correct inaccurate or outdated data.\n\n**Examples:**\n- Profile edit features\n- Address update forms\n\n**Elicitation questions:**\n- Which fields can users edit themselves?\n- How are corrections propagated?\n- Are changes logged?\n\n[[ChatGPT]]" + "additionalInformation": "**Description**\n(Un)awareness of processing focuses on data subjects (not) being aware about the collection, processing, storage, or disclosure of their personal data. The user does not need to be the data subject.\n\n**Examples**\n- Data subjects are not aware of the identities of the third parties with whom their data will be shared.\n- The privacy notice provided to the data subject was not presented in clear and plain language.\n- Data subjects are unaware that traffic cameras collect not only number plates but also facial images.\n- The data subject is unaware that their personal data is collected from third parties to construct more detailed user profiles for targeted advertising (e.g., Facebook).\n- The data subject is unaware the data may also be used for marketing purposes.\n- The data subject was not informed why or for how long their personal data is processed.\n- A user posting a picture on social media may not be aware that others in the picture are automatically tagged with a facial recognition system.\n- Using a DNA testing service can entail sharing medical information about family members.\n- A service invites a user to share their address book to find contacts on a service.\n\n**Elicitation Questions**\n- Are data subjects insufficiently informed about the processing of personal data, including the purposes and methods of the processing involved?\n- If a user shares personal data of others, is it clear what, why, and how that data is further processed?\n\n[[LINDDUN (Sion 2025)]]" } ] }, @@ -283,7 +194,7 @@ "labelTypeValueId": "L-I78hTu4G" } ], - "additionalInformation": "**Description:** \nUnnecessary categories or attributes are collected.\n\n**Examples:**\n- Collecting gender when not needed\n- Storing device fingerprints unnecessarily\n\n**Elicitation questions:**\n- Why is each data type needed?\n- What happens if this field is removed?\n- Who requested its inclusion?\n\n[[ChatGPT]]" + "additionalInformation": "**Description:** \nThe system acquires more sensitive or \ufb01negrained data than strictly necessary for its functionality.\n\n**Examples:**\n- Tracking a patient\u2019s weight is pertinent for dieting apps but not for a contact tracing application.\n- A smart meter shares realtime measurements rather than the aggregated consumption.\n- A camera application on a smartphone does not necessarily need to record the picture\u2019s location.\n\n**Elicitation questions:**\n- Is the data more sensitive than strictly necessary?\n- Is the data more \ufb01ne-grained than strictly necessary?\n- Does the data encoding include additional (meta)data?\n\n[[LINDDUN (Sion 2025)]]" }, { "id": "L-uoXCDQCw", @@ -305,7 +216,7 @@ "labelTypeValueId": "L-I78hTu4G" } ], - "additionalInformation": "**Description**\nThe total quantity of data collected and stored, considering scale across users, time, and systems. Volume affects storage risk, breach impact, system performance, and feasibility of privacy controls.\n\n**Examples**\n- Millions of daily log entries\n- High-resolution telemetry collected continuously\n- Small profile dataset per user but retained for millions of users\n- Large historical dataset kept for analytics\n\n**Elicitation questions**\n- How much data is collected per user, per event, or per time period?\n- What is the total volume currently stored?\n- How fast does the dataset grow?\n- Is all collected data actively used?\n- Could older data be summarized, sampled, or deleted?\n\n[[ChatGPT]]" + "additionalInformation": "**Description**\nThe system acquires more data than strictly needed for its functionality.\n\n**Examples**\n- Small profile dataset per user but retained for millions of users\n- Large historical dataset kept for analytics\n\n**Elicitation questions**\n- Is the amount of collected data necessary for the correct functioning of the system?\n- Are there more data subjects involved than necessary?\n\n[[LINDDUN (Sion 2025)]]" }, { "id": "L-I78hTu4G", @@ -340,6 +251,17 @@ } ], "additionalInformation": "**Description**\nThe length of time data is stored before it is deleted, anonymized, or irreversibly aggregated. Retention directly affects privacy risk, breach impact, and regulatory compliance.\n\n**Examples**\n- Authentication logs retained for 30 days for security investigations\n- Customer account data retained for the life of the account plus 90 days\n- Transaction records retained for 7 years to meet legal obligations\n- Telemetry data aggregated after 14 days and raw data deleted\n\n**Elicitation questions**\n- What is the defined retention period for this data?\n- Does retention differ between active systems, archives, and backups?\n- What event triggers deletion (time-based, account closure, user request)?\n- Is data deleted, anonymized, or merely archived at the end of retention?\n- Are there legal or contractual reasons for extended retention?\n\n[[ChatGPT]]" + }, + { + "id": "L-sZLmyixT", + "text": "ExcessiveEnrichment", + "excludes": [ + { + "labelTypeId": "T-3TGOog3X", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "**Description**\nPersonal data is further treated, analyzed, and enriched in a way that is not necessary to achieve the functionality of the system.\n\n**Examples**\n- A camera application on a smartphone does not need to perform face-based recognition or emotion detection.\n- Analyzing a user's blog posts for language proficiency is unnecessary for blogging functionality.\n- User profiles accumulate unnecessary details over time, tracking a broad range of actions or service usage that is not essential for the provided functionality.\n- Sentiment analysis can be applied to faces in pictures to derive the emotional state of the individual in the photo.\n\n**Elicitation Questions**\n- Is the data enrichment/analysis necessary for the system's functionality?\n\n[[LINDDUN (Sion 2025)]]" } ] }, @@ -373,8 +295,33 @@ ] }, { - "id": "T-1ez1Gjk9", - "name": "Observability", + "id": "T-OEnmvpc0", + "name": "DataIdentifiers", + "intendedFor": "Flow", + "values": [ + { + "id": "L-mSnT5dRo", + "text": "QuasiIdentifier", + "excludes": [], + "additionalInformation": "**Description:** \nData that contains combinations of attributes that are unique to a data subject.\n\n**Examples:**\n- Browser fingerprinting entails the combination of various properties to create a unique identifier, serving as a pseudonym for the user.\n- A combination of various attributes (city, birth date, language preference, etc.) could be adequate to identify individuals.\n\n**Elicitation questions:**\n- Is there free-form user provided data that is received or processed by the system?\n- Is data collected that may reveal the identifying information?" + }, + { + "id": "L-KMjL5GNo", + "text": "DirectIdentifier", + "excludes": [], + "additionalInformation": "**Description:** \nData that uniquely and directly identifies a person.\n\n**Examples:**\n- Full name + address\n- National ID number\n- Biometric identifiers\n\n**Elicitation questions:**\n- Does this data uniquely identify an individual on its own?\n- Is it legally protected or regulated?\n\n[[ChatGPT]]" + }, + { + "id": "L-fJBfXb28", + "text": "RevealingAttributes", + "excludes": [], + "additionalInformation": "**Description**\nA number of revealing attributes are included in the data which support the identification of the data subject.\n\n**Examples**\n- When an individual shares detailed data (such as location, employer, device type, etc.) in a feedback form, the provided information may be revealing enough to uniquely identify that person.\n\n**Elicitation Questions**\n- Is there free-form user provided data that is received or processed by the system?\n- Is data collected that may reveal the identifying information?" + } + ] + }, + { + "id": "T-VRJKLL6a", + "name": "DataObservability", "intendedFor": null, "values": [ { @@ -382,11 +329,11 @@ "text": "FullyVisible", "excludes": [ { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-M5uQ5X3f" }, { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-nvKZS1U9" } ], @@ -397,11 +344,11 @@ "text": "MetadataOnly", "excludes": [ { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-irbu2qs8" }, { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-nvKZS1U9" } ], @@ -412,39 +359,139 @@ "text": "NonObservable", "excludes": [ { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-irbu2qs8" }, { - "labelTypeId": "T-1ez1Gjk9", + "labelTypeId": "T-VRJKLL6a", "labelTypeValueId": "L-M5uQ5X3f" } ], "additionalInformation": "**Description:** \nNeither content nor metadata is accessible.\n\n**Examples:**\n- End-to-end encrypted messages with sealed metadata\n- Secure enclaves\n\n**Elicitation questions:**\n- Who (if anyone) can observe anything?\n- Is this by design or limitation?\n- How is misuse detected?\n\n[[ChatGPT]]" } ] + }, + { + "id": "T-06mBkaWM", + "name": "Data", + "intendedFor": "Flow", + "values": [ + { + "id": "L-kX4fJEps", + "text": "HasSideEffects", + "excludes": [ + { + "labelTypeId": "T-06mBkaWM", + "labelTypeValueId": "L-I78hTu4G" + } + ], + "additionalInformation": "**Description**\nAction side-effects can cause an action to be attributable to an individual.\n\n**Examples**\n- Read notifications serve as evidence that the user has opened/read the message.\n- An individual's browser history may be used to substantiate claims about their online activities.\n- Actions can be logged as evidence.\n\n**Elicitation Questions**\n- Do (passive) interactions with the system (e.g., receiving a message) have side-effects (e.g., trigger transmissions, logging)?" + }, + { + "id": "L-OFziM7dm", + "text": "ContainsHiddenData", + "excludes": [], + "additionalInformation": "**Description**\nEmbedded or hidden data can be used to reveal additional information.\n\n**Examples**\n- Data watermarked with hidden artifacts (uniquely linked to a person) can be used to track the person revealing or disclosing the data afterwards.\n- Remote resources (e.g. image in email) are automatically loaded to track the user opening it.\n- Secrets shared with one person, or modified for each recipient can serve as evidence of the disclosure of that data by that person.\n\n**Elicitation Questions**\n- Are there embedded data or hidden patterns in the data or transmissions?" + } + ] } ], "constraints": [ { "name": "DataWithExcessiveFrequencyIsProcessed", - "constraint": "data DataPrecision.ExcessiveFrequency neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "constraint": "data DataPrecision.ExcessiveFrequency neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" }, { "name": "DataWithExcessiveVolumeIsProcessed", - "constraint": "data DataPrecision.ExcessiveVolume neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "constraint": "data DataPrecision.ExcessiveVolume neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" }, { - "name": "IdentifyingThroughQuasiIdentifier", - "constraint": "data DataIdentifiability.QuasiIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "name": "NonRepudiationThroughSignature", + "constraint": "data DataIntegrity.Signed neverFlows vertex VertexLocation.Organization" }, { - "name": "IdentifyingThroughUniqueIdentifier", - "constraint": "data DataIdentifiability.UniquelyIdentifiable neverFlows vertex VertexLocation.Organization,VertexLocation.External" + "name": "DataWithExcessiveTypesIsProcessed", + "constraint": "data DataPrecision.ExcessiveDataTypes neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" }, { - "name": "NonRepudiationThroughSignature", - "constraint": "data DataIntegrity.Signed neverFlows vertex VertexLocation.Organization" + "name": "DataIsEnriched", + "constraint": "data DataPrecision.ExcessiveEnrichment neverFlows VertexLocation.Organization,VertexLocation.ThirdParty" + }, + { + "name": "DataIsStoredForLongerThanNeeded", + "constraint": "data DataPrecision.ExcessiveRetention neverFlows VertexLocation.Organization" + }, + { + "name": "PersonalDataCollectionCannotBeControlled", + "constraint": "data !DataInterveniability.ControlViaPreferences neverFlows vertex VertexLocation.Organization" + }, + { + "name": "PersonalDataCannotBeAccessed", + "constraint": "data !DataInterveniability.Accessible neverFlows vertex VertexLocation.Organization" + }, + { + "name": "PersonalDataCannotBeRectified", + "constraint": "data !DataInterveniability.RectificationPossible neverFlows vertex VertexLocation.Organization" + }, + { + "name": "InsufficientTransparency", + "constraint": "data !DataInterveniability.Awareness neverFlows vertex VertexLocation.Organization" + }, + { + "name": "StoringofIdentifiedData", + "constraint": "data DataIdentifiers.DirectIdentifier,DataIdentifiers.QuasiIdentifier neverFlows vertex type STORE" + }, + { + "name": "LinkingThroughUniqueIdentifier", + "constraint": "data DataIdentifiers.DirectIdentifier neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" + }, + { + "name": "DataIsDisclosedToOtherParties", + "constraint": "neverFlows vertex VertexLocation.ThirdParty,VertexLocation.Public" + }, + { + "name": "LinkingThroughQuasiIdentifier", + "constraint": "data DataIdentifiers.QuasiIdentifier neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" + }, + { + "name": "LinkingThroughDataOfDifferentIndividuals", + "constraint": "data DataSensitivity.PersonalDataAboutOtherPeople neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" + }, + { + "name": "ProfilingAnIndividual", + "constraint": "data DataSensitivity.PersonalData neverFlows vertex VertexLocation.Organization,VertexLocation.ThirdParty" + }, + { + "name": "PersonalDataIsPublishedMoreBroadlyThanNecessary", + "constraint": "data DataSensitivity.PersonalData neverFlows VertexLocation.Public" + }, + { + "name": "DataHasSideEffects", + "constraint": "data Data.HasSideEffects neverFlows" + }, + { + "name": "NonRepudiationThroughHiddenData", + "constraint": "data Data.ContainsHiddenData neverFlows vertex VertexLocation.Organization" + }, + { + "name": "ImplicitDataDisclosure", + "constraint": "data Data.HasSideEffects neverFlows vertex type STORE" + }, + { + "name": "ProcessingOfIdentifiedData", + "constraint": "data DataIdentifiers.DirectIdentifier neverFlows vertex VertexLocation.Organization" + }, + { + "name": "ProcessingOfIdentifiedMetadata", + "constraint": "data DataIdentifiers.DirectIdentifier data DataObservability.MetadataOnly neverFlows vertex VertexLocation.Organization" + }, + { + "name": "ProcessingOfQuasiIdentifiedData", + "constraint": "data DataIdentifiers.QuasiIdentifier neverFlows vertex VertexLocation.Organization" + }, + { + "name": "ProcessingOfRevealingAttributes", + "constraint": "data DataIdentifiers.RevealingAttributes neverFlows vertex VertexLocation.Organization" } ] -} +} \ No newline at end of file From 38413d92a68411928f3b0f5a8aea82d6861ec48e Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Wed, 18 Feb 2026 18:53:24 +0100 Subject: [PATCH 23/23] Remove TODO --- frontend/webEditor/src/labels/ThreatModelingLabelType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts index 4b0bda2c..d68c73d7 100644 --- a/frontend/webEditor/src/labels/ThreatModelingLabelType.ts +++ b/frontend/webEditor/src/labels/ThreatModelingLabelType.ts @@ -1,7 +1,7 @@ import { LabelAssignment, LabelType, LabelTypeValue } from "./LabelType.ts"; export interface ThreatModelingLabelType extends LabelType { - intendedFor: 'Vertex' | 'Flow' //TODO maybe stattdessen hier 'Node' und 'Edge' verwenden + intendedFor: 'Vertex' | 'Flow' } export interface ThreatModelingLabelTypeValue extends LabelTypeValue {