Skip to content

feat: Implement Custom Sensitivity Rules#52

Merged
MaximumTrainer merged 4 commits intomainfrom
copilot/implement-custom-sensitivity-rules
Apr 13, 2026
Merged

feat: Implement Custom Sensitivity Rules#52
MaximumTrainer merged 4 commits intomainfrom
copilot/implement-custom-sensitivity-rules

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

  • Backend: Create GenericDataType enum
  • Backend: Create CustomSensitivityRule JPA entity
  • Backend: Create CustomSensitivityRulePort, CustomSensitivityRuleRepository
  • Backend: Create DTOs with @field:NotBlank/@field:NotNull bean validation constraints
  • Backend: Create CustomSensitivityRuleUseCase, CustomSensitivityRuleService
  • Backend: Create CustomSensitivityRuleController with @Valid on all @RequestBody parameters
  • Backend: matchesColumnName catches IllegalArgumentException from invalid regex patterns (returns false, logs warning)
  • Backend: previewRule logs warning with workspaceId on schema JSON parse failure
  • Backend: toResponse uses requireNotNull(id) instead of id ?: 0L
  • Backend: Update ColumnSensitivity with customSensitivityLabel and recommendedPresetId
  • Backend: SensitivityScanService evaluates custom rules for ALL columns (including built-in matches) so a linked preset can be attached
  • Backend: PrivacyHubService.applyRecommendations preloads presets and generators into maps (N+1 elimination)
  • Backend: Updated PrivacyRecommendation DTO with recommendedPresetId
  • Tests: Stub findByIsActiveTrue()emptyList() in SensitivityScanServiceTest and SensitivityScanLogServiceTest
  • Tests: Added CustomSensitivityRuleControllerTest, CustomSensitivityRuleServiceTest
  • Frontend: Removed unused listWorkspacePresets import, CustomRuleMatcher type import, and workspaces ref
  • Frontend: Loads both system presets and workspace presets (when a workspace is selected); combined list used for preset picker and name resolution
  • Frontend: openCreateDrawer/openEditDrawer now resets all preview state including previewWorkspaceId and previewing
  • Frontend: Workspace dropdown change triggers onPreviewWorkspaceChange to reload workspace presets and clear stale results
  • Frontend: Router + NavBar with "Sensitivity Rules" link

Copilot AI and others added 2 commits April 13, 2026 14:00
Copilot AI changed the title [WIP] Add implementation of custom sensitivity rules for data masking feat: Implement Custom Sensitivity Rules Apr 13, 2026
Copilot AI requested a review from MaximumTrainer April 13, 2026 14:08
@MaximumTrainer MaximumTrainer removed their assignment Apr 13, 2026
@MaximumTrainer MaximumTrainer requested review from Copilot and removed request for MaximumTrainer April 13, 2026 14:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a full-stack “Custom Sensitivity Rules” feature so users can define column-name + generic-type matching rules, preview matches against stored schema snapshots, and (optionally) link matches to generator presets for bulk recommendation application in the Privacy Hub.

Changes:

  • Added backend entity/ports/service/controller for CRUD + preview of custom sensitivity rules, and integrated custom rule matching into sensitivity scans.
  • Extended Privacy Hub recommendations to carry an optional recommendedPresetId and apply linked presets when present.
  • Added a new frontend Settings view (+ router/nav + API/types) to manage rules and preview matches.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
frontend/src/views/SensitivityRulesView.vue New management UI with side-drawer CRUD and preview panel.
frontend/src/types/index.ts Adds frontend enums/types for rules, matchers, and preview payloads/results.
frontend/src/router/index.ts Registers /settings/sensitivity-rules route.
frontend/src/components/NavBar.vue Adds “Sensitivity Rules” navigation link.
frontend/src/api/sensitivityRules.ts Adds typed API client for rules CRUD + preview.
backend/src/main/kotlin/com/opendatamask/domain/port/output/CustomSensitivityRulePort.kt New output port for rule persistence.
backend/src/main/kotlin/com/opendatamask/domain/port/input/dto/CustomSensitivityRuleDto.kt New request/response/preview DTOs.
backend/src/main/kotlin/com/opendatamask/domain/port/input/CustomSensitivityRuleUseCase.kt New use-case interface for rules.
backend/src/main/kotlin/com/opendatamask/domain/model/GenericDataType.kt Adds generic data type enum for rule filtering.
backend/src/main/kotlin/com/opendatamask/domain/model/CustomSensitivityRule.kt New JPA entity + matcher model/enums.
backend/src/main/kotlin/com/opendatamask/adapter/output/persistence/CustomSensitivityRuleRepository.kt Spring Data repository implementing the new port.
backend/src/main/kotlin/com/opendatamask/application/service/CustomSensitivityRuleService.kt Implements CRUD, preview, and matching helpers.
backend/src/main/kotlin/com/opendatamask/adapter/input/rest/CustomSensitivityRuleController.kt REST endpoints for CRUD + preview.
backend/src/main/kotlin/com/opendatamask/application/service/SensitivityScanService.kt Integrates custom rules into scanning and persists custom label/preset ID.
backend/src/main/kotlin/com/opendatamask/domain/model/ColumnSensitivity.kt Adds customSensitivityLabel and recommendedPresetId fields.
backend/src/main/kotlin/com/opendatamask/domain/port/input/dto/PrivacyHubDto.kt Extends PrivacyRecommendation with recommendedPresetId.
backend/src/main/kotlin/com/opendatamask/application/service/PrivacyHubService.kt Applies preset-based recommendations and surfaces custom labels.
backend/src/test/kotlin/com/opendatamask/application/service/CustomSensitivityRuleServiceTest.kt Adds unit coverage for matching + preview + error paths.
backend/src/test/kotlin/com/opendatamask/adapter/input/rest/CustomSensitivityRuleControllerTest.kt Adds MVC test coverage for CRUD + preview endpoint.
backend/src/test/kotlin/com/opendatamask/application/service/SensitivityScanServiceTest.kt Updates scan tests for new dependencies.
backend/src/test/kotlin/com/opendatamask/application/service/SensitivityScanLogServiceTest.kt Updates scan/log tests for new dependencies.
backend/src/test/kotlin/com/opendatamask/application/service/PrivacyHubServiceTest.kt Updates Privacy Hub tests for new preset dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +23
import { listSystemPresets, listWorkspacePresets } from '@/api/presets'
import type { GeneratorPresetResponse } from '@/api/presets'
import type {
CustomSensitivityRule,
CustomSensitivityRuleRequest,
CustomRuleMatcher,
CustomRulePreviewResult
} from '@/types'
import { GenericDataType, MatcherType } from '@/types'

const workspaceStore = useWorkspaceStore()
const workspaces = ref(workspaceStore.workspaces)

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listWorkspacePresets, CustomRuleMatcher, and the local workspaces ref are imported/declared but never used. This will typically fail typecheck/lint in the frontend; either remove them or wire listWorkspacePresets/workspaces into the preset picker / workspace dropdown logic.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +76
async function fetchPresets() {
try {
systemPresets.value = await listSystemPresets()
} catch {
// non-critical
}
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked preset picker only loads listSystemPresets(); workspace presets are never loaded, and presetName() only resolves against systemPresets. This prevents linking to (or even correctly displaying) workspace presets even though listWorkspacePresets(workspaceId) exists. Consider loading both system + workspace presets (scoped by selected workspace if needed) and resolving names from the combined list.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +108
function openCreateDrawer() {
editingRule.value = null
form.value = {
name: '',
description: null,
dataTypeFilter: GenericDataType.ANY,
matchers: [],
linkedPresetId: null,
isActive: true
}
previewResults.value = []
previewError.value = null
saveError.value = null
drawerOpen.value = true
}

function openEditDrawer(rule: CustomSensitivityRule) {
editingRule.value = rule
form.value = {
name: rule.name,
description: rule.description,
dataTypeFilter: rule.dataTypeFilter,
matchers: rule.matchers.map((m) => ({ ...m })),
linkedPresetId: rule.linkedPresetId,
isActive: rule.isActive
}
previewResults.value = []
previewError.value = null
saveError.value = null
drawerOpen.value = true
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openCreateDrawer()/openEditDrawer() reset preview results/errors but do not reset previewWorkspaceId (or previewing). This can cause the drawer to reopen with a stale workspace selection and immediately show "No columns matched" for the previous selection. Consider resetting preview-related state (including previewWorkspaceId) when opening/closing the drawer.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +116
@@ -74,22 +93,67 @@ class SensitivityScanService(
columnName = column
)
entity.isSensitive = true
entity.sensitivityType = result.sensitivityType
entity.confidenceLevel = result.confidence
entity.recommendedGeneratorType = result.recommendedGenerator
entity.sensitivityType = builtInResult.sensitivityType
entity.confidenceLevel = builtInResult.confidence
entity.recommendedGeneratorType = builtInResult.recommendedGenerator
entity.customSensitivityLabel = null
entity.recommendedPresetId = null
columnSensitivityRepository.save(entity)
}
sensitivityScanLogEntryRepository.save(
SensitivityScanLogEntry(
scanLogId = log.id!!,
tableName = table,
columnName = column,
detectedType = result?.sensitivityType?.name,
confidenceLevel = result?.confidence?.name,
recommendedGenerator = result?.recommendedGenerator?.name,
scannedAt = LocalDateTime.now()
sensitivityScanLogEntryRepository.save(
SensitivityScanLogEntry(
scanLogId = log.id!!,
tableName = table,
columnName = column,
detectedType = builtInResult.sensitivityType.name,
confidenceLevel = builtInResult.confidence.name,
recommendedGenerator = builtInResult.recommendedGenerator.name,
scannedAt = LocalDateTime.now()
)
)
)
} else {
val customMatch = detectCustomRuleSensitivity(column, columnInfo.type, activeCustomRules)
if (customMatch != null) {
val (matchedRule, _) = customMatch
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom rules are only evaluated in the else branch when builtInResult == null. This means a user cannot attach a linked preset via a custom rule if a built-in rule already detects the column (e.g., EMAIL), which appears to conflict with the stated behavior of applying custom rule matches after built-ins. Consider running custom rule evaluation after built-in detection as well, and define precedence (e.g., custom preset overrides built-in recommendedGeneratorType, while keeping the built-in sensitivityType).

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +118
MatcherType.REGEX -> Regex(
matcher.value,
if (matcher.caseSensitive) emptySet() else setOf(RegexOption.IGNORE_CASE)
).containsMatchIn(columnName)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matchesColumnName() constructs a Regex(...) directly from user-provided input. If the regex is invalid, this will throw and can fail preview requests and even mark an entire workspace scan as FAILED. Catch regex compilation/match exceptions per matcher (and/or validate regexes during create/update) so one bad rule doesn't break scans.

Suggested change
MatcherType.REGEX -> Regex(
matcher.value,
if (matcher.caseSensitive) emptySet() else setOf(RegexOption.IGNORE_CASE)
).containsMatchIn(columnName)
MatcherType.REGEX -> {
try {
Regex(
matcher.value,
if (matcher.caseSensitive) emptySet() else setOf(RegexOption.IGNORE_CASE)
).containsMatchIn(columnName)
} catch (e: IllegalArgumentException) {
false
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +20
data class CustomSensitivityRuleRequest(
val name: String,
val description: String? = null,
val dataTypeFilter: GenericDataType = GenericDataType.ANY,
val matchers: List<CustomRuleMatcherDto> = emptyList(),
val linkedPresetId: Long? = null,
val isActive: Boolean = true
)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This request DTO lacks the bean-validation annotations used elsewhere (e.g., @field:NotBlank) and the controller doesn't use @Valid. As a result, invalid payloads (blank names, empty matcher values, etc.) can reach persistence/matching logic and cause runtime issues. Add validation constraints here and annotate controller @RequestBody parameters with @Valid for create/update/preview as appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +106
if (rec.recommendedPresetId != null) {
// Apply linked preset from a custom sensitivity rule
val preset = generatorPresetRepository.findById(rec.recommendedPresetId).orElse(null)
if (preset == null) {
logger.warn(
"Cannot apply recommendation for ${rec.tableName}.${rec.columnName}: " +
"linked preset id=${rec.recommendedPresetId} not found"
)
continue
}
val existingGenerator = columnGeneratorRepository.findByTableConfigurationId(tableConfig.id)
.find { it.columnName == rec.columnName }
if (existingGenerator != null) {
existingGenerator.presetId = preset.id
existingGenerator.generatorType = preset.generatorType
existingGenerator.generatorParams = preset.generatorParams
columnGeneratorRepository.save(existingGenerator)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyRecommendations() performs per-recommendation lookups (generatorPresetRepository.findById(...)) and per-recommendation generator scans (findByTableConfigurationId(...).find { ... }). For many recommendations this becomes an N+1 pattern. Consider preloading presets into a map and preloading existing generators per tableConfig (or extending the port with a targeted lookup) to reduce DB round-trips.

Copilot uses AI. Check for mistakes.
Comment on lines 27 to 30
@Mock private lateinit var EncryptionPort: EncryptionPort
@Mock private lateinit var customSensitivityRuleRepository: CustomSensitivityRulePort
@Mock private lateinit var customSensitivityRuleService: CustomSensitivityRuleService

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SensitivityScanService.scanWorkspace() now calls customSensitivityRuleRepository.findByIsActiveTrue() unconditionally. In these tests, the new mock isn't stubbed, and Mockito defaults can return null for non-null Kotlin return types, causing an NPE when .map { ... } is invoked. Stub findByIsActiveTrue() to return emptyList() (or a specific list) in the test setup.

Copilot uses AI. Check for mistakes.
Comment on lines 26 to 30
@Mock private lateinit var connectorFactory: ConnectorFactory
@Mock private lateinit var EncryptionPort: EncryptionPort
@Mock private lateinit var customSensitivityRuleRepository: CustomSensitivityRuleRepository
@Mock private lateinit var customSensitivityRuleService: CustomSensitivityRuleService

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: customSensitivityRuleRepository.findByIsActiveTrue() is invoked during scans. This test adds the new mock but never stubs it, which can lead to an NPE at runtime when the service does .map { ... } on the returned list. Stub it to emptyList() (or configure the desired custom-rule behavior).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +38
@PostMapping
fun createRule(
@RequestBody request: CustomSensitivityRuleRequest
): ResponseEntity<CustomSensitivityRuleResponse> =
ResponseEntity.status(HttpStatus.CREATED)
.body(customSensitivityRuleService.createRule(request))

@PutMapping("/{id}")
fun updateRule(
@PathVariable id: Long,
@RequestBody request: CustomSensitivityRuleRequest
): ResponseEntity<CustomSensitivityRuleResponse> =
ResponseEntity.ok(customSensitivityRuleService.updateRule(id, request))
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other controllers (e.g., GeneratorPresetController) use @Valid on request bodies to enforce DTO constraints. Here, create/update/preview accept request bodies without @Valid, so any added bean validation on the DTOs will not run. Consider adding @Valid to the @RequestBody parameters.

Copilot uses AI. Check for mistakes.
@MaximumTrainer
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

… scan ordering, preview reset

Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/5a92e1d9-53ef-44f8-b2b9-31addf85947e

Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 13, 2026

@copilot apply changes based on the comments in this thread

All review feedback has been addressed in commit 29a7b19. Here's a summary of what was changed:

Backend:

  • Added @field:NotBlank/@field:NotNull bean validation to CustomSensitivityRuleRequest, CustomRuleMatcherDto, and CustomRulePreviewRequest DTOs
  • Added @Valid to all @RequestBody parameters in CustomSensitivityRuleController (create, update, preview)
  • matchesColumnName now catches IllegalArgumentException from invalid regex patterns per-matcher (returns false and logs a warning instead of throwing)
  • previewRule logs a warning with the workspaceId on schema JSON parse failure before returning an empty list
  • toResponse replaced id ?: 0L with requireNotNull(id) to throw early if an un-persisted entity is mapped
  • SensitivityScanService now evaluates custom rules for every column (moved out of the else branch), so a custom rule's linked preset can be attached even when a built-in rule already detected the column
  • PrivacyHubService.applyRecommendations preloads all referenced presets and existing column generators into maps upfront, eliminating the N+1 pattern
  • findByIsActiveTrue() stubbed to emptyList() in both SensitivityScanServiceTest and SensitivityScanLogServiceTest to prevent NPE

Frontend:

  • Removed unused CustomRuleMatcher type import, listWorkspacePresets (now used properly), and the redundant workspaces ref
  • Preset picker now loads both system presets and workspace presets when a workspace is selected; the combined list resolves preset names correctly
  • openCreateDrawer/openEditDrawer fully resets all preview state (previewWorkspaceId, previewResults, previewing, previewError)
  • Workspace dropdown in the preview panel calls onPreviewWorkspaceChange on selection, which clears stale results and loads workspace-scoped presets

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java -Dorg.gradle.internal.worker.tmpdir=/home/REDACTED/work/OpenDataMask/OpenDataMask/backend/build/tmp/test/work -Dorg.gradle.native=false -javaagent:/home/REDACTED/work/OpenDataMask/OpenDataMask/backend/build/tmp/.cache/expanded/zip_0bd0851504e6286cb5fc6f37a81e25cc/jacocoagent.jar=destfile=build/jacoco/test.exec,append=true,inclnolocationclasses=false,dumponexit=true,output=file,jmx=false @/home/REDACTED/.gradle/.tmp/gradle-worker-classpath6803424827199600263txt -Xmx512m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -ea worker.org.gradle.process.internal.worker.GradleWorkerMain &#39;Gradle Test Executor 1&#39; (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from MaximumTrainer April 13, 2026 15:23
@MaximumTrainer MaximumTrainer marked this pull request as ready for review April 13, 2026 15:32
@MaximumTrainer MaximumTrainer merged commit 5337049 into main Apr 13, 2026
10 checks passed
@MaximumTrainer MaximumTrainer deleted the copilot/implement-custom-sensitivity-rules branch April 13, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementation of Custom Sensitivity Rules

3 participants