From 4dca827382f55edc684dcdf6d8c9df7955b02baf Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Wed, 26 Aug 2026 09:43:33 +0200 Subject: [PATCH 1/6] [backend] ADR markings --- ...-marking-definitions-create-manage-plan.md | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 adr/ADR-007-marking-definitions-create-manage-plan.md diff --git a/adr/ADR-007-marking-definitions-create-manage-plan.md b/adr/ADR-007-marking-definitions-create-manage-plan.md new file mode 100644 index 00000000000..d00e76af4ce --- /dev/null +++ b/adr/ADR-007-marking-definitions-create-manage-plan.md @@ -0,0 +1,326 @@ +# ADR-007: Marking definitions (create/manage) - brainstorm and implementation plan + +| | | +|---------|--------------------------------------------------------| +| Status | Proposed | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7512 | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7513 | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7514 | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7515 | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7516 | +| Related | https://github.com/OpenAEV-Platform/openaev/issues/7517 | + +## 1. Context + +Task 1 introduces the foundation for marking-based visibility controls in OpenAEV. +Scope from the provided user stories: + +- US1: split RBAC into two independent capability chains: + - Marking definitions: Access -> Manage -> Delete + - Assign marking: Access -> Assign -> Delete +- US2: add a dedicated Marking Definitions entry/page under Settings -> Security +- US3: create marking definitions (type, definition, color, order) +- US4: edit existing definitions +- US5: delete definitions with in-use warning +- US6: preload default TLP definitions on platform initialization and protect them from deletion + +Product constraints confirmed in the source document: + +- Type is user-defined, not restricted to TLP. +- Type is immutable after creation (not editable from UI or API update). +- TLP is a built-in default type only. +- Order comparison is within a given type only. +- Menu/actions are hidden when unauthorized (not disabled). +- BYPASS capability grants access. + +## 2. Decision drivers + +- Security and segregation of duties (definition management vs assignment operations). +- Multi-tenant safety with v2 isolation (no cross-tenant visibility or modifications). +- Consistency with existing OpenAEV RBAC hierarchy and UI behavior. +- Backward-safe rollout (defaults preloaded, no forced migrations for users). +- Search/filter UX parity with existing admin list screens. + +## 3. Brainstorm outcomes + +### 3.1 Domain model proposal + +Introduce a new entity `MarkingDefinition` (tenant-scoped, v2 table) with fields: + +- `marking_definition_id` (UUID string) +- `marking_definition_type` (string, required) +- `marking_definition_definition` (string, required) - human-visible label like `TLP:AMBER` +- `marking_definition_color` (string, optional hex with validation) +- `marking_definition_order` (integer, required, `>= 0`) +- `marking_definition_protected` (boolean, default false) - true for built-in or locked rows +- audit fields (`created_at`, `updated_at`) +- `tenant_id` (not null) + +Multi-tenancy mode for this feature: + +- implement as tenant **v2** (`TenantStatementInspector` + `TxCtx`), not v1 Hibernate `@Filter` +- once activated in `openaev.tenant.active-tables`, do not add `@Filter("tenantFilter")` on this table +- every transactional entrypoint that can reach `marking_definitions` must declare `TxCtx` + +Uniqueness and ordering rules: + +- unique by tenant on (`marking_definition_type`, `marking_definition_definition`) +- order must be a non-negative integer (0 or greater) +- duplicate order values are allowed within the same type +- order has meaning only inside one type (sorting/precedence), but it is not unique + +### 3.2 RBAC model proposal + +Add a new top-level capability group `MARKING` with two independent chains: + +- definitions chain: + - `ACCESS_MARKING_DEFINITION` + - `MANAGE_MARKING_DEFINITION` + - `DELETE_MARKING_DEFINITION` +- assignment chain: + - `ACCESS_MARKING_ASSIGNMENT` + - `ASSIGN_MARKING` + - `DELETE_MARKING_ASSIGNMENT` + +Behavior: + +- parent auto-enable cascade follows current capability tree behavior +- BYPASS continues to override both chains + +### 3.3 API and UX proposal + +- Add page route under Security menu: Marking Definitions +- Add CRUD endpoints for Marking Definitions with DTOs and pagination/search +- Add list columns required by US2: Type, Definition, Color, Order, Creation date +- List must support sorting on Type, Definition, Color, Order, and Creation date +- List must support filtering on Type, Definition, Color, Order, and Creation date +- Search must cover Type, Definition, Color, Order, and Creation date +- Add create/edit modal with required validation for type/definition/order (order `>= 0`) +- Create modal fields are: Type, Definition, Color, Order +- Type input supports selecting an existing type or entering a new custom type (not restricted to TLP) +- Color uses a color picker UI and stores a normalized color value (hex) +- Order is required and numeric +- After successful creation, the new marking appears immediately in the list +- In edit mode, `type` is read-only/disabled on frontend and ignored/rejected if sent to update API +- Add delete confirmation dialog +- For protected markings, hide/disable delete action in the UI +- If marking is in use, return warning payload and confirm deletion explicitly (or block based on product final choice) +- After a successful delete, refresh impacted frontend stores so Assets and Users no longer display the removed marking + +### 3.4 Seed/default proposal (US6) + +On tenant creation, seed the 5 TLP defaults through the tenant datapack: + +- TLP:CLEAR (1) +- TLP:GREEN (2) +- TLP:AMBER (3) +- TLP:AMBER+STRICT (4) +- TLP:RED (5) + +Mark these rows as `protected=true` so update and delete are forbidden. +Protected rows are immutable from the UI and API (no edit, no delete). +Seeding must be idempotent in case datapack application is retried. + +## 4. Considered options + +### Option A: hardcode only TLP enum + +Pros: +- simple validation + +Cons: +- conflicts with requirement that type is user-defined +- blocks PAP/custom future types + +### Option B: user-defined type string + per-type ordering (selected) + +Pros: +- matches all user stories +- extensible without schema changes +- keeps TLP as defaults, not restriction + +Cons: +- needs stronger validation while keeping order non-unique within a type + +### Option C: separate tables for types and definitions + +Pros: +- normalized type management + +Cons: +- larger scope for Task 1 +- not required by current acceptance criteria + +## 5. Decision + +Choose Option B. + +Implement a tenant-scoped `MarkingDefinition` with user-defined `type`, per-type `order`, and default non-deletable TLP rows seeded by the tenant datapack at tenant creation. +Implement RBAC split with two independent capability chains under a new `MARKING` group. + +## 6. Implementation plan (single feature plan, chunked delivery) + +### Chunk 1 - RBAC foundation (US1) + +Backend: + +- update capability catalog and parent hierarchy +- expose new group in capability tree API +- ensure permission checks include BYPASS behavior + +Frontend: + +- map capability strings in permission parser +- ensure role editor renders `MARKING` group and both chains +- gate Security menu entry and Group/Asset marking actions by new capabilities + +Tests: + +- role capability cascade and independence tests +- hidden vs visible menu/action behavior +- BYPASS override tests + +### Chunk 2 - Marking Definitions backend CRUD (US2/US3/US4/US5 backend side) + +Backend model/repository/service/API: + +- create `MarkingDefinition` entity, repository, service +- create search endpoint with pagination/filter/sort +- expose searchable/filterable/sortable fields for Type, Definition, Color, Order, and Creation date +- create create/update/delete endpoints and DTOs/mappers +- enforce uniqueness and validation rules +- enforce create validation: required `type`, `definition`, and non-negative numeric `order` (`>= 0`) +- enforce immutable type on update (`marking_definition_type` cannot change after creation) +- enforce no delete for `protected=true` rows (backend guard) +- enforce no update for `protected=true` rows (backend guard) +- TODO (future): implement in-use check API contract for delete warning flow +- TODO (future): define and implement deletion side-effects for linked entities (Assets and Users): remove/unlink the deleted marking and keep data consistent +- wire `TxCtx` on API/service transactional entrypoints that read/write marking definitions +- register required entrypoints in `TenantScopedEntrypointsTxCtxArchTest` + +Migration: + +- add table and indexes +- add unique constraint `(type, definition, tenant_id)` +- keep `(type, order, tenant_id)` non-unique to allow same order in a type +- include FK/index on `tenant_id` +- activate `marking_definitions` in `openaev.tenant.active-tables` in the same rollout commit + +Tests: + +- integration tests for CRUD, validation, uniqueness, and non-deletable defaults +- integration tests for type immutability on update (backend rejects/ignores type changes) +- integration tests for search/filter/sort on Type, Definition, Color, Order, and Creation date +- integration tests for delete propagation: deleted marking is no longer linked from Assets and Users +- tenant isolation tests for search and mutations + +### Chunk 3 - Marking Definitions frontend CRUD (US2/US3/US4/US5 frontend side) + +Frontend: + +- add Marking Definitions page in Settings -> Security +- add paginated table, search, filters, and sorting +- render columns Type, Definition, Color, Order, and Creation date +- add create/edit dialog with field validation +- make `type` non-editable in edit form (readonly/disabled field) +- when `order` changes in edit mode, show a warning confirmation dialog before saving (reuse delete confirmation dialog pattern) +- add delete confirmation +- TODO (future): add in-use warning path +- disable/hide edit action for `marking_definition_protected=true` +- disable/hide delete action for `marking_definition_protected=true` +- on delete success, refresh Marking Definitions store/query +- TODO (future): refresh Assets and Users stores/queries if unlink-on-delete is implemented +- hide entire feature when unauthorized + +Tests: + +- component tests for visibility, validations, and CRUD interactions +- component tests for edit form type field locked in update mode +- component tests for order-change warning dialog and confirmed save path +- component tests for column rendering and search/filter/sort behavior on Type, Definition, Color, Order, and Creation date +- component tests for create modal: field presence, required-field validation, non-negative numeric order validation (`>= 0`), and immediate list refresh after create +- component tests for delete refresh behavior on Marking Definitions, Assets, and Users views +- permission-based rendering tests + +### Chunk 4 - Default seed lifecycle (US6) + +Backend: + +- seed TLP defaults idempotently in the tenant datapack (applied at tenant creation) +- set `protected=true` for defaults + +Tests: + +- datapack idempotency test on tenant provisioning (no duplicates) +- update/delete forbidden for protected rows (backend) and edit/delete actions hidden/disabled (frontend) +- coexistence test with custom types + +## 7. API contract sketch + +Base path (tenant API style): + +- `POST /api/{tenant}/marking-definitions/search` -> `Page` +- `POST /api/{tenant}/marking-definitions` -> create +- `PUT /api/{tenant}/marking-definitions/{id}` -> update +- `DELETE /api/{tenant}/marking-definitions/{id}` -> delete + +Output fields: + +- `marking_definition_id` +- `marking_definition_type` +- `marking_definition_definition` +- `marking_definition_color` +- `marking_definition_order` +- `marking_definition_protected` +- `marking_definition_created_at` + +## 8. Data safety, tenancy, and performance notes + +- Entity must be tenant-scoped with `tenant_id NOT NULL` and never expose tenant relation in output. +- Tenant isolation must use v2 (`TxCtx` + statement inspector), not v1 `@Filter` for this table. +- Any native query touching this table must use a SQL shape accepted by `TenantStatementInspector`. +- Search endpoint must be paginated; avoid unbounded lists. +- Add DB indexes for all FK and common filter/sort fields (`type`, `definition`, `order`, `created_at`). +- Keep delete flow transactionally safe with explicit in-use checks. + +## 9. Telemetry plan (minimum) + +Use classic OpenAEV audit expectations for: + +- marking definition viewed/searched +- create success/failure +- update success/failure +- delete attempted/blocked/success +- RBAC-denied access attempts + +Optional counters (future, if needed): + +- `marking_definition_created_total` +- `marking_definition_updated_total` +- `marking_definition_deleted_total` +- `marking_definition_delete_blocked_in_use_total` + +## 10. Risks and open questions + +- TODO (future): Delete semantics for in-use markings (block hard vs force detach after explicit confirmation). +- TODO (future): Existing unlink/refresh behavior for Assets and Users after marking deletion must be verified; implement if missing. +- Decision (option 1): protected markings are immutable (no edit, no delete). +- Decision (option 2): assignment actions for Groups and Assets remain partially stubbed behind capabilities in Task 1. +- Deferred: import/export behavior is out of current scope and will be defined later. + +## 11. Acceptance mapping checklist + +- US1 AC1-AC5: RBAC tree, independence, hidden behavior, BYPASS, parent cascade. +- US2 AC1-AC6: menu + list columns (Type, Definition, Color, Order, Creation date) + search/filter/sort on those columns + auth denial. +- US3 AC1-AC4: create modal fields (Type/Definition/Color/Order), existing-or-new Type input, required Type/Definition/non-negative numeric Order validation (`>= 0`), immediate list refresh after create, and per-type order semantics. +- US4 AC1-AC3: edit action and immediate list refresh, with `type` non-editable and warning confirmation when `order` is changed. +- US5 AC1-AC3: delete action, confirmation, in-use warning behavior. +- US6 AC1-AC2: default TLP preloaded and non-deletable. + +## 12. Rollout + +- Feature branch with sequential chunks above. +- Keep migrations idempotent and forward-only. +- Validate RBAC and tenant v2 isolation first, then UI exposure, then seed behavior. +- Prepare release note: "Marking Definitions foundation (RBAC + CRUD + default TLP seeds)". + From b9d05f80b4fd6b23f7c19b6e393700ee8c28f80a Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Wed, 26 Aug 2026 11:44:30 +0200 Subject: [PATCH 2/6] [backend] ADR markings --- adr/ADR-007-marking-definitions-create-manage-plan.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/adr/ADR-007-marking-definitions-create-manage-plan.md b/adr/ADR-007-marking-definitions-create-manage-plan.md index d00e76af4ce..19d31abf9dd 100644 --- a/adr/ADR-007-marking-definitions-create-manage-plan.md +++ b/adr/ADR-007-marking-definitions-create-manage-plan.md @@ -32,6 +32,7 @@ Product constraints confirmed in the source document: - Order comparison is within a given type only. - Menu/actions are hidden when unauthorized (not disabled). - BYPASS capability grants access. +- Feature is gated by feature flag `MARKING`; when disabled, the frontend menu entry is hidden. ## 2. Decision drivers @@ -89,8 +90,11 @@ Behavior: ### 3.3 API and UX proposal +- Add feature flag `MARKING` to gate Marking Definitions exposure in frontend. +- When `MARKING` is disabled, mask the Marking Definitions menu entry in the frontend Security section. - Add page route under Security menu: Marking Definitions - Add CRUD endpoints for Marking Definitions with DTOs and pagination/search +- Frontend list uses the classic pagination front list component pattern. - Add list columns required by US2: Type, Definition, Color, Order, Creation date - List must support sorting on Type, Definition, Color, Order, and Creation date - List must support filtering on Type, Definition, Color, Order, and Creation date @@ -167,12 +171,15 @@ Backend: - update capability catalog and parent hierarchy - expose new group in capability tree API - ensure permission checks include BYPASS behavior +- add feature flag plumbing for `MARKING` and default rollout value (off by default) Frontend: - map capability strings in permission parser - ensure role editor renders `MARKING` group and both chains - gate Security menu entry and Group/Asset marking actions by new capabilities +- gate Marking Definitions pages/routes/actions behind the same feature flag +- mask Marking Definitions menu entry when `MARKING` is disabled Tests: @@ -219,7 +226,7 @@ Tests: Frontend: - add Marking Definitions page in Settings -> Security -- add paginated table, search, filters, and sorting +- add paginated table, search, filters, and sorting using the classic pagination front list component - render columns Type, Definition, Color, Order, and Creation date - add create/edit dialog with field validation - make `type` non-editable in edit form (readonly/disabled field) @@ -307,6 +314,7 @@ Optional counters (future, if needed): - Decision (option 1): protected markings are immutable (no edit, no delete). - Decision (option 2): assignment actions for Groups and Assets remain partially stubbed behind capabilities in Task 1. - Deferred: import/export behavior is out of current scope and will be defined later. +- Decision: feature flag key is `MARKING`; frontend behavior is menu masking when disabled. ## 11. Acceptance mapping checklist @@ -322,5 +330,6 @@ Optional counters (future, if needed): - Feature branch with sequential chunks above. - Keep migrations idempotent and forward-only. - Validate RBAC and tenant v2 isolation first, then UI exposure, then seed behavior. +- Roll out behind feature flag: merge dark, enable progressively after validation. - Prepare release note: "Marking Definitions foundation (RBAC + CRUD + default TLP seeds)". From 325118b4f28512f1147b36cc0b4794eabf85d5c8 Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Thu, 27 Aug 2026 17:59:25 +0200 Subject: [PATCH 3/6] [backend/frontend] first dev markings # Conflicts: # openaev-front/src/admin/components/nav/config/settings.config.tsx # openaev-front/src/admin/components/settings/Index.tsx # openaev-front/src/admin/components/settings/SecurityMenu.tsx --- adr/ADR-007-implementation-plan.md | 186 ++++++++ adr/ADR-007-implementation-task-board.md | 179 +++++++ ...-marking-definitions-create-manage-plan.md | 8 +- .../MarkingDefinitionApi.java | 110 +++++ .../MarkingDefinitionMapper.java | 30 ++ .../form/MarkingDefinitionInput.java | 12 + .../form/MarkingDefinitionOutput.java | 15 + ...826120000000__Add_marking_definitions.java | 80 ++++ .../processor/datapack/PresetTenantData.java | 18 + .../V20260826_Default_tenant_markings.java | 48 ++ .../openaev/rest/settings/PreviewFeature.java | 3 +- .../MarkingDefinitionService.java | 170 +++++++ .../src/main/resources/application.properties | 2 +- .../MarkingDefinitionApiTest.java | 440 ++++++++++++++++++ .../TenantActiveTableAccessArchTest.java | 24 +- .../TenantScopedEntrypointsTxCtxArchTest.java | 5 + openaev-front/src/actions/Schema.js | 11 + .../marking-definition-actions.ts | 35 ++ .../components/nav/config/settings.config.tsx | 9 +- .../src/admin/components/settings/Index.tsx | 26 ++ .../components/settings/SecurityMenu.tsx | 4 + .../MarkingDefinitionForm.tsx | 121 +++++ .../MarkingDefinitionPopover.tsx | 115 +++++ .../MarkingDefinitionStoreHelper.ts | 15 + .../MarkingDefinitions.tsx | 242 ++++++++++ openaev-front/src/reducers/Referential.ts | 1 + openaev-front/src/utils/api-types.d.ts | 71 +++ openaev-front/src/utils/lang/de.json | 14 + openaev-front/src/utils/lang/en.json | 14 + openaev-front/src/utils/lang/es.json | 14 + openaev-front/src/utils/lang/fr.json | 14 + openaev-front/src/utils/lang/it.json | 14 + openaev-front/src/utils/lang/ja.json | 14 + openaev-front/src/utils/lang/ko.json | 14 + openaev-front/src/utils/lang/ru.json | 14 + openaev-front/src/utils/lang/zh.json | 14 + openaev-front/src/utils/permissions/types.ts | 2 + .../io/openaev/database/model/Capability.java | 27 ++ .../database/model/CapabilityGroup.java | 1 + .../database/model/MarkingDefinition.java | 112 +++++ .../openaev/database/model/ResourceType.java | 2 + .../MarkingDefinitionRepository.java | 28 ++ 42 files changed, 2270 insertions(+), 8 deletions(-) create mode 100644 adr/ADR-007-implementation-plan.md create mode 100644 adr/ADR-007-implementation-task-board.md create mode 100644 openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java create mode 100644 openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionMapper.java create mode 100644 openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionInput.java create mode 100644 openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionOutput.java create mode 100644 openaev-api/src/main/java/io/openaev/migration/V6_20260826120000000__Add_marking_definitions.java create mode 100644 openaev-api/src/main/java/io/openaev/processor/datapack/V20260826_Default_tenant_markings.java create mode 100644 openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java create mode 100644 openaev-api/src/test/java/io/openaev/api/marking_definition/MarkingDefinitionApiTest.java create mode 100644 openaev-front/src/actions/marking_definitions/marking-definition-actions.ts create mode 100644 openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionForm.tsx create mode 100644 openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx create mode 100644 openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionStoreHelper.ts create mode 100644 openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitions.tsx create mode 100644 openaev-model/src/main/java/io/openaev/database/model/MarkingDefinition.java create mode 100644 openaev-model/src/main/java/io/openaev/database/repository/MarkingDefinitionRepository.java diff --git a/adr/ADR-007-implementation-plan.md b/adr/ADR-007-implementation-plan.md new file mode 100644 index 00000000000..b4e861cfa94 --- /dev/null +++ b/adr/ADR-007-implementation-plan.md @@ -0,0 +1,186 @@ +# ADR-007 Implementation Plan - Marking Definitions (Single-Chunk Delivery) + +## 1) Goal + +Deliver ADR-007 Task 1 in one integrated execution chunk, behind feature flag `MARKING`, with tenant-v2-safe backend CRUD, frontend classic paginated list UX, protected default TLP seeds, and complete validation gates. + +## 2) Confirmed decisions and constraints + +- `marking_definition_order` is required and must be `>= 0`. +- Protected rows are immutable: no edit and no delete. +- Assignment actions remain partially stubbed in Task 1. +- In-use delete flow and unlink side-effects are deferred TODOs. +- Import/export behavior is deferred. +- Feature flag key is `MARKING`; frontend behavior is menu masking when disabled. +- Front list uses classic pagination component pattern. + +## 3) Single execution chunk (end-to-end) + +### 3.1 Workstream A - Architecture and baseline alignment + +- Create a baseline scope note in the PR description with impacted layers: RBAC, model, migration, API, frontend, seeds, tests. +- Confirm no code is added in deprecated `openaev-framework`. +- Keep API additions in `io.openaev.api.*` (not legacy `io.openaev.rest`). +- Define rollback strategy: keep `MARKING` default off so merge is dark-deploy safe. + +### 3.2 Workstream B - RBAC and feature flag (`MARKING`) + +- Add capability chains: + - Definitions: `ACCESS_MARKING_DEFINITION` -> `MANAGE_MARKING_DEFINITION` -> `DELETE_MARKING_DEFINITION` + - Assignment: `ACCESS_MARKING_ASSIGNMENT` -> `ASSIGN_MARKING` -> `DELETE_MARKING_ASSIGNMENT` +- Register parent hierarchy and ensure BYPASS still grants effective access. +- Expose MARKING group in capability tree API. +- Add/confirm preview flag `MARKING` backend plumbing (default off). +- Frontend: + - Parse new capabilities. + - Render MARKING group in role editor. + - Mask Marking Definitions menu entry when `MARKING` is disabled. + - Keep route/action guards consistent with capabilities and backend checks. + +### 3.3 Workstream C - Data model, migration, and tenant-v2 activation + +- Create entity `MarkingDefinition` (tenant-scoped v2 table) with fields: + - `marking_definition_id` + - `marking_definition_type` + - `marking_definition_definition` + - `marking_definition_color` + - `marking_definition_order` + - `marking_definition_protected` + - `tenant_id` + - audit timestamps +- Add Java Flyway migration with idempotent guards. +- Add indexes/constraints: + - unique `(marking_definition_type, marking_definition_definition, tenant_id)` + - non-unique `(marking_definition_type, marking_definition_order, tenant_id)` + - index on `tenant_id` + - indexes for `type`, `definition`, `order`, `created_at` +- Activate `marking_definitions` in `openaev.tenant.active-tables` in same go-live commit. +- Add required `TxCtx` parameters on entrypoints reaching this table. +- Update `TenantScopedEntrypointsTxCtxArchTest` registration for all required entrypoints. + +### 3.4 Workstream D - Backend CRUD API and business rules + +- Build full stack: repository, service, DTOs, mapper, controller. +- Endpoints: + - `POST /api/{tenant}/marking_definitions/search` + - `POST /api/{tenant}/marking_definitions` + - `PUT /api/{tenant}/marking_definitions/{id}` + - `DELETE /api/{tenant}/marking_definitions/{id}` +- Validation and invariants: + - required `type`, `definition`, `order` + - `order >= 0` + - `type` immutable on update + - `protected=true` blocks update/delete +- Keep explicit TODO boundaries in code/API behavior: + - in-use delete warning flow not implemented in Task 1 + - unlink propagation to Assets/Users not implemented in Task 1 + +### 3.5 Workstream E - Frontend page and forms + +- Add Marking Definitions page under Security. +- Implement classic list pattern with pagination/search/filter/sort. +- Columns: Type, Definition, Color, Order, Creation date. +- Create/Edit dialog: + - required validation + - non-negative order validation + - type editable on create, read-only on edit + - order-change confirmation in edit mode +- Delete: + - confirmation dialog + - hide/disable actions for protected rows + - refresh Marking Definitions list/store on success +- Feature flag behavior: + - menu entry masked when `MARKING` disabled + +### 3.6 Workstream F - Default TLP seed lifecycle + +- Seed on tenant creation via datapack (idempotent): + - `TLP:CLEAR (1)` + - `TLP:GREEN (2)` + - `TLP:AMBER (3)` + - `TLP:AMBER+STRICT (4)` + - `TLP:RED (5)` +- Set `protected=true` for seeded rows. + +### 3.7 Workstream G - Telemetry and audit + +- Apply classic OpenAEV audit coverage for view/search/create/update/delete attempt outcomes and RBAC denials. +- Optional counters remain future and non-blocking for Task 1. + +### 3.8 Workstream H - Deferred items tracking + +- Create follow-up issues for: + - in-use delete policy + - unlink side-effects + Assets/Users refresh + - import/export behavior + - assignment UX completion for Groups/Assets + +## 4) All OpenAEV skills usage map + +This delivery uses every listed skill either as implementation driver or as mandatory review gate. + +| Skill | How it is used in this plan | Expected output | +|---|---|---| +| `create-feature-module` | Primary scaffold for entity -> repository -> service -> API -> frontend path | Base implementation skeleton aligned with layering | +| `add-migration` | Build Java Flyway migration + indexes/constraints + active-tables update | Safe, idempotent migration | +| `activate-tenant-table` | Apply tenant-v2 activation checklist + TxCtx call graph inventory | v2 isolation correctness and entrypoint coverage | +| `add-test` | Add integration/component tests for CRUD, validation, visibility | Test suite for Task 1 scope | +| `review-code` | Global review gate for architecture/convention compliance | Consolidated review findings | +| `review-security` | Verify RBAC, endpoint protection, exposure risks | Security review sign-off | +| `review-performance` | Check pagination/search/index usage and query patterns | Performance review sign-off | +| `review-multi-tenancy` | Verify tenant-v2 isolation, TxCtx propagation, cross-tenant safety | Multi-tenancy review sign-off | +| `review-migration` | Audit migration idempotency, safety, and rollout risk | Migration review sign-off | +| `review-frontend` | Validate list/form/permission/i18n patterns and flag behavior | Frontend review sign-off | +| `review-docs` | Ensure ADR/docs updates reflect functional changes | Docs coverage sign-off | +| `review-chaining-engine` | Explicitly run N/A check to confirm no chaining engine impact | Recorded N/A confirmation | +| `reduce-tx-baseline` | Apply only if any new change introduces baseline-related transaction pattern regressions | No new transaction-architecture debt | +| `add-contract-output-type` | Explicit N/A check for this feature (no new injector contract output type) | Recorded N/A confirmation | + +## 5) Validation matrix (must pass before merge) + +### 5.1 Backend + +- Build compiles with new model/API classes. +- CRUD integration tests pass. +- Tenant-v2 isolation tests pass. +- TxCtx entrypoint arch test passes. +- Migration applies in dev profile without checksum/rerun issues. + +### 5.2 Frontend + +- Typecheck + lint pass on changed files. +- Component tests pass for list, forms, permissions, and feature flag behavior. +- Menu masking verified with `MARKING` disabled. + +### 5.3 Review gates + +- `review-code` + `review-security` + `review-performance` + `review-multi-tenancy` + `review-migration` + `review-frontend` + `review-docs` completed. +- `review-chaining-engine` recorded as N/A (no touched chaining packages/files). + +## 6) Detailed Definition of Done + +- RBAC MARKING chains are available and correctly inherited. +- Feature flag `MARKING` masks frontend menu entry when disabled. +- Marking Definitions CRUD works with pagination/search/filter/sort. +- Backend enforces `order >= 0`, immutable type, and protected-row guards. +- Table is tenant-v2-active with complete `TxCtx` coverage. +- Default TLP rows are seeded idempotently and protected. +- Deferred items are tracked as follow-up issues, not silently omitted. +- All quality and review gates are green. + +## 7) Rollout plan + +- Merge with `MARKING` off by default (dark rollout). +- Validate in controlled environment/tenant(s). +- Enable progressively after review-gate sign-off and smoke checks. +- Release note: Marking Definitions foundation (RBAC + CRUD + protected default TLP seeds). + +## 8) Traceability to user stories + +- US1 covered by Workstream B. +- US2 covered by Workstreams B, D, E. +- US3 covered by Workstreams D, E. +- US4 covered by Workstreams D, E. +- US5 covered by Workstreams D, E (with explicit deferred in-use/unlink TODO boundaries). +- US6 covered by Workstream F. + diff --git a/adr/ADR-007-implementation-task-board.md b/adr/ADR-007-implementation-task-board.md new file mode 100644 index 00000000000..6b55e7a7be9 --- /dev/null +++ b/adr/ADR-007-implementation-task-board.md @@ -0,0 +1,179 @@ +# ADR-007 Task Board - Marking Definitions (Single Chunk) + +This checklist is derived from `adr/ADR-007-implementation-plan.md` and is ready to use in issue/PR tracking. + +## 0. Scope lock (before coding) + +- [ ] Confirm Task 1 keeps naming as `MarkingDefinition` (entity/API/DTOs). +- [ ] Confirm deferred items remain out of scope: + - [ ] in-use delete behavior + - [ ] unlink propagation to Assets/Users + - [ ] import/export behavior + - [ ] full assignment UX for Groups/Assets +- [ ] Confirm feature flag is `MARKING` and default is OFF. +- [ ] Add PR scope note listing impacted layers (RBAC, model, migration, API, frontend, seeds, tests). + +## 1. RBAC + feature flag + +### Backend + +- [ ] Add capabilities: + - [ ] `ACCESS_MARKING_DEFINITION` + - [ ] `MANAGE_MARKING_DEFINITION` + - [ ] `DELETE_MARKING_DEFINITION` + - [ ] `ACCESS_MARKING_ASSIGNMENT` + - [ ] `ASSIGN_MARKING` + - [ ] `DELETE_MARKING_ASSIGNMENT` +- [ ] Wire parent hierarchy for both chains. +- [ ] Ensure BYPASS behavior remains effective. +- [ ] Expose MARKING group in capability tree API. +- [ ] Add/confirm backend feature-flag plumbing for `MARKING` (default off). + +### Frontend + +- [ ] Add capability parsing/mapping for new MARKING capabilities. +- [ ] Render MARKING group in role editor UI. +- [ ] Gate marking-related actions by capabilities. +- [ ] Hide Marking Definitions menu entry when `MARKING` is disabled. +- [ ] Keep direct route access protected by existing route guards + backend RBAC. + +### Tests + +- [ ] Capability cascade tests. +- [ ] Capability independence tests. +- [ ] BYPASS tests. +- [ ] Front menu masking test with `MARKING=false`. + +## 2. Data model + migration + tenant-v2 activation + +### Model + +- [ ] Add `MarkingDefinition` entity fields: + - [ ] `marking_definition_id` + - [ ] `marking_definition_type` + - [ ] `marking_definition_definition` + - [ ] `marking_definition_color` + - [ ] `marking_definition_order` + - [ ] `marking_definition_protected` + - [ ] `tenant_id` + - [ ] audit timestamps + +### Migration (`add-migration` skill) + +- [ ] Create new Java Flyway migration (do not edit existing migrations). +- [ ] Add table `marking_definitions` with idempotent guards. +- [ ] Add unique constraint: `(marking_definition_type, marking_definition_definition, tenant_id)`. +- [ ] Add non-unique index/constraint support for order: `(marking_definition_type, marking_definition_order, tenant_id)`. +- [ ] Add index on `tenant_id`. +- [ ] Add supporting indexes for `type`, `definition`, `order`, `created_at`. +- [ ] Activate `marking_definitions` in `openaev.tenant.active-tables` in same rollout commit. + +### Tenant v2 (`activate-tenant-table` skill) + +- [ ] Add `TxCtx` on every transactional entrypoint that reaches marking definitions. +- [ ] Update `TenantScopedEntrypointsTxCtxArchTest` with all required entrypoints. +- [ ] Validate no v1 `@Filter` is used for this v2-active table. + +## 3. Backend CRUD API + +### API surface + +- [ ] Implement `POST /api/{tenant}/marking_definitions/search` (paginated). +- [ ] Implement `POST /api/{tenant}/marking_definitions`. +- [ ] Implement `PUT /api/{tenant}/marking_definitions/{id}`. +- [ ] Implement `DELETE /api/{tenant}/marking_definitions/{id}`. + +### Business rules + +- [ ] Enforce required fields: `type`, `definition`, `order`. +- [ ] Enforce `order >= 0`. +- [ ] Enforce immutable `type` on update. +- [ ] Enforce protected-row guards (`protected=true` blocks update/delete). +- [ ] Keep deferred TODO boundaries explicit: + - [ ] no in-use warning flow in Task 1 + - [ ] no unlink propagation in Task 1 + +### Backend tests (`add-test` skill) + +- [ ] CRUD happy-path integration tests. +- [ ] Validation tests (`order >= 0`, required fields). +- [ ] Type immutability tests. +- [ ] Protected-row update/delete rejection tests. +- [ ] Search/filter/sort tests for Type/Definition/Color/Order/Creation date. +- [ ] Tenant isolation tests (read/write). + +## 4. Frontend page and forms + +### UI implementation + +- [ ] Add Marking Definitions page under Security. +- [ ] Use classic pagination front list component pattern. +- [ ] Render columns: Type, Definition, Color, Order, Creation date. +- [ ] Implement search/filter/sort on the same fields. +- [ ] Implement create/edit dialogs: + - [ ] required validation + - [ ] non-negative numeric order + - [ ] type editable on create only + - [ ] order-change warning confirmation on edit +- [ ] Implement delete confirmation. +- [ ] Hide/disable edit/delete for protected rows. +- [ ] Refresh Marking Definitions list/store after create/update/delete. + +### Frontend tests (`add-test` skill) + +- [ ] Visibility tests by permission + feature flag. +- [ ] Validation tests for form and `order >= 0`. +- [ ] List behavior tests for pagination/filter/sort/search. +- [ ] Protected-row action visibility tests. +- [ ] Success refresh tests after create/update/delete. + +## 5. Default TLP seed lifecycle + +- [ ] Seed defaults in tenant datapack (idempotent): + - [ ] `TLP:CLEAR (1)` + - [ ] `TLP:GREEN (2)` + - [ ] `TLP:AMBER (3)` + - [ ] `TLP:AMBER+STRICT (4)` + - [ ] `TLP:RED (5)` +- [ ] Set `protected=true` for all seeded rows. +- [ ] Add seed idempotency tests. +- [ ] Add tests that seeded rows are immutable. +- [ ] Add coexistence test with custom types. + +## 6. Skills-driven review gates (must run) + +- [ ] `review-code` completed. +- [ ] `review-security` completed. +- [ ] `review-performance` completed. +- [ ] `review-multi-tenancy` completed. +- [ ] `review-migration` completed. +- [ ] `review-frontend` completed. +- [ ] `review-docs` completed. +- [ ] `review-chaining-engine` recorded as N/A (no touched chaining files). +- [ ] `reduce-tx-baseline` checked (apply only if new baseline debt appears). +- [ ] `add-contract-output-type` recorded as N/A for this feature. +- [ ] `create-feature-module` used as structure checklist to ensure complete cross-layer delivery. + +## 7. Final verification before merge + +- [ ] Backend formatting checks pass. +- [ ] Backend compile/tests pass. +- [ ] Frontend lint/type-check/tests pass. +- [ ] Tenant-v2 TxCtx arch checks pass. +- [ ] Migration applies cleanly. +- [ ] Feature is dark-merge safe (`MARKING` remains OFF by default). + +## 8. Rollout + +- [ ] Merge with `MARKING` disabled. +- [ ] Validate in controlled tenant(s). +- [ ] Progressive enablement after validation gates. +- [ ] Publish release note for Marking Definitions foundation. + +## 9. Follow-up issues (deferred) + +- [ ] In-use delete policy decision and implementation. +- [ ] Unlink side-effects + Assets/Users refresh. +- [ ] Import/export behavior for markings. +- [ ] Assignment UX completion for Groups/Assets. + diff --git a/adr/ADR-007-marking-definitions-create-manage-plan.md b/adr/ADR-007-marking-definitions-create-manage-plan.md index 19d31abf9dd..c49d06a849b 100644 --- a/adr/ADR-007-marking-definitions-create-manage-plan.md +++ b/adr/ADR-007-marking-definitions-create-manage-plan.md @@ -266,10 +266,10 @@ Tests: Base path (tenant API style): -- `POST /api/{tenant}/marking-definitions/search` -> `Page` -- `POST /api/{tenant}/marking-definitions` -> create -- `PUT /api/{tenant}/marking-definitions/{id}` -> update -- `DELETE /api/{tenant}/marking-definitions/{id}` -> delete +- `POST /api/{tenant}/marking_definitions/search` -> `Page` +- `POST /api/{tenant}/marking_definitions` -> create +- `PUT /api/{tenant}/marking_definitions/{id}` -> update +- `DELETE /api/{tenant}/marking_definitions/{id}` -> delete Output fields: diff --git a/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java new file mode 100644 index 00000000000..1f0c0445223 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java @@ -0,0 +1,110 @@ +package io.openaev.api.marking_definition; + +import static io.openaev.config.TenantUriUtils.TENANT_PREFIX; + +import io.openaev.aop.AccessControl; +import io.openaev.aop.LogExecutionTime; +import io.openaev.api.marking_definition.form.MarkingDefinitionInput; +import io.openaev.api.marking_definition.form.MarkingDefinitionOutput; +import io.openaev.config.TenantWriteScopeResolver; +import io.openaev.context.TxCtx; +import io.openaev.database.model.Action; +import io.openaev.database.model.MarkingDefinition; +import io.openaev.database.model.ResourceType; +import io.openaev.rest.helper.RestBehavior; +import io.openaev.service.marking_definition.MarkingDefinitionService; +import io.openaev.utils.pagination.SearchPaginationInput; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping({MarkingDefinitionApi.TENANT_MARKING_DEFINITIONS_URI}) +@Tag(name = "Marking definition API", description = "Operations related to marking definitions") +public class MarkingDefinitionApi extends RestBehavior { + + public static final String TENANT_MARKING_DEFINITIONS_URI = + TENANT_PREFIX + "/marking_definitions"; + + private final MarkingDefinitionService service; + private final TenantWriteScopeResolver writeScopeResolver; + + // -- SEARCH -- + + @LogExecutionTime + @GetMapping + @Transactional(readOnly = true) + @AccessControl(actionPerformed = Action.SEARCH, resourceType = ResourceType.MARKING_DEFINITION) + @Operation(summary = "Get marking definitions", description = "Get the list of marking definitions") + public List list(TxCtx ctx) { + return service.list(ctx).stream().map(MarkingDefinitionMapper::toOutput).toList(); + } + + @LogExecutionTime + @PostMapping("/search") + @Transactional(readOnly = true) + @AccessControl(actionPerformed = Action.SEARCH, resourceType = ResourceType.MARKING_DEFINITION) + @Operation(summary = "Search marking definitions") + public Page search( + TxCtx ctx, @RequestBody @Valid SearchPaginationInput searchPaginationInput) { + return service.search(ctx, searchPaginationInput).map(MarkingDefinitionMapper::toOutput); + } + + // -- CREATE -- + + @PostMapping + @Transactional + @AccessControl(actionPerformed = Action.WRITE, resourceType = ResourceType.MARKING_DEFINITION) + @Operation(summary = "Create a marking definition") + public MarkingDefinitionOutput create( + TxCtx ctx, @Valid @RequestBody MarkingDefinitionInput input) { + String tenantId = writeScopeResolver.tenantForWrite(ctx, null); + MarkingDefinition created = service.create(input, tenantId); + return MarkingDefinitionMapper.toOutput(created); + } + + // -- UPDATE -- + + @PutMapping("/{markingDefinitionId}") + @Transactional + @AccessControl( + resourceId = "#markingDefinitionId", + actionPerformed = Action.WRITE, + resourceType = ResourceType.MARKING_DEFINITION) + @Operation(summary = "Update a marking definition") + public MarkingDefinitionOutput update( + TxCtx ctx, + @PathVariable String markingDefinitionId, + @Valid @RequestBody MarkingDefinitionInput input) { + return MarkingDefinitionMapper.toOutput(service.update(ctx, markingDefinitionId, input)); + } + + // -- DELETE -- + + @DeleteMapping("/{markingDefinitionId}") + @Transactional + @ResponseStatus(HttpStatus.NO_CONTENT) + @AccessControl( + resourceId = "#markingDefinitionId", + actionPerformed = Action.DELETE, + resourceType = ResourceType.MARKING_DEFINITION) + @Operation(summary = "Delete a marking definition") + public void delete(TxCtx ctx, @PathVariable String markingDefinitionId) { + service.delete(ctx, markingDefinitionId); + } +} diff --git a/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionMapper.java b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionMapper.java new file mode 100644 index 00000000000..df5254887fa --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionMapper.java @@ -0,0 +1,30 @@ +package io.openaev.api.marking_definition; + +import io.openaev.api.marking_definition.form.MarkingDefinitionInput; +import io.openaev.api.marking_definition.form.MarkingDefinitionOutput; +import io.openaev.database.model.MarkingDefinition; + +public final class MarkingDefinitionMapper { + + private MarkingDefinitionMapper() {} + + public static MarkingDefinitionOutput toOutput(MarkingDefinition entity) { + return new MarkingDefinitionOutput( + entity.getId(), + entity.getType(), + entity.getDefinition(), + entity.getColor(), + entity.getOrder(), + entity.getProtectedDefinition(), + entity.getCreatedAt()); + } + + public static MarkingDefinition fromInput(MarkingDefinitionInput input) { + MarkingDefinition entity = new MarkingDefinition(); + entity.setType(input.type()); + entity.setDefinition(input.definition()); + entity.setColor(input.color()); + entity.setOrder(input.order()); + return entity; + } +} diff --git a/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionInput.java b/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionInput.java new file mode 100644 index 00000000000..9ae7741994c --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionInput.java @@ -0,0 +1,12 @@ +package io.openaev.api.marking_definition.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record MarkingDefinitionInput( + @JsonProperty("marking_definition_type") @NotBlank String type, + @JsonProperty("marking_definition_definition") @NotBlank String definition, + @JsonProperty("marking_definition_color") String color, + @JsonProperty("marking_definition_order") @NotNull @Min(0) Integer order) {} diff --git a/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionOutput.java b/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionOutput.java new file mode 100644 index 00000000000..319b9753dff --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/api/marking_definition/form/MarkingDefinitionOutput.java @@ -0,0 +1,15 @@ +package io.openaev.api.marking_definition.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; + +public record MarkingDefinitionOutput( + @JsonProperty("marking_definition_id") @NotBlank String id, + @JsonProperty("marking_definition_type") @NotBlank String type, + @JsonProperty("marking_definition_definition") @NotBlank String definition, + @JsonProperty("marking_definition_color") String color, + @JsonProperty("marking_definition_order") @NotNull Integer order, + @JsonProperty("marking_definition_protected") @NotNull Boolean protectedDefinition, + @JsonProperty("marking_definition_created_at") @NotNull Instant createdAt) {} diff --git a/openaev-api/src/main/java/io/openaev/migration/V6_20260826120000000__Add_marking_definitions.java b/openaev-api/src/main/java/io/openaev/migration/V6_20260826120000000__Add_marking_definitions.java new file mode 100644 index 00000000000..7109a1e00e7 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/migration/V6_20260826120000000__Add_marking_definitions.java @@ -0,0 +1,80 @@ +package io.openaev.migration; + +import java.sql.Statement; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +@Component +public class V6_20260826120000000__Add_marking_definitions extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.execute( + """ + CREATE TABLE IF NOT EXISTS marking_definitions ( + marking_definition_id VARCHAR(255) NOT NULL CONSTRAINT marking_definitions_pkey PRIMARY KEY, + marking_definition_type VARCHAR(255) NOT NULL, + marking_definition_definition VARCHAR(255) NOT NULL, + marking_definition_color VARCHAR(255), + marking_definition_order INTEGER NOT NULL DEFAULT 0, + marking_definition_protected BOOLEAN NOT NULL DEFAULT false, + marking_definition_created_at TIMESTAMP NOT NULL DEFAULT now(), + marking_definition_updated_at TIMESTAMP NOT NULL DEFAULT now(), + tenant_id VARCHAR(255) NOT NULL CONSTRAINT marking_definitions_tenant_fk REFERENCES tenants (tenant_id) ON DELETE CASCADE + ) + """); + + statement.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_marking_definitions_type_definition_tenant_uq ON marking_definitions (marking_definition_type, marking_definition_definition, tenant_id)"); + statement.execute( + "CREATE INDEX IF NOT EXISTS idx_marking_definitions_type_order_tenant ON marking_definitions (marking_definition_type, marking_definition_order, tenant_id)"); + statement.execute( + "CREATE INDEX IF NOT EXISTS idx_marking_definitions_tenant ON marking_definitions (tenant_id)"); + statement.execute( + "CREATE INDEX IF NOT EXISTS idx_marking_definitions_created_at ON marking_definitions (marking_definition_created_at)"); + + // Observer gets access to marking definitions. + statement.execute( + """ + INSERT INTO roles_capabilities (role_id, capability) + SELECT r.role_id, 'ACCESS_MARKING_DEFINITION' + FROM roles r + WHERE r.role_name = 'Observer' + AND r.tenant_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM roles_capabilities rc + WHERE rc.role_id = r.role_id + AND rc.capability = 'ACCESS_MARKING_DEFINITION' + ) + """); + + // Manager gets full marking definition and assignment capabilities. + statement.execute( + """ + INSERT INTO roles_capabilities (role_id, capability) + SELECT r.role_id, c.capability + FROM roles r + JOIN ( + VALUES + ('ACCESS_MARKING_DEFINITION'), + ('MANAGE_MARKING_DEFINITION'), + ('DELETE_MARKING_DEFINITION'), + ('ACCESS_MARKING_ASSIGNMENT'), + ('ASSIGN_MARKING'), + ('DELETE_MARKING_ASSIGNMENT') + ) AS c(capability) ON true + WHERE r.role_name = 'Manager' + AND r.tenant_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM roles_capabilities rc + WHERE rc.role_id = r.role_id + AND rc.capability = c.capability + ) + """); + } + } +} diff --git a/openaev-api/src/main/java/io/openaev/processor/datapack/PresetTenantData.java b/openaev-api/src/main/java/io/openaev/processor/datapack/PresetTenantData.java index 33e6e50b7db..618ad6cf312 100644 --- a/openaev-api/src/main/java/io/openaev/processor/datapack/PresetTenantData.java +++ b/openaev-api/src/main/java/io/openaev/processor/datapack/PresetTenantData.java @@ -17,6 +17,17 @@ public class PresetTenantData { public record VulnerabilityCwe(Vulnerability vulnerability, Cwe cwe) {} + public record MarkingSeed(String type, String definition, String color, int order) {} + + public static List createDefaultMarkings() { + return List.of( + new MarkingSeed("TLP", "TLP:CLEAR", "#E6E7E8", 1), + new MarkingSeed("TLP", "TLP:GREEN", "#4CAF50", 2), + new MarkingSeed("TLP", "TLP:AMBER", "#FFB300", 3), + new MarkingSeed("TLP", "TLP:AMBER+STRICT", "#FF8F00", 4), + new MarkingSeed("TLP", "TLP:RED", "#E53935", 5)); + } + /** * Creates fresh {@link VulnerabilityCwe} instances for each call. Must not be a static field * because JPA-managed entities retain persistence state (version, managed status) after the first @@ -182,6 +193,7 @@ public static List createDefaultVulnerabilityCwes() { Capability.ACCESS_ASSESSMENT, Capability.ACCESS_ASSETS, Capability.ACCESS_CREDENTIALS, + Capability.ACCESS_MARKING_DEFINITION, Capability.ACCESS_THREAT_ARSENALS, Capability.ACCESS_DASHBOARDS, Capability.ACCESS_REPORTINGS, @@ -205,6 +217,12 @@ public static List createDefaultVulnerabilityCwes() { Capability.DELETE_ASSETS, Capability.MANAGE_CREDENTIALS, Capability.DELETE_CREDENTIALS, + Capability.ACCESS_MARKING_DEFINITION, + Capability.MANAGE_MARKING_DEFINITION, + Capability.DELETE_MARKING_DEFINITION, + Capability.ACCESS_MARKING_ASSIGNMENT, + Capability.ASSIGN_MARKING, + Capability.DELETE_MARKING_ASSIGNMENT, Capability.ACCESS_THREAT_ARSENALS, Capability.MANAGE_THREAT_ARSENALS, Capability.DELETE_THREAT_ARSENALS, diff --git a/openaev-api/src/main/java/io/openaev/processor/datapack/V20260826_Default_tenant_markings.java b/openaev-api/src/main/java/io/openaev/processor/datapack/V20260826_Default_tenant_markings.java new file mode 100644 index 00000000000..0c2693338cd --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/processor/datapack/V20260826_Default_tenant_markings.java @@ -0,0 +1,48 @@ +package io.openaev.processor.datapack; + +import io.openaev.context.TenantContext; +import io.openaev.database.model.MarkingDefinition; +import io.openaev.database.model.Tenant; +import io.openaev.database.repository.MarkingDefinitionRepository; +import io.openaev.service.DataPackService; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class V20260826_Default_tenant_markings extends DataPack { + + private final MarkingDefinitionRepository markingDefinitionRepository; + @PersistenceContext private EntityManager entityManager; + + public V20260826_Default_tenant_markings( + DataPackService dataPackService, MarkingDefinitionRepository markingDefinitionRepository) { + super(dataPackService); + this.markingDefinitionRepository = markingDefinitionRepository; + } + + @Override + public boolean doProcess() { + try { + PresetTenantData.createDefaultMarkings() + .forEach( + seed -> { + MarkingDefinition markingDefinition = new MarkingDefinition(); + markingDefinition.setType(seed.type()); + markingDefinition.setDefinition(seed.definition()); + markingDefinition.setColor(seed.color()); + markingDefinition.setOrder(seed.order()); + markingDefinition.setProtectedDefinition(true); + markingDefinition.setTenant( + entityManager.getReference(Tenant.class, TenantContext.getCurrentTenant())); + markingDefinitionRepository.save(markingDefinition); + }); + return true; + } catch (Exception e) { + log.error("Unexpected error during DataPack 20260826 initialization.", e); + return false; + } + } +} diff --git a/openaev-api/src/main/java/io/openaev/rest/settings/PreviewFeature.java b/openaev-api/src/main/java/io/openaev/rest/settings/PreviewFeature.java index 8d20298c762..5edebb57471 100644 --- a/openaev-api/src/main/java/io/openaev/rest/settings/PreviewFeature.java +++ b/openaev-api/src/main/java/io/openaev/rest/settings/PreviewFeature.java @@ -20,7 +20,8 @@ public enum PreviewFeature { TENANT_FIELDS_FOR_SECURITY_COVERAGE("TENANT_FIELDS_FOR_SECURITY_COVERAGE"), LEGACY_INGESTION_EXECUTION_TRACE("LEGACY_INGESTION_EXECUTION_TRACE"), OPENAEV_TRIALS_XTMHUB("OPENAEV_TRIALS_XTMHUB"), - CREDENTIAL_ASSET("CREDENTIAL_ASSET"); + CREDENTIAL_ASSET("CREDENTIAL_ASSET"), + MARKING("MARKING"); private final String value; diff --git a/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java b/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java new file mode 100644 index 00000000000..a786f4e9686 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java @@ -0,0 +1,170 @@ +package io.openaev.service.marking_definition; + +import static io.openaev.utils.pagination.PaginationUtils.buildPaginationJPA; + +import io.openaev.api.marking_definition.MarkingDefinitionMapper; +import io.openaev.api.marking_definition.form.MarkingDefinitionInput; +import io.openaev.context.TxCtx; +import io.openaev.database.model.MarkingDefinition; +import io.openaev.database.model.Tenant; +import io.openaev.database.repository.MarkingDefinitionRepository; +import io.openaev.rest.exception.BadRequestException; +import io.openaev.rest.exception.ElementNotFoundException; +import io.openaev.utils.TxCtxScopeUtils; +import io.openaev.utils.pagination.SearchPaginationInput; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.Objects; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(rollbackFor = Exception.class) +public class MarkingDefinitionService { + + private final MarkingDefinitionRepository repository; + + // -- SEARCH -- + + /** + * Searches marking definitions within the current tenant scope. + * + * @param ctx transaction context containing tenant scope + * @param searchPaginationInput pagination and filter criteria + * @return page of matching marking definitions + */ + @Transactional(readOnly = true) + public Page search( + @NotNull TxCtx ctx, @NotNull SearchPaginationInput searchPaginationInput) { + Set tenantIds = TxCtxScopeUtils.tenantIdsFromHTTPCtx(ctx); + return buildPaginationJPA( + (specification, pageable) -> findAllByTenantIds(tenantIds, specification, pageable), + searchPaginationInput, + MarkingDefinition.class); + } + + // -- READ -- + + /** + * Lists all marking definitions visible in the current tenant scope. + * + * @param ctx transaction context containing tenant scope + * @return marking definitions visible to the caller + */ + @Transactional(readOnly = true) + public List list(@NotNull TxCtx ctx) { + Set tenantIds = TxCtxScopeUtils.tenantIdsFromHTTPCtx(ctx); + if (tenantIds.isEmpty()) { + return List.of(); + } + return repository.findAll(tenantSpecification(tenantIds), Sort.by("order").ascending()); + } + + private MarkingDefinition findByIdOrThrow( + @NotNull TxCtx ctx, @NotBlank String markingDefinitionId) { + MarkingDefinition markingDefinition = + repository + .findById(markingDefinitionId) + .orElseThrow(() -> new ElementNotFoundException("Marking definition not found")); + Set tenantIds = TxCtxScopeUtils.tenantIdsFromHTTPCtx(ctx); + if (!tenantIds.contains(markingDefinition.getTenant().getId())) { + throw new ElementNotFoundException("Marking definition not found"); + } + return markingDefinition; + } + + // -- CREATE -- + + /** + * Creates a marking definition for a tenant after duplicate checks. + * + * @param input create payload + * @param tenantId tenant that owns the new row + * @return persisted marking definition + */ + public MarkingDefinition create( + @NotNull MarkingDefinitionInput input, @NotBlank String tenantId) { + validateUniqueOrThrow(input.type(), input.definition(), tenantId, null); + MarkingDefinition entity = MarkingDefinitionMapper.fromInput(input); + entity.setProtectedDefinition(false); + entity.setTenant(new Tenant(tenantId)); + return repository.save(entity); + } + + // -- UPDATE -- + + /** + * Updates mutable fields of a marking definition while preserving immutable type and protection. + * + * @param ctx transaction context containing tenant scope + * @param markingDefinitionId identifier of the marking definition + * @param input update payload + * @return updated marking definition + */ + public MarkingDefinition update( + @NotNull TxCtx ctx, + @NotBlank String markingDefinitionId, + @NotNull MarkingDefinitionInput input) { + MarkingDefinition existing = findByIdOrThrow(ctx, markingDefinitionId); + if (Boolean.TRUE.equals(existing.getProtectedDefinition())) { + throw new BadRequestException("Protected marking definitions cannot be updated"); + } + if (!Objects.equals(existing.getType(), input.type())) { + throw new BadRequestException("Marking definition type is immutable"); + } + validateUniqueOrThrow( + input.type(), input.definition(), existing.getTenant().getId(), existing.getId()); + existing.setDefinition(input.definition()); + existing.setColor(input.color()); + existing.setOrder(input.order()); + return repository.save(existing); + } + + // -- DELETE -- + + /** + * Deletes a marking definition when it is not protected. + * + * @param ctx transaction context containing tenant scope + * @param markingDefinitionId identifier of the marking definition + */ + public void delete(@NotNull TxCtx ctx, @NotBlank String markingDefinitionId) { + MarkingDefinition existing = findByIdOrThrow(ctx, markingDefinitionId); + if (Boolean.TRUE.equals(existing.getProtectedDefinition())) { + throw new BadRequestException("Protected marking definitions cannot be deleted"); + } + repository.delete(existing); + } + + private void validateUniqueOrThrow( + String type, String definition, String tenantId, String ignoredId) { + boolean duplicateExists = + repository.existsByTypeAndDefinitionAndTenantIdExcludingId( + type, definition, tenantId, ignoredId); + if (duplicateExists) { + throw new BadRequestException( + "A marking definition with the same type and definition already exists"); + } + } + + private Page findAllByTenantIds( + Set tenantIds, + Specification specification, + org.springframework.data.domain.Pageable pageable) { + if (tenantIds.isEmpty()) { + return Page.empty(pageable); + } + return repository.findAll(tenantSpecification(tenantIds).and(specification), pageable); + } + + private Specification tenantSpecification(Set tenantIds) { + return (root, query, criteriaBuilder) -> root.get("tenant").get("id").in(tenantIds); + } +} diff --git a/openaev-api/src/main/resources/application.properties b/openaev-api/src/main/resources/application.properties index 38dc4426a34..0069b329986 100644 --- a/openaev-api/src/main/resources/application.properties +++ b/openaev-api/src/main/resources/application.properties @@ -619,7 +619,7 @@ openaev.enabled-dev-features= # table, filtered through the finding it is joined to). # autonomous_runs / autonomous_events / autonomous_directives are v2-native (TenantBase, no # @Filter; TenantBaseListener removed on activation) so they MUST stay here (#7396). -openaev.tenant.active-tables=import_mappers,lessons_templates,mitigations,cwes,collectors,executors,injectors,attackpath_execution,attackpath_finding,secret_references,secrets,connector_instances,autonomous_runs,autonomous_events,autonomous_directives,security_coverages +openaev.tenant.active-tables=import_mappers,lessons_templates,mitigations,cwes,collectors,executors,injectors,attackpath_execution,attackpath_finding,secret_references,secrets,connector_instances,autonomous_runs,autonomous_events,autonomous_directives,security_coverages,marking_definitions ############################# # Attack path diff --git a/openaev-api/src/test/java/io/openaev/api/marking_definition/MarkingDefinitionApiTest.java b/openaev-api/src/test/java/io/openaev/api/marking_definition/MarkingDefinitionApiTest.java new file mode 100644 index 00000000000..c79d1fba35f --- /dev/null +++ b/openaev-api/src/test/java/io/openaev/api/marking_definition/MarkingDefinitionApiTest.java @@ -0,0 +1,440 @@ +package io.openaev.api.marking_definition; + +import static io.openaev.utils.JsonTestUtils.asJsonString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.jayway.jsonpath.JsonPath; +import io.openaev.IntegrationTest; +import io.openaev.database.model.Capability; +import io.openaev.database.model.Filters; +import io.openaev.database.model.MarkingDefinition; +import io.openaev.database.model.Tenant; +import io.openaev.database.repository.MarkingDefinitionRepository; +import io.openaev.utils.TenantIsolationTestHelper; +import io.openaev.utils.mockUser.WithMockUser; +import io.openaev.utils.pagination.SearchPaginationInput; +import io.openaev.utils.pagination.SortField; +import jakarta.persistence.EntityManager; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional +@DisplayName("Marking definition API") +class MarkingDefinitionApiTest extends IntegrationTest { + + private static final String URI = "/api/tenants/{tenantId}/marking_definitions"; + + @Autowired private MockMvc mvc; + @Autowired private MarkingDefinitionRepository repository; + @Autowired private EntityManager entityManager; + @Autowired private TenantIsolationTestHelper tenantIsolationTestHelper; + + @Nested + @WithMockUser( + withCapabilities = { + Capability.MANAGE_MARKING_DEFINITION, + Capability.DELETE_MARKING_DEFINITION, + Capability.ACCESS_MARKING_DEFINITION + }) + @DisplayName("CRUD operations") + class CrudOperations { + + @Test + @DisplayName("given_validInput_should_createMarkingDefinition") + void given_validInput_should_createMarkingDefinition() throws Exception { + // Arrange + String body = + """ + { + "marking_definition_type": "TLP", + "marking_definition_definition": "TLP:BLUE", + "marking_definition_color": "#2196F3", + "marking_definition_order": 6 + } + """; + + // Act & Assert + mvc.perform( + post(URI, Tenant.DEFAULT_TENANT_UUID) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.marking_definition_type").value("TLP")) + .andExpect(jsonPath("$.marking_definition_definition").value("TLP:BLUE")) + .andExpect(jsonPath("$.marking_definition_order").value(6)) + .andExpect(jsonPath("$.marking_definition_protected").value(false)); + } + + @Test + @DisplayName("given_negativeOrder_should_rejectCreation") + void given_negativeOrder_should_rejectCreation() throws Exception { + // Arrange + String body = + """ + { + "marking_definition_type": "TLP", + "marking_definition_definition": "TLP:INVALID", + "marking_definition_color": "#2196F3", + "marking_definition_order": -1 + } + """; + + // Act & Assert + mvc.perform( + post(URI, Tenant.DEFAULT_TENANT_UUID) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().is4xxClientError()); + } + + @Test + @DisplayName("given_protectedDefinition_should_notDelete") + void given_protectedDefinition_should_notDelete() throws Exception { + // Arrange + MarkingDefinition protectedDefinition = new MarkingDefinition(); + protectedDefinition.setType("TLP"); + protectedDefinition.setDefinition("TLP:RED"); + protectedDefinition.setColor("#E53935"); + protectedDefinition.setOrder(5); + protectedDefinition.setProtectedDefinition(true); + protectedDefinition.setTenant( + entityManager.getReference(Tenant.class, Tenant.DEFAULT_TENANT_UUID)); + MarkingDefinition saved = repository.save(protectedDefinition); + + // Act & Assert + mvc.perform(delete(URI + "/{id}", Tenant.DEFAULT_TENANT_UUID, saved.getId())) + .andExpect(status().is4xxClientError()); + } + } + + @Nested + @WithMockUser( + withCapabilities = { + Capability.MANAGE_MARKING_DEFINITION, + Capability.ACCESS_MARKING_DEFINITION + }) + @DisplayName("Tenant isolation") + class TenantIsolation { + + @Test + @DisplayName("given_twoTenantRows_should_onlyListRowsFromRequestedTenant") + void given_twoTenantRows_should_onlyListRowsFromRequestedTenant() throws Exception { + // Arrange + Tenant tenantA = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-tenant-a"); + Tenant tenantB = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-tenant-b"); + + MarkingDefinition tenantARow = + createPersistedMarkingDefinition( + tenantA.getId(), + "TLP", + "TENANT-A-ONLY", + "#0066CC", + 10, + Instant.parse("2026-01-01T10:00:00Z")); + MarkingDefinition tenantBRow = + createPersistedMarkingDefinition( + tenantB.getId(), + "TLP", + "TENANT-B-ONLY", + "#CC6600", + 20, + Instant.parse("2026-01-01T11:00:00Z")); + + SearchPaginationInput input = new SearchPaginationInput(); + + // Act + String responseA = + mvc.perform( + post(URI + "/search", tenantA.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + String responseB = + mvc.perform( + post(URI + "/search", tenantB.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + List idsA = JsonPath.read(responseA, "$.content[*].marking_definition_id"); + List idsB = JsonPath.read(responseB, "$.content[*].marking_definition_id"); + + assertThat(idsA).contains(tenantARow.getId()).doesNotContain(tenantBRow.getId()); + assertThat(idsB).contains(tenantBRow.getId()).doesNotContain(tenantARow.getId()); + } + + @Test + @DisplayName("given_crossTenantUpdate_should_notUpdateRow") + void given_crossTenantUpdate_should_notUpdateRow() throws Exception { + // Arrange + Tenant tenantA = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-update-a"); + Tenant tenantB = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-update-b"); + + MarkingDefinition tenantARow = + createPersistedMarkingDefinition( + tenantA.getId(), + "TLP", + "ORIGINAL-A", + "#123456", + 3, + Instant.parse("2026-02-01T10:00:00Z")); + + String updateBody = + """ + { + "marking_definition_type": "TLP", + "marking_definition_definition": "MUTATED-B", + "marking_definition_color": "#FFFFFF", + "marking_definition_order": 99 + } + """; + + // Act + mvc.perform( + put(URI + "/{id}", tenantB.getId(), tenantARow.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(updateBody)) + .andExpect(status().is4xxClientError()); + + // Assert + MarkingDefinition reloaded = repository.findById(tenantARow.getId()).orElseThrow(); + assertThat(reloaded.getDefinition()).isEqualTo("ORIGINAL-A"); + assertThat(reloaded.getColor()).isEqualTo("#123456"); + assertThat(reloaded.getOrder()).isEqualTo(3); + } + } + + @Nested + @WithMockUser(withCapabilities = Capability.ACCESS_MARKING_DEFINITION) + @DisplayName("Search, filter and sort") + class SearchFilterSort { + + @Test + @DisplayName("given_typeAndColorFilters_should_returnMatchingRowsOnly") + void given_typeAndColorFilters_should_returnMatchingRowsOnly() throws Exception { + // Arrange + Tenant tenant = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-filter"); + + MarkingDefinition matching = + createPersistedMarkingDefinition( + tenant.getId(), "TLP", "MATCH", "#00AA00", 1, Instant.parse("2026-03-01T10:00:00Z")); + MarkingDefinition wrongColor = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "WRONG-COLOR", + "#AA0000", + 2, + Instant.parse("2026-03-01T11:00:00Z")); + MarkingDefinition wrongType = + createPersistedMarkingDefinition( + tenant.getId(), + "PAP", + "WRONG-TYPE", + "#00AA00", + 3, + Instant.parse("2026-03-01T12:00:00Z")); + + SearchPaginationInput input = new SearchPaginationInput(); + input.setFilterGroup( + Filters.FilterGroup.filterGroupWithFilters( + List.of( + Filters.Filter.getNewDefaultEqualFilter( + "marking_definition_type", List.of("TLP")), + Filters.Filter.getNewDefaultEqualFilter( + "marking_definition_color", List.of("#00AA00"))))); + + // Act + String response = + mvc.perform( + post(URI + "/search", tenant.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + List ids = JsonPath.read(response, "$.content[*].marking_definition_id"); + assertThat(ids).containsExactly(matching.getId()); + assertThat(ids).doesNotContain(wrongColor.getId(), wrongType.getId()); + } + + @Test + @DisplayName("given_textSearch_should_matchDefinition") + void given_textSearch_should_matchDefinition() throws Exception { + // Arrange + Tenant tenant = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-text-search"); + + MarkingDefinition matching = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "SEARCH-ME-DEFINITION", + "#101010", + 4, + Instant.parse("2026-04-01T09:00:00Z")); + MarkingDefinition other = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "OTHER-DEFINITION", + "#202020", + 5, + Instant.parse("2026-04-01T10:00:00Z")); + + SearchPaginationInput input = new SearchPaginationInput(); + input.setTextSearch("SEARCH-ME"); + + // Act + String response = + mvc.perform( + post(URI + "/search", tenant.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + List ids = JsonPath.read(response, "$.content[*].marking_definition_id"); + assertThat(ids).containsExactly(matching.getId()).doesNotContain(other.getId()); + } + + @Test + @DisplayName("given_sortByOrderDesc_should_returnHighestOrderFirst") + void given_sortByOrderDesc_should_returnHighestOrderFirst() throws Exception { + // Arrange + Tenant tenant = tenantIsolationTestHelper.createTenantWithCurrentUser("marking-sort-order"); + + MarkingDefinition low = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "ORDER-LOW", + "#111111", + 1, + Instant.parse("2026-05-01T08:00:00Z")); + MarkingDefinition high = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "ORDER-HIGH", + "#222222", + 9, + Instant.parse("2026-05-01T09:00:00Z")); + + SearchPaginationInput input = new SearchPaginationInput(); + input.setSorts( + List.of( + SortField.builder().property("marking_definition_order").direction("desc").build())); + + // Act + String response = + mvc.perform( + post(URI + "/search", tenant.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + List ids = JsonPath.read(response, "$.content[*].marking_definition_id"); + assertThat(ids.indexOf(high.getId())).isLessThan(ids.indexOf(low.getId())); + } + + @Test + @DisplayName("given_sortByCreatedAtAsc_should_returnOldestFirst") + void given_sortByCreatedAtAsc_should_returnOldestFirst() throws Exception { + // Arrange + Tenant tenant = + tenantIsolationTestHelper.createTenantWithCurrentUser("marking-sort-created-at"); + + MarkingDefinition oldest = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "CREATED-OLDEST", + "#333333", + 2, + Instant.parse("2026-06-01T08:00:00Z")); + MarkingDefinition newest = + createPersistedMarkingDefinition( + tenant.getId(), + "TLP", + "CREATED-NEWEST", + "#444444", + 3, + Instant.parse("2026-06-01T10:00:00Z")); + + SearchPaginationInput input = new SearchPaginationInput(); + input.setSorts( + List.of( + SortField.builder() + .property("marking_definition_created_at") + .direction("asc") + .build())); + + // Act + String response = + mvc.perform( + post(URI + "/search", tenant.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + List ids = JsonPath.read(response, "$.content[*].marking_definition_id"); + assertThat(ids.indexOf(oldest.getId())).isLessThan(ids.indexOf(newest.getId())); + } + } + + private MarkingDefinition createPersistedMarkingDefinition( + String tenantId, String type, String definition, String color, int order, Instant createdAt) { + MarkingDefinition markingDefinition = new MarkingDefinition(); + markingDefinition.setType(type); + markingDefinition.setDefinition(definition); + markingDefinition.setColor(color); + markingDefinition.setOrder(order); + markingDefinition.setProtectedDefinition(false); + markingDefinition.setTenant(entityManager.getReference(Tenant.class, tenantId)); + markingDefinition.setCreatedAt(createdAt); + markingDefinition.setUpdatedAt(createdAt); + return repository.save(markingDefinition); + } +} diff --git a/openaev-api/src/test/java/io/openaev/architecture/TenantActiveTableAccessArchTest.java b/openaev-api/src/test/java/io/openaev/architecture/TenantActiveTableAccessArchTest.java index c455b624b6c..83bc0f672a5 100644 --- a/openaev-api/src/test/java/io/openaev/architecture/TenantActiveTableAccessArchTest.java +++ b/openaev-api/src/test/java/io/openaev/architecture/TenantActiveTableAccessArchTest.java @@ -9,6 +9,7 @@ import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import io.openaev.api.chaining.InjectExecutionStep; +import io.openaev.api.marking_definition.MarkingDefinitionApi; import io.openaev.database.model.CatalogConnector; import io.openaev.database.model.Exercise; import io.openaev.database.model.Inject; @@ -24,6 +25,7 @@ import io.openaev.database.repository.ImportMapperRepository; import io.openaev.database.repository.InjectorRepository; import io.openaev.database.repository.LessonsTemplateRepository; +import io.openaev.database.repository.MarkingDefinitionRepository; import io.openaev.database.repository.MitigationRepository; import io.openaev.database.repository.SecurityCoverageRepository; import io.openaev.database.repository.attackpath.AttackPathExecutionRepository; @@ -100,6 +102,7 @@ import io.openaev.service.chaining.ScopeSnapshotService; import io.openaev.service.connector_instances.ConnectorInstanceService; import io.openaev.service.connectors.ConnectorOrchestrationService; +import io.openaev.service.marking_definition.MarkingDefinitionService; import io.openaev.service.scenario.ScenarioService; import io.openaev.service.stix.SecurityCoverageService; import io.openaev.service.targets.search.AgentTargetSearchAdaptor; @@ -157,7 +160,8 @@ class TenantActiveTableAccessArchTest { "autonomous_runs", "autonomous_events", "autonomous_directives", - "security_coverages"); + "security_coverages", + "marking_definitions"); @ArchTest static void every_active_table_is_guarded(JavaClasses classes) throws Exception { @@ -259,6 +263,24 @@ static void every_active_table_is_guarded(JavaClasses classes) throws Exception "mitigations is tenant-active: an accessor without a tenant scope silently reads" + " zero rows. New accessors must carry a scope and be allowlisted here"); + @ArchTest + static final ArchRule marking_definitions_repository_access_is_reviewed = + noClasses() + .that() + .doNotBelongToAnyOf( + // TxCtx-carrying entrypoint, pinned by TenantScopedEntrypointsTxCtxArchTest: + MarkingDefinitionApi.class, + // Service behind the handler; every caller is a wired handler: + MarkingDefinitionService.class, + // Provisioning datapack: seeds protected defaults during tenant creation. + V20260330_Default_tenant_data.class) + .should() + .dependOnClassesThat() + .areAssignableTo(MarkingDefinitionRepository.class) + .because( + "marking_definitions is tenant-active: an accessor without a tenant scope silently reads" + + " zero rows. New accessors must carry a scope and be allowlisted here"); + @ArchTest static final ArchRule collectors_repository_access_is_reviewed = noClasses() diff --git a/openaev-api/src/test/java/io/openaev/architecture/TenantScopedEntrypointsTxCtxArchTest.java b/openaev-api/src/test/java/io/openaev/architecture/TenantScopedEntrypointsTxCtxArchTest.java index 18de39a5901..c2796b921e0 100644 --- a/openaev-api/src/test/java/io/openaev/architecture/TenantScopedEntrypointsTxCtxArchTest.java +++ b/openaev-api/src/test/java/io/openaev/architecture/TenantScopedEntrypointsTxCtxArchTest.java @@ -81,6 +81,11 @@ class TenantScopedEntrypointsTxCtxArchTest { "io.openaev.api.attackpath.AttackPathApi#graphDelta", "io.openaev.api.attackpath.AttackPathApi#simulations", "io.openaev.api.attackpath.AttackPathApi#expandEndpointFindings", + // marking_definitions (v2) + "io.openaev.api.marking_definition.MarkingDefinitionApi#search", + "io.openaev.api.marking_definition.MarkingDefinitionApi#create", + "io.openaev.api.marking_definition.MarkingDefinitionApi#update", + "io.openaev.api.marking_definition.MarkingDefinitionApi#delete", "io.openaev.api.attackpath.AttackPathApi#relations", "io.openaev.api.attackpath.AttackPathApi#findings", "io.openaev.api.attackpath.AttackPathApi#executionDetail", diff --git a/openaev-front/src/actions/Schema.js b/openaev-front/src/actions/Schema.js index 56e91f50618..dc793f73c3a 100644 --- a/openaev-front/src/actions/Schema.js +++ b/openaev-front/src/actions/Schema.js @@ -276,6 +276,13 @@ export const notification = new schema.Entity( ); export const arrayOfNotifications = new schema.Array(notification); +export const markingDefinition = new schema.Entity( + 'marking_definitions', + {}, + { idAttribute: 'marking_definition_id' }, +); +export const arrayOfMarkingDefinitions = new schema.Array(markingDefinition); + token.define({ token_user: user }); user.define({ user_organization: organization }); @@ -355,6 +362,10 @@ export const storeHelper = state => ({ getTag: id => entity(id, 'tags', state), getTags: () => entities('tags', state), getTagsMap: () => maps('tags', state), + // marking definitions + getMarkingDefinition: id => entity(id, 'marking_definitions', state), + getMarkingDefinitions: () => entities('marking_definitions', state), + getMarkingDefinitionsMap: () => maps('marking_definitions', state), // injects getInject: id => entity(id, 'injects', state), diff --git a/openaev-front/src/actions/marking_definitions/marking-definition-actions.ts b/openaev-front/src/actions/marking_definitions/marking-definition-actions.ts new file mode 100644 index 00000000000..4fb94a67bfd --- /dev/null +++ b/openaev-front/src/actions/marking_definitions/marking-definition-actions.ts @@ -0,0 +1,35 @@ +import type { Dispatch } from 'redux'; + +import { delReferential, getReferential, postReferential, putReferential, simplePostCall } from '../../utils/Action'; +import { + type MarkingDefinitionInput, + type MarkingDefinitionOutput, + type SearchPaginationInput, +} from '../../utils/api-types'; +import * as schema from '../Schema'; + +const MARKING_DEFINITIONS_URI = '/api/marking_definitions'; + +export const searchMarkingDefinitions = (searchPaginationInput: SearchPaginationInput) => { + return simplePostCall(`${MARKING_DEFINITIONS_URI}/search`, searchPaginationInput); +}; + +export const fetchMarkingDefinitions = () => + (dispatch: Dispatch): Promise => { + return getReferential(schema.arrayOfMarkingDefinitions, MARKING_DEFINITIONS_URI)(dispatch); + }; + +export const createMarkingDefinition = (input: MarkingDefinitionInput) => (dispatch: Dispatch) => { + return postReferential(schema.markingDefinition, MARKING_DEFINITIONS_URI, input)(dispatch); +}; + +export const updateMarkingDefinition = ( + markingDefinitionId: string, + input: MarkingDefinitionInput, +) => (dispatch: Dispatch) => { + return putReferential(schema.markingDefinition, `${MARKING_DEFINITIONS_URI}/${markingDefinitionId}`, input)(dispatch); +}; + +export const deleteMarkingDefinition = (markingDefinitionId: string) => (dispatch: Dispatch) => { + return delReferential(`${MARKING_DEFINITIONS_URI}/${markingDefinitionId}`, 'marking_definitions', markingDefinitionId)(dispatch); +}; diff --git a/openaev-front/src/admin/components/nav/config/settings.config.tsx b/openaev-front/src/admin/components/nav/config/settings.config.tsx index f1023f2c577..d79b1917519 100644 --- a/openaev-front/src/admin/components/nav/config/settings.config.tsx +++ b/openaev-front/src/admin/components/nav/config/settings.config.tsx @@ -3,6 +3,7 @@ import { SettingsOutlined } from '@mui/icons-material'; import { type LeftMenuItem } from '../../../../components/common/menu/leftmenu/leftmenu-model'; import { type AppAbility } from '../../../../utils/permissions/ability'; import { ACTIONS, type Actions, SUBJECTS, type Subjects } from '../../../../utils/permissions/types'; +import { isFeatureEnabled } from '../../../../utils/utils'; export const SETTINGS_LABEL = 'Settings'; @@ -35,6 +36,10 @@ export const SETTINGS_ACCESS_CHECKS: { action: ACTIONS.ACCESS, subject: SUBJECTS.TENANTS, }, + { + action: ACTIONS.ACCESS, + subject: SUBJECTS.MARKING_DEFINITION, + }, { // Lessons learned templates live under Settings > Customization. action: ACTIONS.ACCESS, @@ -68,6 +73,8 @@ const settingsEntries = (ability: AppAbility): LeftMenuItem[] => { const canAccessPlatformSettings = ability.can(ACTIONS.ACCESS, SUBJECTS.PLATFORM_SETTINGS); const canAccessPlatformUGR = ability.can(ACTIONS.ACCESS, SUBJECTS.PLATFORM_USERS_GROUPS_AND_ROLES); const canAccessTenants = ability.can(ACTIONS.ACCESS, SUBJECTS.TENANTS); + const canAccessMarkingDefinitions = isFeatureEnabled('MARKING') + && ability.can(ACTIONS.ACCESS, SUBJECTS.MARKING_DEFINITION); const canAccessLessonsLearned = ability.can(ACTIONS.ACCESS, SUBJECTS.LESSONS_LEARNED); const hasTagsAccess = canAccessTags(ability); const canManageAnySessions = ability.can(ACTIONS.MANAGE, SUBJECTS.SESSIONS) @@ -83,7 +90,7 @@ const settingsEntries = (ability: AppAbility): LeftMenuItem[] => { link: '/admin/settings/security', label: 'Security', userRight: hasTenantSettingsAccess || canAccessTenantUsers || canAccessPlatformUGR || canAccessTenants - || canManageAnySessions, + || canManageAnySessions || canAccessMarkingDefinitions, }, { // Section root: redirects to asset_rules; Notifiers and Lessons learned diff --git a/openaev-front/src/admin/components/settings/Index.tsx b/openaev-front/src/admin/components/settings/Index.tsx index 79031e5e373..7a9bb6fd741 100644 --- a/openaev-front/src/admin/components/settings/Index.tsx +++ b/openaev-front/src/admin/components/settings/Index.tsx @@ -3,9 +3,11 @@ import { Navigate, Route, Routes } from 'react-router'; import { errorWrapper } from '../../../components/Error'; import NotFound from '../../../components/NotFound'; +import NoAccess from '../../../utils/permissions/NoAccess'; import { AbilityContext } from '../../../utils/permissions/permissionsContext'; import ProtectedRoute from '../../../utils/permissions/ProtectedRoute'; import { ACTIONS, SUBJECTS } from '../../../utils/permissions/types'; +import { isFeatureEnabled } from '../../../utils/utils'; import LessonsTemplateIndex from '../components/lessons/Index'; import LessonsTemplates from '../components/lessons/LessonsTemplates'; import Tenants from '../platform/tenants/Tenants'; @@ -16,6 +18,7 @@ import XlsMappers from './data_ingestion/XlsMappers'; import Experience from './experience/Experience'; import Groups from './groups/Groups'; import KillChainPhases from './kill_chain_phases/KillChainPhases'; +import MarkingDefinitions from './marking_definitions/MarkingDefinitions'; import Notifiers from './notifiers/Notifiers'; import Organizations from './organizations/Organizations'; import Policies from './policies/Policies'; @@ -56,6 +59,9 @@ const SecurityLanding = () => { canAccessSession, } = useSecurityScope(); const ability = useContext(AbilityContext); + const canAccessMarkingDefinitions + = isFeatureEnabled('MARKING') + && ability.can(ACTIONS.ACCESS, SUBJECTS.MARKING_DEFINITION); // The landing arbitrates between both scopes, so each one is named explicitly. const canManageSessions = canAccessSession('TENANT'); const canManagePlatformSessions = canAccessSession('PLATFORM'); @@ -68,6 +74,9 @@ const SecurityLanding = () => { if (ability.can(ACTIONS.ACCESS, SUBJECTS.TENANTS)) { return ; } + if (canAccessMarkingDefinitions) { + return ; + } if (canManageSessions || canManagePlatformSessions) { return ; } @@ -76,6 +85,7 @@ const SecurityLanding = () => { }; const Index = () => { + const isMarkingEnabled = isFeatureEnabled('MARKING'); return ( } /> @@ -135,6 +145,22 @@ const Index = () => { /> )} /> + + ) : + )} + /> { } = useSecurityScope(); const ability = useContext(AbilityContext); const canAccessTenants = ability.can(ACTIONS.ACCESS, SUBJECTS.TENANTS); + const canAccessMarkingDefinitions + = isFeatureEnabled('MARKING') + && ability.can(ACTIONS.ACCESS, SUBJECTS.MARKING_DEFINITION); // The platform scope is an EE feature: in Community Edition the switcher is // not displayed at all and the section stays on the tenant scope. diff --git a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionForm.tsx b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionForm.tsx new file mode 100644 index 00000000000..3988130d212 --- /dev/null +++ b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionForm.tsx @@ -0,0 +1,121 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button, TextField } from '@mui/material'; +import { type FunctionComponent, type SyntheticEvent } from 'react'; +import { type SubmitHandler, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import ColorPickerField from '../../../../components/ColorPickerField'; +import { useFormatter } from '../../../../components/i18n'; +import { type MarkingDefinitionInput } from '../../../../utils/api-types'; +import { zodImplement } from '../../../../utils/Zod'; + +interface Props { + defaultValues?: MarkingDefinitionInput; + isEdit?: boolean; + onSubmit: SubmitHandler; +} + +const HEX_COLOR_REGEX = /^#([0-9a-fA-F]{6})$/; + +const MarkingDefinitionForm: FunctionComponent = ({ + defaultValues, + isEdit = false, + onSubmit, +}) => { + const { t } = useFormatter(); + + const { + register, + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + mode: 'onChange', + resolver: zodResolver( + zodImplement().with({ + marking_definition_type: z.string().min(1, { message: t('Should not be empty') }), + marking_definition_definition: z.string().min(1, { message: t('Should not be empty') }), + marking_definition_color: z + .string() + .optional() + .refine(value => !value || HEX_COLOR_REGEX.test(value), { message: t('Color must be a valid hex value, e.g. #4CAF50') }), + marking_definition_order: z + .number({ message: t('Should not be empty') }) + .int({ message: t('Order must be an integer') }) + .min(0, { message: t('Order must be greater than or equal to 0') }), + }), + ), + defaultValues: defaultValues ?? { + marking_definition_type: '', + marking_definition_definition: '', + marking_definition_color: '', + marking_definition_order: 0, + }, + }); + + const handleSubmitWithoutPropagation = (e: SyntheticEvent) => { + e.preventDefault(); + e.stopPropagation(); + handleSubmit(onSubmit)(e); + }; + + return ( +
+ + + + +
+ +
+ + ); +}; + +export default MarkingDefinitionForm; diff --git a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx new file mode 100644 index 00000000000..1690a7828cb --- /dev/null +++ b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx @@ -0,0 +1,115 @@ +import { type FunctionComponent, useContext, useState } from 'react'; + +import { + deleteMarkingDefinition, + updateMarkingDefinition, +} from '../../../../actions/marking_definitions/marking-definition-actions'; +import ButtonPopover, { type PopoverEntry } from '../../../../components/common/ButtonPopover'; +import DialogDelete from '../../../../components/common/DialogDelete'; +import Drawer from '../../../../components/common/Drawer'; +import { useFormatter } from '../../../../components/i18n'; +import { + type MarkingDefinitionInput, + type MarkingDefinitionOutput, +} from '../../../../utils/api-types'; +import { useAppDispatch } from '../../../../utils/hooks'; +import { AbilityContext } from '../../../../utils/permissions/permissionsContext'; +import { ACTIONS, SUBJECTS } from '../../../../utils/permissions/types'; +import MarkingDefinitionForm from './MarkingDefinitionForm'; +import { + extractMarkingDefinitionFromStoreResult, + type MarkingDefinitionStoreResult, +} from './MarkingDefinitionStoreHelper'; + +interface Props { + markingDefinition: MarkingDefinitionOutput; + onDelete?: (id: string) => void; + onUpdate?: (result: MarkingDefinitionOutput) => void; +} + +const MarkingDefinitionPopover: FunctionComponent = ({ + markingDefinition, + onDelete, + onUpdate, +}) => { + const { t } = useFormatter(); + const dispatch = useAppDispatch(); + const ability = useContext(AbilityContext); + const canManage = ability.can(ACTIONS.MANAGE, SUBJECTS.MARKING_DEFINITION); + const canDelete = ability.can(ACTIONS.DELETE, SUBJECTS.MARKING_DEFINITION); + + const [openUpdate, setOpenUpdate] = useState(false); + const [openDelete, setOpenDelete] = useState(false); + + const isProtected = markingDefinition.marking_definition_protected; + + const updateInputFromDefinition + = (value: MarkingDefinitionOutput): MarkingDefinitionInput => ({ + marking_definition_type: value.marking_definition_type, + marking_definition_definition: value.marking_definition_definition, + marking_definition_color: value.marking_definition_color, + marking_definition_order: value.marking_definition_order, + }); + + const submitUpdate = (input: MarkingDefinitionInput) => { + if (input.marking_definition_order !== markingDefinition.marking_definition_order) { + const confirmed = window.confirm(t('Changing order can impact precedence. Do you want to continue?')); + if (!confirmed) { + return Promise.resolve(); + } + } + return dispatch(updateMarkingDefinition(markingDefinition.marking_definition_id, input)) + .then((result: MarkingDefinitionStoreResult) => { + const updatedMarkingDefinition = extractMarkingDefinitionFromStoreResult(result); + if (updatedMarkingDefinition) { + onUpdate?.(updatedMarkingDefinition); + setOpenUpdate(false); + } + return result; + }) + .catch((error: unknown) => error); + }; + + const submitDelete = () => { + return dispatch(deleteMarkingDefinition(markingDefinition.marking_definition_id)) + .then(() => { + onDelete?.(markingDefinition.marking_definition_id); + setOpenDelete(false); + }) + .catch((error: unknown) => error); + }; + + const entries: PopoverEntry[] = [ + { + label: 'Update', + action: () => setOpenUpdate(true), + userRight: canManage && !isProtected, + }, + { + label: 'Delete', + action: () => setOpenDelete(true), + userRight: canDelete && !isProtected, + }, + ]; + + return ( + <> + + setOpenUpdate(false)} title={t('Update a marking definition')}> + + + setOpenDelete(false)} + handleSubmit={submitDelete} + text={t('Do you want to delete this marking definition?')} + /> + + ); +}; + +export default MarkingDefinitionPopover; diff --git a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionStoreHelper.ts b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionStoreHelper.ts new file mode 100644 index 00000000000..fdd33dd8b30 --- /dev/null +++ b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionStoreHelper.ts @@ -0,0 +1,15 @@ +import { type MarkingDefinitionOutput } from '../../../../utils/api-types'; + +export type MarkingDefinitionStoreResult = { + result?: string; + entities?: { marking_definitions?: Record }; +}; + +export const extractMarkingDefinitionFromStoreResult = ( + value: MarkingDefinitionStoreResult, +): MarkingDefinitionOutput | null => { + if (!value?.result) { + return null; + } + return value.entities?.marking_definitions?.[value.result] ?? null; +}; diff --git a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitions.tsx b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitions.tsx new file mode 100644 index 00000000000..0b4e1cb95bf --- /dev/null +++ b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitions.tsx @@ -0,0 +1,242 @@ +import { LensOutlined, SecurityOutlined } from '@mui/icons-material'; +import { Box, List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; +import { type CSSProperties, useMemo, useState } from 'react'; +import { makeStyles } from 'tss-react/mui'; + +import { + createMarkingDefinition, + fetchMarkingDefinitions, + searchMarkingDefinitions, +} from '../../../../actions/marking_definitions/marking-definition-actions'; +import Breadcrumbs from '../../../../components/Breadcrumbs'; +import ButtonCreate from '../../../../components/common/ButtonCreate'; +import Drawer from '../../../../components/common/Drawer'; +import PaginationComponentV2 from '../../../../components/common/queryable/pagination/PaginationComponentV2'; +import { buildSearchPagination } from '../../../../components/common/queryable/QueryableUtils'; +import SortHeadersComponentV2 from '../../../../components/common/queryable/sort/SortHeadersComponentV2'; +import useBodyItemsStyles from '../../../../components/common/queryable/style/style'; +import { useQueryableWithLocalStorage } from '../../../../components/common/queryable/useQueryableWithLocalStorage'; +import { type Header } from '../../../../components/common/SortHeadersList'; +import { useFormatter } from '../../../../components/i18n'; +import { + type MarkingDefinitionInput, + type MarkingDefinitionOutput, +} from '../../../../utils/api-types'; +import { useAppDispatch } from '../../../../utils/hooks'; +import useDataLoader from '../../../../utils/hooks/useDataLoader'; +import { Can } from '../../../../utils/permissions/permissionsContext'; +import { ACTIONS, SUBJECTS } from '../../../../utils/permissions/types'; +import { SETTINGS_LABEL } from '../../nav/config/settings.config'; +import SecurityMenu from '../SecurityMenu'; +import MarkingDefinitionForm from './MarkingDefinitionForm'; +import MarkingDefinitionPopover from './MarkingDefinitionPopover'; +import { + extractMarkingDefinitionFromStoreResult, + type MarkingDefinitionStoreResult, +} from './MarkingDefinitionStoreHelper'; + +const useStyles = makeStyles()(() => ({ itemHead: { textTransform: 'uppercase' } })); + +const inlineStyles: Record = { + marking_definition_type: { width: '18%' }, + marking_definition_definition: { width: '26%' }, + marking_definition_color: { width: '16%' }, + marking_definition_order: { width: '10%' }, + marking_definition_created_at: { width: '20%' }, +}; + +const MarkingDefinitions = () => { + const { t, fldt } = useFormatter(); + const dispatch = useAppDispatch(); + const { classes } = useStyles(); + const bodyItemsStyles = useBodyItemsStyles(); + + const [markingDefinitions, setMarkingDefinitions] = useState([]); + const [openCreate, setOpenCreate] = useState(false); + + useDataLoader(() => { + dispatch(fetchMarkingDefinitions()); + }); + + const availableFilterNames = [ + 'marking_definition_type', + 'marking_definition_definition', + 'marking_definition_color', + 'marking_definition_order', + 'marking_definition_created_at', + ]; + + const { queryableHelpers, searchPaginationInput } = useQueryableWithLocalStorage( + 'marking_definitions', + buildSearchPagination({}), + ); + + const upsert = (result: MarkingDefinitionOutput) => + setMarkingDefinitions((prev) => { + return prev.some(d => d.marking_definition_id === result.marking_definition_id) + ? prev.map(d => + d.marking_definition_id === result.marking_definition_id ? result : d) + : [...prev, result]; + }); + + const headers: Header[] = useMemo( + () => [ + { + field: 'marking_definition_type', + label: 'Type', + isSortable: true, + value: (item: MarkingDefinitionOutput) => item.marking_definition_type, + }, + { + field: 'marking_definition_definition', + label: 'Definition', + isSortable: true, + value: (item: MarkingDefinitionOutput) => item.marking_definition_definition, + }, + { + field: 'marking_definition_color', + label: 'Color', + isSortable: true, + value: (item: MarkingDefinitionOutput) => ( + + {item.marking_definition_color ? ( + + ) : null} + {item.marking_definition_color ?? '-'} + + ), + }, + { + field: 'marking_definition_order', + label: 'Order', + isSortable: true, + value: (item: MarkingDefinitionOutput) => (item.marking_definition_order ?? '-').toString(), + }, + { + field: 'marking_definition_created_at', + label: 'Creation date', + isSortable: true, + value: (item: MarkingDefinitionOutput) => fldt(item.marking_definition_created_at), + }, + ], + [fldt], + ); + + const submitCreate = (input: MarkingDefinitionInput) => { + return dispatch(createMarkingDefinition(input)) + .then((result: MarkingDefinitionStoreResult) => { + const createdMarkingDefinition = extractMarkingDefinitionFromStoreResult(result); + if (createdMarkingDefinition) { + upsert(createdMarkingDefinition); + setOpenCreate(false); + } + return result; + }) + .catch((error: unknown) => error); + }; + + return ( +
+
+ + + setOpenCreate(true)} label={t('Add a marking definition')} /> + + )} + /> + +  } + > + + + )} + /> + + {markingDefinitions.map((item: MarkingDefinitionOutput) => ( + + setMarkingDefinitions(prev => + prev.filter(d => d.marking_definition_id !== result))} + onUpdate={upsert} + /> + )} + divider + > + + + + + {headers.map(header => ( +
+ {header.value?.(item)} +
+ ))} +
+ )} + /> + + ))} + +
+ + setOpenCreate(false)} + title={t('Add a marking definition')} + > + + + + ); +}; + +export default MarkingDefinitions; diff --git a/openaev-front/src/reducers/Referential.ts b/openaev-front/src/reducers/Referential.ts index f86d536404d..fe3f2d6bf6c 100644 --- a/openaev-front/src/reducers/Referential.ts +++ b/openaev-front/src/reducers/Referential.ts @@ -65,6 +65,7 @@ export const entitiesInitializer = Map({ notifications: Map({}), phishinglandingpages: Map({}), phishingemailtemplates: Map({}), + marking_definitions: Map({}), }), }); diff --git a/openaev-front/src/utils/api-types.d.ts b/openaev-front/src/utils/api-types.d.ts index e0fd36dfa52..752a98681a3 100644 --- a/openaev-front/src/utils/api-types.d.ts +++ b/openaev-front/src/utils/api-types.d.ts @@ -7622,6 +7622,34 @@ export interface MapperConditionOutput { condition_value?: string; } +export interface MarkingDefinitionInput { + marking_definition_color?: string; + /** @minLength 1 */ + marking_definition_definition: string; + /** + * @format int32 + * @min 0 + */ + marking_definition_order: number; + /** @minLength 1 */ + marking_definition_type: string; +} + +export interface MarkingDefinitionOutput { + marking_definition_color?: string; + /** @format date-time */ + marking_definition_created_at: string; + /** @minLength 1 */ + marking_definition_definition: string; + /** @minLength 1 */ + marking_definition_id: string; + /** @format int32 */ + marking_definition_order: number; + marking_definition_protected: boolean; + /** @minLength 1 */ + marking_definition_type: string; +} + export interface MissingImportedAction { name?: string; type?: string; @@ -7829,6 +7857,8 @@ export interface NotificationTriggerInput { | "RESOURCE_TYPE" | "SECURITY_PLATFORM" | "CREDENTIAL" + | "MARKING_DEFINITION" + | "MARKING_ASSIGNMENT" | "DOCUMENT" | "CHANNEL" | "PHISHING_LANDING_PAGE" @@ -7934,6 +7964,8 @@ export interface NotificationTriggerOutput { | "RESOURCE_TYPE" | "SECURITY_PLATFORM" | "CREDENTIAL" + | "MARKING_DEFINITION" + | "MARKING_ASSIGNMENT" | "DOCUMENT" | "CHANNEL" | "PHISHING_LANDING_PAGE" @@ -8524,6 +8556,25 @@ export interface PageLessonsTemplate { totalPages?: number; } +export interface PageMarkingDefinitionOutput { + content?: MarkingDefinitionOutput[]; + empty?: boolean; + first?: boolean; + last?: boolean; + /** @format int32 */ + number?: number; + /** @format int32 */ + numberOfElements?: number; + pageable?: PageableObject; + /** @format int32 */ + size?: number; + sort?: SortObject[]; + /** @format int64 */ + totalElements?: number; + /** @format int32 */ + totalPages?: number; +} + export interface PageMitigation { content?: Mitigation[]; empty?: boolean; @@ -9557,6 +9608,12 @@ export interface PlatformRoleInput { | "ACCESS_CREDENTIALS" | "MANAGE_CREDENTIALS" | "DELETE_CREDENTIALS" + | "ACCESS_MARKING_DEFINITION" + | "MANAGE_MARKING_DEFINITION" + | "DELETE_MARKING_DEFINITION" + | "ACCESS_MARKING_ASSIGNMENT" + | "ASSIGN_MARKING" + | "DELETE_MARKING_ASSIGNMENT" | "ACCESS_DASHBOARDS" | "MANAGE_DASHBOARDS" | "DELETE_DASHBOARDS" @@ -9649,6 +9706,7 @@ export interface PlatformSettings { | "LEGACY_INGESTION_EXECUTION_TRACE" | "OPENAEV_TRIALS_XTMHUB" | "CREDENTIAL_ASSET" + | "MARKING" )[]; /** True if the Tanium Executor is enabled */ executor_tanium_enable?: boolean; @@ -9944,6 +10002,7 @@ export interface PublicPlatformSettings { | "LEGACY_INGESTION_EXECUTION_TRACE" | "OPENAEV_TRIALS_XTMHUB" | "CREDENTIAL_ASSET" + | "MARKING" )[]; /** Map of the messages to display on the screen by their level (the level available are DEBUG, INFO, WARN, ERROR, FATAL) */ platform_banner_by_level?: Record; @@ -10387,6 +10446,12 @@ export interface RoleInput { | "ACCESS_CREDENTIALS" | "MANAGE_CREDENTIALS" | "DELETE_CREDENTIALS" + | "ACCESS_MARKING_DEFINITION" + | "MANAGE_MARKING_DEFINITION" + | "DELETE_MARKING_DEFINITION" + | "ACCESS_MARKING_ASSIGNMENT" + | "ASSIGN_MARKING" + | "DELETE_MARKING_ASSIGNMENT" | "ACCESS_DASHBOARDS" | "MANAGE_DASHBOARDS" | "DELETE_DASHBOARDS" @@ -12525,6 +12590,12 @@ export interface User { | "ACCESS_CREDENTIALS" | "MANAGE_CREDENTIALS" | "DELETE_CREDENTIALS" + | "ACCESS_MARKING_DEFINITION" + | "MANAGE_MARKING_DEFINITION" + | "DELETE_MARKING_DEFINITION" + | "ACCESS_MARKING_ASSIGNMENT" + | "ASSIGN_MARKING" + | "DELETE_MARKING_ASSIGNMENT" | "ACCESS_DASHBOARDS" | "MANAGE_DASHBOARDS" | "DELETE_DASHBOARDS" diff --git a/openaev-front/src/utils/lang/de.json b/openaev-front/src/utils/lang/de.json index 914453ccc77..0e05e65bb16 100644 --- a/openaev-front/src/utils/lang/de.json +++ b/openaev-front/src/utils/lang/de.json @@ -160,6 +160,7 @@ "Add 1 inject": "1 Injektion hinzufügen", "Add a category": "Kategorie hinzufügen", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Markierungsdefinition hinzufügen", "Add a new vulnerability": "Eine neue Schwachstelle hinzufügen", "Add a result": "Ein Ergebnis hinzufügen", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "Logo ändern", "Change tone": "Ton ändern", "Change your password": "Ändern Sie Ihr Passwort", + "Changing order can impact precedence. Do you want to continue?": "Eine Änderung der Reihenfolge kann sich auf die Priorität auswirken. Möchten Sie fortfahren?", "Channel": "Kanal", "channels": "Kanäle", "Channels": "Kanäle", @@ -704,6 +706,7 @@ "collectors": "sammler", "Collectors": "Sammler", "Color": "Farbe", + "Color must be a valid hex value, e.g. #4CAF50": "Die Farbe muss ein gültiger Hex-Wert sein, z. B. #4CAF50", "Colors": "Colors", "Comcheck": "Comcheck", "Comchecks": "Comchecks", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "Möchten Sie diese Frage zu den gelernten Lektionen löschen?", "Do you want to delete this lessons learned template?": "Möchten Sie diese Vorlage zu den gelernten Lektionen löschen?", "Do you want to delete this log?": "Möchten Sie dieses Protokoll löschen?", + "Do you want to delete this marking definition?": "Möchten Sie diese Markierungsdefinition löschen?", "Do you want to delete this media pressure article?": "Möchten Sie diesen Mediendruck-Artikel löschen?", "Do you want to delete this media pressure?": "Möchten Sie diesen Mediendruck löschen?", "Do you want to delete this mitigation?": "Möchten Sie diese Entschärfung löschen?", @@ -2272,6 +2276,12 @@ "Mark as done": "Markieren als erledigt", "Mark as read": "Als gelesen markieren", "Mark as unread": "Als ungelesen markieren", + "Marking definitions": "Markierungsdefinitionen", + "marking_definition_color": "Farbe", + "marking_definition_created_at": "Erstellungsdatum", + "marking_definition_definition": "Definition", + "marking_definition_order": "Reihenfolge", + "marking_definition_type": "Typ", "Massive operations": "Massenoperationen", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "Entspricht dem, was Empfänger sehen", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Bestellung", + "Order must be an integer": "Die Reihenfolge muss eine ganze Zahl sein", + "Order must be greater than or equal to 0": "Die Reihenfolge muss größer oder gleich 0 sein", "Organisation": "Organisation", "Organization": "Organisation", "Organizations": "Organisationen", @@ -2923,6 +2935,7 @@ "Proof": "Nachweis", "Proof of exploitation": "Nachweis der Ausnutzung", "Proofs": "Nachweise", + "Protected": "Geschützt", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Anbieter", "providing": "Bereitstellung", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "Nicht verifiziert: Noch nicht getestet", "Up next": "Als Nächstes", "Update": "Aktualisierung", + "Update a marking definition": "Markierungsdefinition aktualisieren", "Update a notification rule": "Aktualisieren einer Benachrichtigungsregel", "Update action": "Aktion aktualisieren", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/en.json b/openaev-front/src/utils/lang/en.json index 653d5b651eb..336d1210e76 100644 --- a/openaev-front/src/utils/lang/en.json +++ b/openaev-front/src/utils/lang/en.json @@ -160,6 +160,7 @@ "Add 1 inject": "Add 1 inject", "Add a category": "Add a category", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Add a marking definition", "Add a new vulnerability": "Add a new vulnerability", "Add a result": "Add a result", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "Change logo", "Change tone": "Change tone", "Change your password": "Change your password", + "Changing order can impact precedence. Do you want to continue?": "Changing order can impact precedence. Do you want to continue?", "Channel": "Channel", "channels": "Channels", "Channels": "Channels", @@ -704,6 +706,7 @@ "collectors": "collectors", "Collectors": "Collectors", "Color": "Color", + "Color must be a valid hex value, e.g. #4CAF50": "Color must be a valid hex value, e.g. #4CAF50", "Colors": "Colors", "Comcheck": "Comcheck", "Comchecks": "Comchecks", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "Do you want to delete this lessons learned question?", "Do you want to delete this lessons learned template?": "Do you want to delete this lessons learned template?", "Do you want to delete this log?": "Do you want to delete this log?", + "Do you want to delete this marking definition?": "Do you want to delete this marking definition?", "Do you want to delete this media pressure article?": "Do you want to delete this media pressure article?", "Do you want to delete this media pressure?": "Do you want to delete this media pressure?", "Do you want to delete this mitigation?": "Do you want to delete this mitigation?", @@ -2272,6 +2276,12 @@ "Mark as done": "Mark as done", "Mark as read": "Mark as read", "Mark as unread": "Mark as unread", + "Marking definitions": "Marking definitions", + "marking_definition_color": "Color", + "marking_definition_created_at": "Creation date", + "marking_definition_definition": "Definition", + "marking_definition_order": "Order", + "marking_definition_type": "Type", "Massive operations": "Massive operations", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "Matches what recipients see", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Order", + "Order must be an integer": "Order must be an integer", + "Order must be greater than or equal to 0": "Order must be greater than or equal to 0", "Organisation": "Organisation", "Organization": "Organization", "Organizations": "Organizations", @@ -2923,6 +2935,7 @@ "Proof": "Proof", "Proof of exploitation": "Proof of exploitation", "Proofs": "Proofs", + "Protected": "Protected", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Provider", "providing": "Providing", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "Unverified: Not yet tested", "Up next": "Up next", "Update": "Update", + "Update a marking definition": "Update a marking definition", "Update a notification rule": "Update a notification rule", "Update action": "Update action", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/es.json b/openaev-front/src/utils/lang/es.json index 4ce0422ce74..ade0cc9c006 100644 --- a/openaev-front/src/utils/lang/es.json +++ b/openaev-front/src/utils/lang/es.json @@ -160,6 +160,7 @@ "Add 1 inject": "Añadir 1 inyección", "Add a category": "Añadir una categoría", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Añadir una definición de marcado", "Add a new vulnerability": "Añadir una nueva vulnerabilidad", "Add a result": "Añadir un resultado", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "Cambiar logo", "Change tone": "Cambiar tono", "Change your password": "Cambiar contraseña", + "Changing order can impact precedence. Do you want to continue?": "Cambiar el orden puede afectar a la prioridad. ¿Deseas continuar?", "Channel": "Canal", "channels": "Canales", "Channels": "Canales", @@ -704,6 +706,7 @@ "collectors": "coleccionistas", "Collectors": "Coleccionistas", "Color": "Color", + "Color must be a valid hex value, e.g. #4CAF50": "El color debe ser un valor hexadecimal válido, p. ej., #4CAF50", "Colors": "Colors", "Comcheck": "Comcheck", "Comchecks": "Comchecks", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "¿Desea eliminar esta pregunta sobre las lecciones aprendidas?", "Do you want to delete this lessons learned template?": "¿Desea eliminar esta plantilla de lecciones aprendidas?", "Do you want to delete this log?": "¿Desea eliminar este registro?", + "Do you want to delete this marking definition?": "¿Deseas eliminar esta definición de marcado?", "Do you want to delete this media pressure article?": "¿Quiere borrar este artículo?", "Do you want to delete this media pressure?": "¿Desea eliminar este medio de prensa?", "Do you want to delete this mitigation?": "¿Desea eliminar esta mitigación?", @@ -2272,6 +2276,12 @@ "Mark as done": "Marcar como hecho", "Mark as read": "Marcar como leido", "Mark as unread": "Marcar como no leido", + "Marking definitions": "Definiciones de marcado", + "marking_definition_color": "Color", + "marking_definition_created_at": "Fecha de creacion", + "marking_definition_definition": "Definicion", + "marking_definition_order": "Orden", + "marking_definition_type": "Tipo", "Massive operations": "Operaciones masivas", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "Coincide con lo que ven los destinatarios", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Pedir", + "Order must be an integer": "El orden debe ser un número entero", + "Order must be greater than or equal to 0": "El orden debe ser mayor o igual que 0", "Organisation": "Organización", "Organization": "Organización", "Organizations": "Organizaciones", @@ -2923,6 +2935,7 @@ "Proof": "Prueba", "Proof of exploitation": "Prueba de explotación", "Proofs": "Pruebas", + "Protected": "Protegido", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Proveedor", "providing": "Proporciona", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "No verificado: Aún no probado", "Up next": "Próximas", "Update": "Actualización", + "Update a marking definition": "Actualizar una definición de marcado", "Update a notification rule": "Actualizar una regla de notificación", "Update action": "Acción de actualización", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/fr.json b/openaev-front/src/utils/lang/fr.json index 5c6cfec44e4..bf1afd30170 100644 --- a/openaev-front/src/utils/lang/fr.json +++ b/openaev-front/src/utils/lang/fr.json @@ -160,6 +160,7 @@ "Add 1 inject": "Ajouter 1 stimuli", "Add a category": "Ajouter une catégorie", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Ajouter une définition de marquage", "Add a new vulnerability": "Ajouter une nouvelle vulnérabilité", "Add a result": "Ajouter un résultat", "Add a schedule": "Ajouter une planification", @@ -632,6 +633,7 @@ "Change logo": "Changer le logo", "Change tone": "Changer le ton", "Change your password": "Changer votre mot de passe", + "Changing order can impact precedence. Do you want to continue?": "La modification de l'ordre peut avoir une incidence sur la priorité. Souhaitez-vous continuer ?", "Channel": "Média", "channels": "Canaux", "Channels": "Canaux", @@ -704,6 +706,7 @@ "collectors": "collecteurs", "Collectors": "Collecteurs", "Color": "Couleur", + "Color must be a valid hex value, e.g. #4CAF50": "La couleur doit être une valeur hexadécimale valide, par exemple #4CAF50", "Colors": "Couleurs", "Comcheck": "Vérification", "Comchecks": "Vérifications", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "Souhaitez-vous supprimer cette question de retour d'expérience ?", "Do you want to delete this lessons learned template?": "Souhaitez-vous supprimer ce template de retour d'expérience ?", "Do you want to delete this log?": "Souhaitez-vous supprimer cette entrée ?", + "Do you want to delete this marking definition?": "Souhaitez-vous supprimer cette définition de marquage ?", "Do you want to delete this media pressure article?": "Voulez-vous supprimer cet article sur la pression des médias ?", "Do you want to delete this media pressure?": "Souhaitez-vous supprimer cette pression médiatique ?", "Do you want to delete this mitigation?": "Voulez-vous supprimer cette atténuation ?", @@ -2272,6 +2276,12 @@ "Mark as done": "Marquer comme fait", "Mark as read": "Marquer comme lu", "Mark as unread": "Marquer comme non lu", + "Marking definitions": "Définitions de marquage", + "marking_definition_color": "Couleur", + "marking_definition_created_at": "Date de creation", + "marking_definition_definition": "Definition", + "marking_definition_order": "Ordre", + "marking_definition_type": "Type", "Massive operations": "Opérations massives", "Match a specific brand": "Reproduire une marque specifique", "Matches what recipients see": "Correspond à ce que voient les destinataires", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Ordre", + "Order must be an integer": "L'ordre doit être un nombre entier", + "Order must be greater than or equal to 0": "L'ordre doit être supérieur ou égal à 0", "Organisation": "Organisation", "Organization": "Organisation", "Organizations": "Organisations", @@ -2923,6 +2935,7 @@ "Proof": "Preuve", "Proof of exploitation": "Preuve d'exploitation", "Proofs": "Preuves", + "Protected": "Protégé", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Fournisseur", "providing": "Fournit", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "Non vérifié : Pas encore testé", "Up next": "À venir", "Update": "Modifier", + "Update a marking definition": "Mettre à jour une définition de marquage", "Update a notification rule": "Mise à jour d'une règle de notification", "Update action": "Action de mise à jour", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/it.json b/openaev-front/src/utils/lang/it.json index 32631fb4a6a..74f1bf12aeb 100644 --- a/openaev-front/src/utils/lang/it.json +++ b/openaev-front/src/utils/lang/it.json @@ -160,6 +160,7 @@ "Add 1 inject": "Aggiungi 1 inject", "Add a category": "Aggiungi una categoria", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Aggiungi una definizione di marcatura", "Add a new vulnerability": "Aggiungi una nuova vulnerabilità", "Add a result": "Aggiungi un risultato", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "Cambia logo", "Change tone": "Cambiare tono", "Change your password": "Cambiare la password", + "Changing order can impact precedence. Do you want to continue?": "La modifica dell'ordine può influire sulla precedenza. Vuoi continuare?", "Channel": "Canale", "channels": "Canali", "Channels": "Canali", @@ -704,6 +706,7 @@ "collectors": "collezionisti", "Collectors": "Collezionisti", "Color": "Colore", + "Color must be a valid hex value, e.g. #4CAF50": "Il colore deve essere un valore esadecimale valido, ad esempio #4CAF50", "Colors": "Colors", "Comcheck": "Controllo", "Comchecks": "Comcheck", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "Volete cancellare questa domanda sulle lezioni apprese?", "Do you want to delete this lessons learned template?": "Volete cancellare questo modello di lezione appresa?", "Do you want to delete this log?": "Volete cancellare questo registro?", + "Do you want to delete this marking definition?": "Vuoi eliminare questa definizione di marcatura?", "Do you want to delete this media pressure article?": "Volete cancellare questo articolo sulla pressione dei media?", "Do you want to delete this media pressure?": "Volete cancellare questa pressione mediatica?", "Do you want to delete this mitigation?": "Vuoi cancellare questa attenuazione?", @@ -2272,6 +2276,12 @@ "Mark as done": "Contrassegnare come fatto", "Mark as read": "Segna come letto", "Mark as unread": "Segna come non letto", + "Marking definitions": "Definizioni di marcatura", + "marking_definition_color": "Colore", + "marking_definition_created_at": "Data di creazione", + "marking_definition_definition": "Definizione", + "marking_definition_order": "Ordine", + "marking_definition_type": "Tipo", "Massive operations": "Operazioni massive", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "Corrisponde a ciò che vedono i destinatari", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Ordine", + "Order must be an integer": "L'ordine deve essere un numero intero", + "Order must be greater than or equal to 0": "L'ordine deve essere maggiore o uguale a 0", "Organisation": "Organizzazione", "Organization": "Organizzazione", "Organizations": "Organizzazioni", @@ -2923,6 +2935,7 @@ "Proof": "Prova", "Proof of exploitation": "Prova di sfruttamento", "Proofs": "Prove", + "Protected": "Protetto", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Fornitore", "providing": "Fornisce", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "Non verificato: Non ancora testato", "Up next": "In arrivo", "Update": "Aggiornamento", + "Update a marking definition": "Aggiorna una definizione di marcatura", "Update a notification rule": "Aggiornare una regola di notifica", "Update action": "Azione di aggiornamento", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/ja.json b/openaev-front/src/utils/lang/ja.json index 1680c06a3f6..1570fe4f1dc 100644 --- a/openaev-front/src/utils/lang/ja.json +++ b/openaev-front/src/utils/lang/ja.json @@ -160,6 +160,7 @@ "Add 1 inject": "インジェクトを1件追加", "Add a category": "カテゴリを追加", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "マーキング定義を追加", "Add a new vulnerability": "新しい脆弱性を追加する", "Add a result": "結果を追加する", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "ロゴ変更", "Change tone": "トーンの変更", "Change your password": "パスワードの変更", + "Changing order can impact precedence. Do you want to continue?": "順序を変更すると、優先順位に影響が出る可能性があります。続行しますか?", "Channel": "チャンネル", "channels": "チャンネル", "Channels": "チャンネル", @@ -704,6 +706,7 @@ "collectors": "コレクター", "Collectors": "コレクター", "Color": "カラー", + "Color must be a valid hex value, e.g. #4CAF50": "色は有効な16進数値である必要があります(例:#4CAF50)。", "Colors": "Colors", "Comcheck": "コムチェック", "Comchecks": "コムチェック", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "この教訓を生かした質問を削除しますか?", "Do you want to delete this lessons learned template?": "この学習済みテンプレートを削除しますか?", "Do you want to delete this log?": "このログを削除しますか?", + "Do you want to delete this marking definition?": "このマーキング定義を削除しますか?", "Do you want to delete this media pressure article?": "この報道圧力記事を削除しますか?", "Do you want to delete this media pressure?": "このメディア圧力を削除しますか?", "Do you want to delete this mitigation?": "この緩和策を削除しますか?", @@ -2272,6 +2276,12 @@ "Mark as done": "完了マーク", "Mark as read": "既読にする", "Mark as unread": "未読にする", + "Marking definitions": "マーキング定義", + "marking_definition_color": "色", + "marking_definition_created_at": "作成日", + "marking_definition_definition": "定義", + "marking_definition_order": "順序", + "marking_definition_type": "タイプ", "Massive operations": "大規模操作", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "受信者が見る内容と一致", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "オーダー", + "Order must be an integer": "順序は整数でなければなりません", + "Order must be greater than or equal to 0": "順序は 0 以上でなければなりません", "Organisation": "組織管理", "Organization": "組織名", "Organizations": "組織", @@ -2923,6 +2935,7 @@ "Proof": "証拠", "Proof of exploitation": "エクスプロイトの証拠", "Proofs": "証拠", + "Protected": "保護済み", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "プロバイダー", "providing": "提供中", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "未検証:未検証", "Up next": "次の実行", "Update": "更新", + "Update a marking definition": "マーキング定義を更新する", "Update a notification rule": "通知ルールの更新", "Update action": "更新アクション", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/ko.json b/openaev-front/src/utils/lang/ko.json index a35858c331c..a1cf4cb082a 100644 --- a/openaev-front/src/utils/lang/ko.json +++ b/openaev-front/src/utils/lang/ko.json @@ -160,6 +160,7 @@ "Add 1 inject": "인젝트 1개 추가", "Add a category": "카테고리 추가", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "표시 정의 추가", "Add a new vulnerability": "새로운 취약점 추가", "Add a result": "결과 추가", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "로고 변경", "Change tone": "톤 변경", "Change your password": "비밀번호 변경하기", + "Changing order can impact precedence. Do you want to continue?": "순서를 변경하면 우선순위에 영향을 줄 수 있습니다. 계속하시겠습니까?", "Channel": "채널", "channels": "채널", "Channels": "채널", @@ -704,6 +706,7 @@ "collectors": "수집자", "Collectors": "수집가", "Color": "색", + "Color must be a valid hex value, e.g. #4CAF50": "색상은 유효한 16진수 값이어야 합니다(예: #4CAF50).", "Colors": "Colors", "Comcheck": "컴체크", "Comchecks": "컴체크", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "이 교훈을 얻은 질문을 삭제하시겠습니까?", "Do you want to delete this lessons learned template?": "이 교훈 템플릿을 삭제하시겠습니까?", "Do you want to delete this log?": "이 로그를 삭제하시겠습니까?", + "Do you want to delete this marking definition?": "이 표시 정의를 삭제하시겠습니까?", "Do you want to delete this media pressure article?": "이 미디어 압박 기사를 삭제하시겠습니까?", "Do you want to delete this media pressure?": "이 미디어 압박을 삭제하시겠습니까?", "Do you want to delete this mitigation?": "이 완화 조치를 삭제하시겠습니까?", @@ -2272,6 +2276,12 @@ "Mark as done": "완료된 것으로 표시", "Mark as read": "읽음으로 표시", "Mark as unread": "읽지 않음으로 표시", + "Marking definitions": "표시 정의", + "marking_definition_color": "색상", + "marking_definition_created_at": "생성일", + "marking_definition_definition": "정의", + "marking_definition_order": "순서", + "marking_definition_type": "유형", "Massive operations": "대량 작업", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "수신자에게 보이는 내용과 일치", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "주문", + "Order must be an integer": "순서는 정수여야 합니다.", + "Order must be greater than or equal to 0": "순서는 0 이상이어야 합니다.", "Organisation": "조직 관리", "Organization": "조직", "Organizations": "조직", @@ -2923,6 +2935,7 @@ "Proof": "증거", "Proof of exploitation": "익스플로잇 증거", "Proofs": "증거", + "Protected": "보호됨", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "공급자", "providing": "제공 중", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "확인되지 않음: 아직 테스트되지 않음", "Up next": "다음 실행", "Update": "업데이트", + "Update a marking definition": "표시 정의 업데이트", "Update a notification rule": "알림 규칙 업데이트", "Update action": "업데이트 작업", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/ru.json b/openaev-front/src/utils/lang/ru.json index 272f46a2417..11251add91d 100644 --- a/openaev-front/src/utils/lang/ru.json +++ b/openaev-front/src/utils/lang/ru.json @@ -160,6 +160,7 @@ "Add 1 inject": "Добавить 1 инъекцию", "Add a category": "Добавить категорию", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "Добавить определение маркировки", "Add a new vulnerability": "Добавить новую уязвимость", "Add a result": "Добавить результат", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "Изменить логотип", "Change tone": "Изменить тон", "Change your password": "Изменить пароль", + "Changing order can impact precedence. Do you want to continue?": "Изменение порядка может повлиять на приоритет. Хотите продолжить?", "Channel": "Канал", "channels": "Каналы", "Channels": "Каналы", @@ -704,6 +706,7 @@ "collectors": "коллекционеры", "Collectors": "Коллекционеры", "Color": "Цвет", + "Color must be a valid hex value, e.g. #4CAF50": "Цвет должен быть действительным шестнадцатеричным значением, например #4CAF50", "Colors": "Colors", "Comcheck": "Comcheck", "Comchecks": "Comchecks", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "Хотите ли вы удалить этот вопрос об извлеченных уроках?", "Do you want to delete this lessons learned template?": "Хотите ли вы удалить этот шаблон уроков?", "Do you want to delete this log?": "Хотите ли вы удалить этот журнал?", + "Do you want to delete this marking definition?": "Хотите удалить это определение маркировки?", "Do you want to delete this media pressure article?": "Хотите ли вы удалить эту статью о медиадавлении?", "Do you want to delete this media pressure?": "Хотите ли вы удалить это медиадавление?", "Do you want to delete this mitigation?": "Хотите ли вы удалить это смягчение?", @@ -2272,6 +2276,12 @@ "Mark as done": "Отметить как сделанное", "Mark as read": "Отметить как прочитанное", "Mark as unread": "Отметить как непрочитанное", + "Marking definitions": "Определения маркировки", + "marking_definition_color": "Цвет", + "marking_definition_created_at": "Дата создания", + "marking_definition_definition": "Определение", + "marking_definition_order": "Порядок", + "marking_definition_type": "Тип", "Massive operations": "Массовые операции", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "Совпадает с тем, что видят получатели", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "Заказ", + "Order must be an integer": "Порядок должен быть целым числом", + "Order must be greater than or equal to 0": "Порядок должен быть не меньше 0", "Organisation": "Организация", "Organization": "Организация", "Organizations": "Организации", @@ -2923,6 +2935,7 @@ "Proof": "Доказательство", "Proof of exploitation": "Доказательство эксплуатации", "Proofs": "Доказательства", + "Protected": "Защищено", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Поставщик", "providing": "Определяет", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "Непроверенный: Еще не проверено", "Up next": "На очереди", "Update": "Обновить", + "Update a marking definition": "Обновить определение маркировки", "Update a notification rule": "Обновление правила уведомления", "Update action": "Действие обновления", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/lang/zh.json b/openaev-front/src/utils/lang/zh.json index a14ee03104d..1fa98cda6fe 100644 --- a/openaev-front/src/utils/lang/zh.json +++ b/openaev-front/src/utils/lang/zh.json @@ -160,6 +160,7 @@ "Add 1 inject": "添加 1 个注入", "Add a category": "添加类别", "Add a custom domain": "Add a custom domain", + "Add a marking definition": "添加标记定义", "Add a new vulnerability": "添加新的漏洞", "Add a result": "添加结果", "Add a schedule": "Add a schedule", @@ -632,6 +633,7 @@ "Change logo": "更改logo", "Change tone": "改变语气", "Change your password": "修改密码", + "Changing order can impact precedence. Do you want to continue?": "更改顺序可能会影响优先级。是否继续?", "Channel": "频道", "channels": "渠道", "Channels": "频道", @@ -704,6 +706,7 @@ "collectors": "收集商", "Collectors": "收集器", "Color": "颜色", + "Color must be a valid hex value, e.g. #4CAF50": "颜色必须是有效的十六进制值,例如 #4CAF50", "Colors": "Colors", "Comcheck": "通信检查", "Comchecks": "检查", @@ -1230,6 +1233,7 @@ "Do you want to delete this lessons learned question?": "你想要删除这个经验教训问题么?", "Do you want to delete this lessons learned template?": "你想要删除这个经验教训模板么?", "Do you want to delete this log?": "你想要删除这个日志么?", + "Do you want to delete this marking definition?": "是否要删除此标记定义?", "Do you want to delete this media pressure article?": "你想删除这篇媒体压力文章吗?", "Do you want to delete this media pressure?": "你想要删除这个媒体么?", "Do you want to delete this mitigation?": "是否要刪除此緩解措施?", @@ -2272,6 +2276,12 @@ "Mark as done": "标记已完成", "Mark as read": "标记为已读", "Mark as unread": "标记为未读", + "Marking definitions": "标记定义", + "marking_definition_color": "颜色", + "marking_definition_created_at": "创建日期", + "marking_definition_definition": "定义", + "marking_definition_order": "顺序", + "marking_definition_type": "类型", "Massive operations": "批量操作", "Match a specific brand": "Match a specific brand", "Matches what recipients see": "与收件人看到的内容一致", @@ -2728,6 +2738,8 @@ "Or type your own answer": "Or type your own answer", "Orchestrator": "Orchestrator", "Order": "订阅", + "Order must be an integer": "顺序必须是整数", + "Order must be greater than or equal to 0": "顺序必须大于或等于 0", "Organisation": "组织管理", "Organization": "组织", "Organizations": "组织", @@ -2923,6 +2935,7 @@ "Proof": "证据", "Proof of exploitation": "利用证据", "Proofs": "证据", + "Protected": "受保护", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "提供商", "providing": "提供", @@ -3874,6 +3887,7 @@ "Unverified: Not yet tested": "未验证:尚未测试", "Up next": "即将执行", "Update": "更新", + "Update a marking definition": "更新标记定义", "Update a notification rule": "更新通知规则", "Update action": "更新操作", "Update an asset": "Update an asset", diff --git a/openaev-front/src/utils/permissions/types.ts b/openaev-front/src/utils/permissions/types.ts index 683c13b2c15..0021f29e8c0 100644 --- a/openaev-front/src/utils/permissions/types.ts +++ b/openaev-front/src/utils/permissions/types.ts @@ -23,6 +23,8 @@ export const SUBJECTS = { TEAMS_AND_PLAYERS: 'TEAMS_AND_PLAYERS', ASSETS: 'ASSETS', CREDENTIALS: 'CREDENTIALS', + MARKING_DEFINITION: 'MARKING_DEFINITION', + MARKING_ASSIGNMENT: 'MARKING_ASSIGNMENT', THREAT_ARSENALS: 'THREAT_ARSENALS', DASHBOARDS: 'DASHBOARDS', REPORTINGS: 'REPORTINGS', diff --git a/openaev-model/src/main/java/io/openaev/database/model/Capability.java b/openaev-model/src/main/java/io/openaev/database/model/Capability.java index 9d88b916ae7..ba1a4a38b32 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/Capability.java +++ b/openaev-model/src/main/java/io/openaev/database/model/Capability.java @@ -163,6 +163,33 @@ public enum Capability { pair(ResourceType.CREDENTIAL, Action.DUPLICATE)), DELETE_CREDENTIALS(MANAGE_CREDENTIALS, pair(ResourceType.CREDENTIAL, Action.DELETE)), + // Marking definitions + ACCESS_MARKING_DEFINITION( + null, + CapabilityGroup.MARKING, + EnumSet.of(CapabilityScope.TENANT), + pair(ResourceType.MARKING_DEFINITION, Action.READ), + pair(ResourceType.MARKING_DEFINITION, Action.SEARCH)), + MANAGE_MARKING_DEFINITION( + ACCESS_MARKING_DEFINITION, + pair(ResourceType.MARKING_DEFINITION, Action.WRITE), + pair(ResourceType.MARKING_DEFINITION, Action.CREATE)), + DELETE_MARKING_DEFINITION( + MANAGE_MARKING_DEFINITION, pair(ResourceType.MARKING_DEFINITION, Action.DELETE)), + + // Marking assignment + ACCESS_MARKING_ASSIGNMENT( + null, + CapabilityGroup.MARKING, + EnumSet.of(CapabilityScope.TENANT), + pair(ResourceType.MARKING_ASSIGNMENT, Action.READ), + pair(ResourceType.MARKING_ASSIGNMENT, Action.SEARCH)), + ASSIGN_MARKING( + ACCESS_MARKING_ASSIGNMENT, + pair(ResourceType.MARKING_ASSIGNMENT, Action.WRITE), + pair(ResourceType.MARKING_ASSIGNMENT, Action.CREATE)), + DELETE_MARKING_ASSIGNMENT(ASSIGN_MARKING, pair(ResourceType.MARKING_ASSIGNMENT, Action.DELETE)), + // Dashboards ACCESS_DASHBOARDS( null, diff --git a/openaev-model/src/main/java/io/openaev/database/model/CapabilityGroup.java b/openaev-model/src/main/java/io/openaev/database/model/CapabilityGroup.java index 3d5f68f9e87..90291539983 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/CapabilityGroup.java +++ b/openaev-model/src/main/java/io/openaev/database/model/CapabilityGroup.java @@ -5,6 +5,7 @@ public enum CapabilityGroup { SUPERUSER, ASSESSMENT, CREDENTIALS, + MARKING, TARGETS, THREAT_ARSENALS, DASHBOARDS, diff --git a/openaev-model/src/main/java/io/openaev/database/model/MarkingDefinition.java b/openaev-model/src/main/java/io/openaev/database/model/MarkingDefinition.java new file mode 100644 index 00000000000..cf505987de1 --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/database/model/MarkingDefinition.java @@ -0,0 +1,112 @@ +package io.openaev.database.model; + +import static java.time.Instant.now; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.annotation.ControlledUuidGeneration; +import io.openaev.annotation.Queryable; +import io.openaev.database.audit.ModelBaseListener; +import io.openaev.database.audit.TenantBaseListener; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; +import java.util.Objects; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +@Getter +@Setter +@Entity +@Table(name = "marking_definitions") +@EntityListeners({ModelBaseListener.class, TenantBaseListener.class}) +// marking_definitions is a tenant-v2 active table (inspector + can_access_tenant), so no v1 +// @Filter. +public class MarkingDefinition implements TenantBase { + + @Id + @ControlledUuidGeneration + @Column(name = "marking_definition_id") + @JsonProperty("marking_definition_id") + @NotBlank + private String id; + + @Queryable(filterable = true, searchable = true, sortable = true) + @Column(name = "marking_definition_type", nullable = false) + @JsonProperty("marking_definition_type") + @NotBlank + private String type; + + @Queryable(filterable = true, searchable = true, sortable = true) + @Column(name = "marking_definition_definition", nullable = false) + @JsonProperty("marking_definition_definition") + @NotBlank + private String definition; + + @Queryable(filterable = true, sortable = true) + @Column(name = "marking_definition_color") + @JsonProperty("marking_definition_color") + private String color; + + @Queryable(filterable = true, sortable = true) + @Column(name = "marking_definition_order", nullable = false) + @JsonProperty("marking_definition_order") + @NotNull + @Min(0) + private Integer order = 0; + + @Column(name = "marking_definition_protected", nullable = false) + @JsonProperty("marking_definition_protected") + @NotNull + private Boolean protectedDefinition = false; + + @ManyToOne + @JoinColumn(name = "tenant_id", updatable = false, nullable = false) + @JsonIgnore + private Tenant tenant; + + @Queryable(filterable = true, sortable = true) + @Column(name = "marking_definition_created_at", nullable = false) + @JsonProperty("marking_definition_created_at") + @NotNull + @CreationTimestamp + private Instant createdAt = now(); + + @Column(name = "marking_definition_updated_at", nullable = false) + @JsonProperty("marking_definition_updated_at") + @NotNull + @UpdateTimestamp + private Instant updatedAt = now(); + + @Getter(onMethod_ = @JsonIgnore) + @Transient + private final ResourceType resourceType = ResourceType.MARKING_DEFINITION; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !Base.class.isAssignableFrom(o.getClass())) { + return false; + } + Base base = (Base) o; + return id.equals(base.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java b/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java index 2844f9229c8..6c89878a679 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java +++ b/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java @@ -21,6 +21,8 @@ public enum ResourceType { RESOURCE_TYPE, SECURITY_PLATFORM, CREDENTIAL, + MARKING_DEFINITION, + MARKING_ASSIGNMENT, DOCUMENT, CHANNEL, PHISHING_LANDING_PAGE, diff --git a/openaev-model/src/main/java/io/openaev/database/repository/MarkingDefinitionRepository.java b/openaev-model/src/main/java/io/openaev/database/repository/MarkingDefinitionRepository.java new file mode 100644 index 00000000000..105378d0a90 --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/database/repository/MarkingDefinitionRepository.java @@ -0,0 +1,28 @@ +package io.openaev.database.repository; + +import io.openaev.database.model.MarkingDefinition; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface MarkingDefinitionRepository + extends JpaRepository, JpaSpecificationExecutor { + + @Query( + """ + SELECT (count(md) > 0) + FROM MarkingDefinition md + WHERE lower(md.type) = lower(:type) + AND lower(md.definition) = lower(:definition) + AND md.tenant.id = :tenantId + AND (:ignoredId IS NULL OR md.id <> :ignoredId) + """) + boolean existsByTypeAndDefinitionAndTenantIdExcludingId( + @Param("type") String type, + @Param("definition") String definition, + @Param("tenantId") String tenantId, + @Param("ignoredId") String ignoredId); +} From 1a43e43b14344551432f6531abbf79b1d4c32f40 Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Fri, 28 Aug 2026 09:05:26 +0200 Subject: [PATCH 4/6] [frontend] translations and new dialog --- .../MarkingDefinitionPopover.tsx | 41 +++++++++++++++---- openaev-front/src/utils/lang/de.json | 11 +++++ openaev-front/src/utils/lang/en.json | 11 +++++ openaev-front/src/utils/lang/es.json | 11 +++++ openaev-front/src/utils/lang/fr.json | 11 +++++ openaev-front/src/utils/lang/it.json | 11 +++++ openaev-front/src/utils/lang/ja.json | 11 +++++ openaev-front/src/utils/lang/ko.json | 11 +++++ openaev-front/src/utils/lang/ru.json | 11 +++++ openaev-front/src/utils/lang/zh.json | 11 +++++ 10 files changed, 133 insertions(+), 7 deletions(-) diff --git a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx index 1690a7828cb..37d84dc285b 100644 --- a/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx +++ b/openaev-front/src/admin/components/settings/marking_definitions/MarkingDefinitionPopover.tsx @@ -5,6 +5,7 @@ import { updateMarkingDefinition, } from '../../../../actions/marking_definitions/marking-definition-actions'; import ButtonPopover, { type PopoverEntry } from '../../../../components/common/ButtonPopover'; +import DialogConfirmation from '../../../../components/common/DialogConfirmation'; import DialogDelete from '../../../../components/common/DialogDelete'; import Drawer from '../../../../components/common/Drawer'; import { useFormatter } from '../../../../components/i18n'; @@ -40,6 +41,8 @@ const MarkingDefinitionPopover: FunctionComponent = ({ const [openUpdate, setOpenUpdate] = useState(false); const [openDelete, setOpenDelete] = useState(false); + const [openOrderConfirm, setOpenOrderConfirm] = useState(false); + const [pendingUpdateInput, setPendingUpdateInput] = useState(null); const isProtected = markingDefinition.marking_definition_protected; @@ -51,13 +54,7 @@ const MarkingDefinitionPopover: FunctionComponent = ({ marking_definition_order: value.marking_definition_order, }); - const submitUpdate = (input: MarkingDefinitionInput) => { - if (input.marking_definition_order !== markingDefinition.marking_definition_order) { - const confirmed = window.confirm(t('Changing order can impact precedence. Do you want to continue?')); - if (!confirmed) { - return Promise.resolve(); - } - } + const performUpdate = (input: MarkingDefinitionInput) => { return dispatch(updateMarkingDefinition(markingDefinition.marking_definition_id, input)) .then((result: MarkingDefinitionStoreResult) => { const updatedMarkingDefinition = extractMarkingDefinitionFromStoreResult(result); @@ -70,6 +67,26 @@ const MarkingDefinitionPopover: FunctionComponent = ({ .catch((error: unknown) => error); }; + const submitUpdate = (input: MarkingDefinitionInput) => { + if (input.marking_definition_order !== markingDefinition.marking_definition_order) { + setPendingUpdateInput(input); + setOpenOrderConfirm(true); + return Promise.resolve(); + } + return performUpdate(input); + }; + + const confirmOrderUpdate = () => { + if (!pendingUpdateInput) { + setOpenOrderConfirm(false); + return Promise.resolve(); + } + return performUpdate(pendingUpdateInput).finally(() => { + setOpenOrderConfirm(false); + setPendingUpdateInput(null); + }); + }; + const submitDelete = () => { return dispatch(deleteMarkingDefinition(markingDefinition.marking_definition_id)) .then(() => { @@ -108,6 +125,16 @@ const MarkingDefinitionPopover: FunctionComponent = ({ handleSubmit={submitDelete} text={t('Do you want to delete this marking definition?')} /> + { + setOpenOrderConfirm(false); + setPendingUpdateInput(null); + }} + handleSubmit={confirmOrderUpdate} + text={t('Changing order can impact precedence. Do you want to continue?')} + submitLabel={t('Update')} + /> ); }; diff --git a/openaev-front/src/utils/lang/de.json b/openaev-front/src/utils/lang/de.json index 0e05e65bb16..d1dc59635fc 100644 --- a/openaev-front/src/utils/lang/de.json +++ b/openaev-front/src/utils/lang/de.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "Ein Dokument, das als Logo einer Sicherheitsplattform verwendet wird, kann nicht gelöscht werden.", "A document used in a payload can't be deleted.": "Ein Dokument, das in einer Nutzlast verwendet wird, kann nicht gelöscht werden.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "Es existiert bereits eine Markierungsdefinition mit demselben Typ und derselben Definition", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "Ein erheblicher Teil der Validierungen kam durch - eine Überprüfung lohnt sich.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "Eine Voraussetzungsprüfung ist fehlgeschlagen, bevor der Hauptbefehl ausgeführt werden konnte. Überprüfen Sie die Abhängigkeiten und stellen Sie sicher, dass sie auf dem Ziel erfüllt sind.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Zugriff auf Dokumente", "ACCESS_FINDINGS": "Zugriff auf Befunde", "ACCESS_LESSONS_LEARNED": "Zugang zu gelernten Lektionen", + "ACCESS_MARKING_ASSIGNMENT": "Zugriffsmarkierung zuweisen", + "ACCESS_MARKING_DEFINITION": "Zugriffsmarkierung definieren", "ACCESS_PAYLOADS": "Auf Payloads zugreifen", "ACCESS_PHISHING": "Zugriff auf Phishing", "ACCESS_PLATFORM_SETTINGS": "Zugriff auf Plattformeinstellungen", @@ -401,6 +404,7 @@ "AssetGroup": "AssetGroup", "AssetGroups": "AssetGroups", "Assets": "Assets", + "ASSIGN_MARKING": "Markierung zuweisen", "Assistant": "Assistent", "Associated file": "Zugehörige Datei", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Dashboards löschen", "DELETE_DOCUMENTS": "Dokumente löschen", "DELETE_LESSONS_LEARNED": "Gelernte Lektionen löschen", + "DELETE_MARKING_ASSIGNMENT": "Markierungszuweisung löschen", + "DELETE_MARKING_DEFINITION": "Markierungsdefinition löschen", "DELETE_PAYLOADS": "Payloads löschen", "DELETE_PHISHING": "Phishing löschen", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Plattformgruppen und Rollen löschen", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Dashboards verwalten", "MANAGE_DOCUMENTS": "Verwalten von Dokumenten", "MANAGE_LESSONS_LEARNED": "Lessons Learned verwalten", + "MANAGE_MARKING_DEFINITION": "Markierungsdefinition verwalten", "MANAGE_PAYLOADS": "Nutzdaten verwalten", "MANAGE_PHISHING": "Phishing verwalten", "MANAGE_PLATFORM_SESSIONS": "Plattform-Sitzungen verwalten", @@ -2276,6 +2283,8 @@ "Mark as done": "Markieren als erledigt", "Mark as read": "Als gelesen markieren", "Mark as unread": "Als ungelesen markieren", + "MARKING": "Markierung", + "Marking definition type is immutable": "Der Typ einer Markierungsdefinition ist unveränderlich", "Marking definitions": "Markierungsdefinitionen", "marking_definition_color": "Farbe", "marking_definition_created_at": "Erstellungsdatum", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Nachweis der Ausnutzung", "Proofs": "Nachweise", "Protected": "Geschützt", + "Protected marking definitions cannot be deleted": "Geschützte Markierungsdefinitionen können nicht gelöscht werden", + "Protected marking definitions cannot be updated": "Geschützte Markierungsdefinitionen können nicht aktualisiert werden", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Anbieter", "providing": "Bereitstellung", diff --git a/openaev-front/src/utils/lang/en.json b/openaev-front/src/utils/lang/en.json index 336d1210e76..e3fb39f98c2 100644 --- a/openaev-front/src/utils/lang/en.json +++ b/openaev-front/src/utils/lang/en.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "A document used as a security platform logo can't be deleted.", "A document used in a payload can't be deleted.": "A document used in a payload can't be deleted.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "A marking definition with the same type and definition already exists", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "A meaningful share of validations got through - worth reviewing.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Access documents", "ACCESS_FINDINGS": "Access findings", "ACCESS_LESSONS_LEARNED": "Access lessons learned", + "ACCESS_MARKING_ASSIGNMENT": "Access marking assignment", + "ACCESS_MARKING_DEFINITION": "Access marking definition", "ACCESS_PAYLOADS": "Access payloads", "ACCESS_PHISHING": "Access phishing", "ACCESS_PLATFORM_SETTINGS": "Access platform settings", @@ -401,6 +404,7 @@ "AssetGroup": "AssetGroup", "AssetGroups": "AssetGroups", "Assets": "Assets", + "ASSIGN_MARKING": "Assign marking", "Assistant": "Assistant", "Associated file": "Associated file", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Delete dashboards", "DELETE_DOCUMENTS": "Delete documents", "DELETE_LESSONS_LEARNED": "Delete lessons learned", + "DELETE_MARKING_ASSIGNMENT": "Delete marking assignment", + "DELETE_MARKING_DEFINITION": "Delete marking definition", "DELETE_PAYLOADS": "Delete payloads", "DELETE_PHISHING": "Delete phishing", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Delete platform users, groups and roles", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Manage dashboards", "MANAGE_DOCUMENTS": "Manage documents", "MANAGE_LESSONS_LEARNED": "Manage lessons learned", + "MANAGE_MARKING_DEFINITION": "Manage marking definition", "MANAGE_PAYLOADS": "Manage payloads", "MANAGE_PHISHING": "Manage phishing", "MANAGE_PLATFORM_SESSIONS": "Manage platform sessions", @@ -2276,6 +2283,8 @@ "Mark as done": "Mark as done", "Mark as read": "Mark as read", "Mark as unread": "Mark as unread", + "MARKING": "Marking", + "Marking definition type is immutable": "Marking definition type is immutable", "Marking definitions": "Marking definitions", "marking_definition_color": "Color", "marking_definition_created_at": "Creation date", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Proof of exploitation", "Proofs": "Proofs", "Protected": "Protected", + "Protected marking definitions cannot be deleted": "Protected marking definitions cannot be deleted", + "Protected marking definitions cannot be updated": "Protected marking definitions cannot be updated", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Provider", "providing": "Providing", diff --git a/openaev-front/src/utils/lang/es.json b/openaev-front/src/utils/lang/es.json index ade0cc9c006..7be551d7830 100644 --- a/openaev-front/src/utils/lang/es.json +++ b/openaev-front/src/utils/lang/es.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "Un documento utilizado como logotipo de una plataforma de seguridad no puede eliminarse.", "A document used in a payload can't be deleted.": "Un documento utilizado en una carga útil no puede eliminarse.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "Ya existe una definición de marca con el mismo tipo y la misma definición", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "Una parte significativa de las validaciones logró pasar - conviene revisarlo.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "Una verificación de requisito previo falló antes de ejecutar el comando principal. Revise las dependencias y asegúrese de que se cumplan en el objetivo.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Acceso a documentos", "ACCESS_FINDINGS": "Acceder a conclusiones", "ACCESS_LESSONS_LEARNED": "Acceso a las lecciones aprendidas", + "ACCESS_MARKING_ASSIGNMENT": "Asignación de calificaciones de acceso", + "ACCESS_MARKING_DEFINITION": "Definición de calificaciones de acceso", "ACCESS_PAYLOADS": "Acceder a las cargas útiles", "ACCESS_PHISHING": "Acceder al phishing", "ACCESS_PLATFORM_SETTINGS": "Acceder a la configuración de la plataforma", @@ -401,6 +404,7 @@ "AssetGroup": "AssetGroup", "AssetGroups": "AssetGroups", "Assets": "Activos", + "ASSIGN_MARKING": "Asignar calificación", "Assistant": "Asistente", "Associated file": "Archivo asociado", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Borrar cuadros de mando", "DELETE_DOCUMENTS": "Borrar documentos", "DELETE_LESSONS_LEARNED": "Borrar lecciones aprendidas", + "DELETE_MARKING_ASSIGNMENT": "Eliminar asignación de calificaciones", + "DELETE_MARKING_DEFINITION": "Eliminar definición de calificaciones", "DELETE_PAYLOADS": "Eliminar cargas útiles", "DELETE_PHISHING": "Eliminar phishing", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Borrar usuarios, grupos y roles de plataforma", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Gestionar cuadros de mando", "MANAGE_DOCUMENTS": "Gestionar documentos", "MANAGE_LESSONS_LEARNED": "Gestionar las lecciones aprendidas", + "MANAGE_MARKING_DEFINITION": "Gestionar definición de calificaciones", "MANAGE_PAYLOADS": "Gestionar cargas útiles", "MANAGE_PHISHING": "Gestionar phishing", "MANAGE_PLATFORM_SESSIONS": "Gestionar las sesiones de la plataforma", @@ -2276,6 +2283,8 @@ "Mark as done": "Marcar como hecho", "Mark as read": "Marcar como leido", "Mark as unread": "Marcar como no leido", + "MARKING": "Calificación", + "Marking definition type is immutable": "El tipo de la definición de marca es inmutable", "Marking definitions": "Definiciones de marcado", "marking_definition_color": "Color", "marking_definition_created_at": "Fecha de creacion", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Prueba de explotación", "Proofs": "Pruebas", "Protected": "Protegido", + "Protected marking definitions cannot be deleted": "Las definiciones de marca protegidas no se pueden eliminar", + "Protected marking definitions cannot be updated": "Las definiciones de marca protegidas no se pueden actualizar", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Proveedor", "providing": "Proporciona", diff --git a/openaev-front/src/utils/lang/fr.json b/openaev-front/src/utils/lang/fr.json index bf1afd30170..a34933d40bb 100644 --- a/openaev-front/src/utils/lang/fr.json +++ b/openaev-front/src/utils/lang/fr.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "Un document utilisé comme logo d'une plateforme de sécurité ne peut pas être supprimé.", "A document used in a payload can't be deleted.": "Un document utilisé dans une charge utile ne peut pas être supprimé.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "Une définition de marquage de même type et de même contenu existe déjà", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "Une part significative des validations est passée - à examiner.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "Un prérequis a échoué avant l'exécution de la commande principale. Vérifiez les dépendances et assurez-vous qu'elles sont satisfaites sur la cible.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Accéder aux documents", "ACCESS_FINDINGS": "Accéder aux findings", "ACCESS_LESSONS_LEARNED": "Accéder aux leçons apprises", + "ACCESS_MARKING_ASSIGNMENT": "Attribution d'une note d'accès", + "ACCESS_MARKING_DEFINITION": "Définition d'une note d'accès", "ACCESS_PAYLOADS": "Accéder aux charges utiles", "ACCESS_PHISHING": "Accéder au phishing", "ACCESS_PLATFORM_SETTINGS": "Accéder aux paramètres de la plate-forme", @@ -401,6 +404,7 @@ "AssetGroup": "Groupe d'assets", "AssetGroups": "Groupes d'assets", "Assets": "Actifs", + "ASSIGN_MARKING": "Attribuer une note", "Assistant": "Assistant", "Associated file": "Fichier associé", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Supprimer les tableaux de bord", "DELETE_DOCUMENTS": "Supprimer des documents", "DELETE_LESSONS_LEARNED": "Supprimer les leçons apprises", + "DELETE_MARKING_ASSIGNMENT": "Supprimer l'attribution d'une note", + "DELETE_MARKING_DEFINITION": "Supprimer la définition d'une note", "DELETE_PAYLOADS": "Supprimer les charges utiles", "DELETE_PHISHING": "Supprimer le phishing", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Supprimer les groupes et les rôles de la plate-forme", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Gérer les tableaux de bord", "MANAGE_DOCUMENTS": "Gérer les documents", "MANAGE_LESSONS_LEARNED": "Gérer les leçons apprises", + "MANAGE_MARKING_DEFINITION": "Gérer la définition d'une note", "MANAGE_PAYLOADS": "Gérer les charges utiles", "MANAGE_PHISHING": "Gérer le phishing", "MANAGE_PLATFORM_SESSIONS": "Gérer les sessions de la plate-forme", @@ -2276,6 +2283,8 @@ "Mark as done": "Marquer comme fait", "Mark as read": "Marquer comme lu", "Mark as unread": "Marquer comme non lu", + "MARKING": "Note", + "Marking definition type is immutable": "Le type d'une définition de marquage est immuable", "Marking definitions": "Définitions de marquage", "marking_definition_color": "Couleur", "marking_definition_created_at": "Date de creation", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Preuve d'exploitation", "Proofs": "Preuves", "Protected": "Protégé", + "Protected marking definitions cannot be deleted": "Les définitions de marquage protégées ne peuvent pas être supprimées", + "Protected marking definitions cannot be updated": "Les définitions de marquage protégées ne peuvent pas être mises à jour", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Fournisseur", "providing": "Fournit", diff --git a/openaev-front/src/utils/lang/it.json b/openaev-front/src/utils/lang/it.json index 74f1bf12aeb..7b3851c019b 100644 --- a/openaev-front/src/utils/lang/it.json +++ b/openaev-front/src/utils/lang/it.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "Un documento usato come logo di una piattaforma di sicurezza non può essere cancellato.", "A document used in a payload can't be deleted.": "Un documento usato in un payload non può essere cancellato.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "Esiste già una definizione di marcatura con lo stesso tipo e la stessa definizione", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "Una parte significativa delle convalide è passata - da esaminare.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Accesso ai documenti", "ACCESS_FINDINGS": "Accesso ai risultati", "ACCESS_LESSONS_LEARNED": "Accesso alle lezioni apprese", + "ACCESS_MARKING_ASSIGNMENT": "Assegnazione dei contrassegni di accesso", + "ACCESS_MARKING_DEFINITION": "Definizione dei contrassegni di accesso", "ACCESS_PAYLOADS": "Accedi ai payload", "ACCESS_PHISHING": "Accesso al phishing", "ACCESS_PLATFORM_SETTINGS": "Accesso alle impostazioni della piattaforma", @@ -401,6 +404,7 @@ "AssetGroup": "Gruppo di attività", "AssetGroups": "Gruppi di attività", "Assets": "Attività", + "ASSIGN_MARKING": "Assegna contrassegno", "Assistant": "Assistente", "Associated file": "File associato", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Eliminare i cruscotti", "DELETE_DOCUMENTS": "Eliminare i documenti", "DELETE_LESSONS_LEARNED": "Cancellare le lezioni apprese", + "DELETE_MARKING_ASSIGNMENT": "Elimina assegnazione contrassegno", + "DELETE_MARKING_DEFINITION": "Elimina definizione contrassegno", "DELETE_PAYLOADS": "Elimina i payload", "DELETE_PHISHING": "Elimina Phishing", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Eliminare gli utenti, i gruppi e i ruoli della piattaforma", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Gestire i cruscotti", "MANAGE_DOCUMENTS": "Gestire i documenti", "MANAGE_LESSONS_LEARNED": "Gestire le lezioni apprese", + "MANAGE_MARKING_DEFINITION": "Gestisci definizione contrassegno", "MANAGE_PAYLOADS": "Gestisci i payload", "MANAGE_PHISHING": "Gestisci Phishing", "MANAGE_PLATFORM_SESSIONS": "Gestire le sessioni della piattaforma", @@ -2276,6 +2283,8 @@ "Mark as done": "Contrassegnare come fatto", "Mark as read": "Segna come letto", "Mark as unread": "Segna come non letto", + "MARKING": "Contrassegno", + "Marking definition type is immutable": "Il tipo della definizione di marcatura è immutabile", "Marking definitions": "Definizioni di marcatura", "marking_definition_color": "Colore", "marking_definition_created_at": "Data di creazione", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Prova di sfruttamento", "Proofs": "Prove", "Protected": "Protetto", + "Protected marking definitions cannot be deleted": "Le definizioni di marcatura protette non possono essere eliminate", + "Protected marking definitions cannot be updated": "Le definizioni di marcatura protette non possono essere aggiornate", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Fornitore", "providing": "Fornisce", diff --git a/openaev-front/src/utils/lang/ja.json b/openaev-front/src/utils/lang/ja.json index 1570fe4f1dc..bd90aaf9672 100644 --- a/openaev-front/src/utils/lang/ja.json +++ b/openaev-front/src/utils/lang/ja.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "セキュリティプラットフォームのロゴとして使用されているドキュメントは削除できません。", "A document used in a payload can't be deleted.": "ペイロードで使用されたドキュメントは削除できません。", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "同じ型および定義を持つマーキング定義がすでに存在します", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "検証のかなりの部分が突破されました - 確認をお勧めします。", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "メインコマンドの実行前に前提条件チェックが失敗しました。前提条件の依存関係を確認し、ターゲット上で満たされていることを確認してください。", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "ドキュメントへのアクセス", "ACCESS_FINDINGS": "所見へのアクセス", "ACCESS_LESSONS_LEARNED": "学んだ教訓にアクセスする", + "ACCESS_MARKING_ASSIGNMENT": "アクセスマーキングの割り当て", + "ACCESS_MARKING_DEFINITION": "アクセスマーキングの定義", "ACCESS_PAYLOADS": "ペイロードへのアクセス", "ACCESS_PHISHING": "フィッシングへのアクセス", "ACCESS_PLATFORM_SETTINGS": "プラットフォーム設定へのアクセス", @@ -401,6 +404,7 @@ "AssetGroup": "アセットグループ", "AssetGroups": "アセットグループ", "Assets": "資産", + "ASSIGN_MARKING": "マーキングの割り当て", "Assistant": "アシスタント", "Associated file": "関連ファイル", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "ダッシュボードを削除する", "DELETE_DOCUMENTS": "ドキュメントの削除", "DELETE_LESSONS_LEARNED": "♪ 教訓を削除する ♪", + "DELETE_MARKING_ASSIGNMENT": "マーキングの割り当てを削除", + "DELETE_MARKING_DEFINITION": "マーキングの定義を削除", "DELETE_PAYLOADS": "ペイロードの削除", "DELETE_PHISHING": "フィッシングの削除", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "プラットフォームグループとロールの削除", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "ダッシュボードの管理", "MANAGE_DOCUMENTS": "ドキュメントの管理", "MANAGE_LESSONS_LEARNED": "教訓の管理", + "MANAGE_MARKING_DEFINITION": "マーキングの定義を管理", "MANAGE_PAYLOADS": "ペイロードの管理", "MANAGE_PHISHING": "フィッシングの管理", "MANAGE_PLATFORM_SESSIONS": "プラットフォーム・セッションの管理", @@ -2276,6 +2283,8 @@ "Mark as done": "完了マーク", "Mark as read": "既読にする", "Mark as unread": "未読にする", + "MARKING": "マーキング", + "Marking definition type is immutable": "マーキング定義の型は不変です", "Marking definitions": "マーキング定義", "marking_definition_color": "色", "marking_definition_created_at": "作成日", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "エクスプロイトの証拠", "Proofs": "証拠", "Protected": "保護済み", + "Protected marking definitions cannot be deleted": "保護されたマーキング定義は削除できません", + "Protected marking definitions cannot be updated": "保護されたマーキング定義は更新できません", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "プロバイダー", "providing": "提供中", diff --git a/openaev-front/src/utils/lang/ko.json b/openaev-front/src/utils/lang/ko.json index a1cf4cb082a..9464bf72d87 100644 --- a/openaev-front/src/utils/lang/ko.json +++ b/openaev-front/src/utils/lang/ko.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "보안 플랫폼 로고로 사용되는 문서는 삭제할 수 없습니다.", "A document used in a payload can't be deleted.": "페이로드에 사용된 문서는 삭제할 수 없습니다.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "동일한 유형과 정의를 가진 마킹 정의가 이미 존재합니다.", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "상당수의 검증이 통과되었습니다 - 검토가 필요합니다.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "문서에 액세스", "ACCESS_FINDINGS": "결과 액세스", "ACCESS_LESSONS_LEARNED": "배운 교훈에 액세스", + "ACCESS_MARKING_ASSIGNMENT": "액세스 마킹 할당", + "ACCESS_MARKING_DEFINITION": "액세스 마킹 정의", "ACCESS_PAYLOADS": "페이로드 액세스", "ACCESS_PHISHING": "피싱 액세스", "ACCESS_PLATFORM_SETTINGS": "플랫폼 설정에 액세스", @@ -401,6 +404,7 @@ "AssetGroup": "자산 그룹", "AssetGroups": "에셋 그룹", "Assets": "자산", + "ASSIGN_MARKING": "마킹 할당", "Assistant": "어시스턴트", "Associated file": "관련 파일", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "대시보드 삭제", "DELETE_DOCUMENTS": "문서 삭제", "DELETE_LESSONS_LEARNED": "배운 교훈 삭제", + "DELETE_MARKING_ASSIGNMENT": "마킹 할당 삭제", + "DELETE_MARKING_DEFINITION": "마킹 정의 삭제", "DELETE_PAYLOADS": "페이로드 삭제", "DELETE_PHISHING": "피싱 삭제", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "플랫폼 그룹 및 역할 삭제", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "대시보드 관리", "MANAGE_DOCUMENTS": "문서 관리", "MANAGE_LESSONS_LEARNED": "배운 교훈 관리", + "MANAGE_MARKING_DEFINITION": "마킹 정의 관리", "MANAGE_PAYLOADS": "페이로드 관리", "MANAGE_PHISHING": "피싱 관리", "MANAGE_PLATFORM_SESSIONS": "플랫폼 세션 관리", @@ -2276,6 +2283,8 @@ "Mark as done": "완료된 것으로 표시", "Mark as read": "읽음으로 표시", "Mark as unread": "읽지 않음으로 표시", + "MARKING": "마킹", + "Marking definition type is immutable": "마킹 정의 유형은 불변입니다.", "Marking definitions": "표시 정의", "marking_definition_color": "색상", "marking_definition_created_at": "생성일", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "익스플로잇 증거", "Proofs": "증거", "Protected": "보호됨", + "Protected marking definitions cannot be deleted": "보호된 마킹 정의는 삭제할 수 없습니다.", + "Protected marking definitions cannot be updated": "보호된 마킹 정의는 업데이트할 수 없습니다.", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "공급자", "providing": "제공 중", diff --git a/openaev-front/src/utils/lang/ru.json b/openaev-front/src/utils/lang/ru.json index 11251add91d..6c1bfb5dee8 100644 --- a/openaev-front/src/utils/lang/ru.json +++ b/openaev-front/src/utils/lang/ru.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "Документ, используемый как логотип платформы безопасности, не может быть удален.", "A document used in a payload can't be deleted.": "Документ, используемый в полезной нагрузке, не может быть удален.", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "Уже существует определение метки с таким же типом и содержанием", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "Значительная часть проверок прошла - стоит проверить.", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "Доступ к документам", "ACCESS_FINDINGS": "Доступ к выводам", "ACCESS_LESSONS_LEARNED": "Доступ к извлеченным урокам", + "ACCESS_MARKING_ASSIGNMENT": "Назначение метки доступа", + "ACCESS_MARKING_DEFINITION": "Определение метки доступа", "ACCESS_PAYLOADS": "Доступ к полезным нагрузкам", "ACCESS_PHISHING": "Доступ к фишингу", "ACCESS_PLATFORM_SETTINGS": "Доступ к настройкам платформы", @@ -401,6 +404,7 @@ "AssetGroup": "AssetGroup", "AssetGroups": "AssetGroups", "Assets": "Активы", + "ASSIGN_MARKING": "Назначить метку", "Assistant": "Ассистент", "Associated file": "Ассоциированный файл", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "Удалить приборные панели", "DELETE_DOCUMENTS": "Удалить документы", "DELETE_LESSONS_LEARNED": "Удалить извлеченные уроки", + "DELETE_MARKING_ASSIGNMENT": "Удалить назначение метки", + "DELETE_MARKING_DEFINITION": "Удалить определение метки", "DELETE_PAYLOADS": "Удалить полезные нагрузки", "DELETE_PHISHING": "Удаление фишинга", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "Удаление групп и ролей платформы", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "Управление приборными панелями", "MANAGE_DOCUMENTS": "Управление документами", "MANAGE_LESSONS_LEARNED": "Управление извлеченными уроками", + "MANAGE_MARKING_DEFINITION": "Управление определением метки", "MANAGE_PAYLOADS": "Управление полезными нагрузками", "MANAGE_PHISHING": "Управление фишингом", "MANAGE_PLATFORM_SESSIONS": "Управление сеансами платформы", @@ -2276,6 +2283,8 @@ "Mark as done": "Отметить как сделанное", "Mark as read": "Отметить как прочитанное", "Mark as unread": "Отметить как непрочитанное", + "MARKING": "Метка", + "Marking definition type is immutable": "Тип определения метки является неизменяемым", "Marking definitions": "Определения маркировки", "marking_definition_color": "Цвет", "marking_definition_created_at": "Дата создания", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "Доказательство эксплуатации", "Proofs": "Доказательства", "Protected": "Защищено", + "Protected marking definitions cannot be deleted": "Защищённые определения меток удалить нельзя", + "Protected marking definitions cannot be updated": "Защищённые определения меток обновить нельзя", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "Поставщик", "providing": "Определяет", diff --git a/openaev-front/src/utils/lang/zh.json b/openaev-front/src/utils/lang/zh.json index 1fa98cda6fe..7b6ee67c835 100644 --- a/openaev-front/src/utils/lang/zh.json +++ b/openaev-front/src/utils/lang/zh.json @@ -94,6 +94,7 @@ "A document used as a security platform logo can't be deleted.": "用作安全平台徽标的文件不能删除。", "A document used in a payload can't be deleted.": "有效载荷中使用的文件不能删除。", "A manual chained scenario was created from this autonomous scenario": "A manual chained scenario was created from this autonomous scenario", + "A marking definition with the same type and definition already exists": "已存在类型和定义相同的标记定义", "A meaningful share of attacks got through - worth reviewing.": "A meaningful share of attacks got through - worth reviewing.", "A meaningful share of validations got through - worth reviewing.": "相当一部分验证被突破 - 值得审查。", "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.": "A prerequisite check failed before the main command could run. Review prerequisite dependencies and ensure they are met on the target.", @@ -118,6 +119,8 @@ "ACCESS_DOCUMENTS": "访问文件", "ACCESS_FINDINGS": "访问结果", "ACCESS_LESSONS_LEARNED": "获取经验教训", + "ACCESS_MARKING_ASSIGNMENT": "访问标记分配", + "ACCESS_MARKING_DEFINITION": "访问标记定义", "ACCESS_PAYLOADS": "访问有效载荷", "ACCESS_PHISHING": "访问钓鱼攻击", "ACCESS_PLATFORM_SETTINGS": "访问平台设置", @@ -401,6 +404,7 @@ "AssetGroup": "资产组", "AssetGroups": "资产组", "Assets": "资产组", + "ASSIGN_MARKING": "分配标记", "Assistant": "助手", "Associated file": "关联文件", "Associated findings": "Associated findings", @@ -1058,6 +1062,8 @@ "DELETE_DASHBOARDS": "删除仪表板", "DELETE_DOCUMENTS": "删除文件", "DELETE_LESSONS_LEARNED": "删除经验教训", + "DELETE_MARKING_ASSIGNMENT": "删除标记分配", + "DELETE_MARKING_DEFINITION": "删除标记定义", "DELETE_PAYLOADS": "删除有效载荷", "DELETE_PHISHING": "删除钓鱼攻击", "DELETE_PLATFORM_USERS_GROUPS_AND_ROLES": "删除平台组和角色", @@ -2242,6 +2248,7 @@ "MANAGE_DASHBOARDS": "管理仪表板", "MANAGE_DOCUMENTS": "管理文件", "MANAGE_LESSONS_LEARNED": "管理经验教训", + "MANAGE_MARKING_DEFINITION": "管理标记定义", "MANAGE_PAYLOADS": "管理有效载荷", "MANAGE_PHISHING": "管理钓鱼攻击", "MANAGE_PLATFORM_SESSIONS": "管理平台会话", @@ -2276,6 +2283,8 @@ "Mark as done": "标记已完成", "Mark as read": "标记为已读", "Mark as unread": "标记为未读", + "MARKING": "标记", + "Marking definition type is immutable": "标记定义类型是不可变的", "Marking definitions": "标记定义", "marking_definition_color": "颜色", "marking_definition_created_at": "创建日期", @@ -2936,6 +2945,8 @@ "Proof of exploitation": "利用证据", "Proofs": "证据", "Protected": "受保护", + "Protected marking definitions cannot be deleted": "受保护的标记定义无法被删除", + "Protected marking definitions cannot be updated": "受保护的标记定义无法被更新", "Prove ownership (TXT record)": "Prove ownership (TXT record)", "Provider": "提供商", "providing": "提供", From 3191bd54a300d1a40ed231022c1d2aa01de0d1aa Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Fri, 28 Aug 2026 09:20:58 +0200 Subject: [PATCH 5/6] [frontend] fix --- .../src/admin/components/settings/SecurityMenu.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openaev-front/src/admin/components/settings/SecurityMenu.tsx b/openaev-front/src/admin/components/settings/SecurityMenu.tsx index be693740df2..148ac79ed82 100644 --- a/openaev-front/src/admin/components/settings/SecurityMenu.tsx +++ b/openaev-front/src/admin/components/settings/SecurityMenu.tsx @@ -132,6 +132,14 @@ const SecurityMenuComponent: FunctionComponent = () => { }); } + if (!isPlatform && canAccessMarkingDefinitions) { + entries.push({ + path: `${SECURITY_BASE}/marking_definitions`, + icon: () => (), + label: 'Marking definitions', + }); + } + // Single context selector at the top of the section (industry pattern: scope // is a primary navigation constraint expressed once, not a per-resource // filter repeated under every entry). From 227136c51d90ac50121693543ec3cb78f41de01d Mon Sep 17 00:00:00 2001 From: Damien Goujard Date: Fri, 28 Aug 2026 09:31:47 +0200 Subject: [PATCH 6/6] [backend] fix --- .../openaev/api/marking_definition/MarkingDefinitionApi.java | 4 +++- .../service/marking_definition/MarkingDefinitionService.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java index 1f0c0445223..398593f7ab1 100644 --- a/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java +++ b/openaev-api/src/main/java/io/openaev/api/marking_definition/MarkingDefinitionApi.java @@ -50,7 +50,9 @@ public class MarkingDefinitionApi extends RestBehavior { @GetMapping @Transactional(readOnly = true) @AccessControl(actionPerformed = Action.SEARCH, resourceType = ResourceType.MARKING_DEFINITION) - @Operation(summary = "Get marking definitions", description = "Get the list of marking definitions") + @Operation( + summary = "Get marking definitions", + description = "Get the list of marking definitions") public List list(TxCtx ctx) { return service.list(ctx).stream().map(MarkingDefinitionMapper::toOutput).toList(); } diff --git a/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java b/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java index a786f4e9686..b7ec4ce720c 100644 --- a/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java +++ b/openaev-api/src/main/java/io/openaev/service/marking_definition/MarkingDefinitionService.java @@ -14,8 +14,8 @@ import io.openaev.utils.pagination.SearchPaginationInput; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.util.Objects; import java.util.List; +import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page;