From d7e3ce3fb60a6ce555ca8f8f7e0bbd274c8f06fd Mon Sep 17 00:00:00 2001 From: Benoit Legien Date: Thu, 8 Jan 2026 10:20:24 +0100 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 +}