diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..94f480de94
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
\ No newline at end of file
diff --git a/client b/client
index 273fa3c4cc..5b4c9fbf18 160000
--- a/client
+++ b/client
@@ -1 +1 @@
-Subproject commit 273fa3c4cc7d87f942ade231ce9220b98fa595e2
+Subproject commit 5b4c9fbf18cde5f7c2b3455eef434d96c7ddf771
diff --git a/pom.xml b/pom.xml
index 42262c105b..54505aea16 100644
--- a/pom.xml
+++ b/pom.xml
@@ -196,6 +196,17 @@
${springboot.version}
+
+
+ com.openai
+ openai-java
+ 2.16.0
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk7
+ 1.9.24
+
org.codehaus.groovy
diff --git a/server/integration-test/src/test/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManagerTest.java b/server/integration-test/src/test/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManagerTest.java
index 2d8b7e1980..d1123b82c7 100644
--- a/server/integration-test/src/test/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManagerTest.java
+++ b/server/integration-test/src/test/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManagerTest.java
@@ -47,6 +47,7 @@
import com.oceanbase.odc.service.collaboration.project.model.Project;
import com.oceanbase.odc.service.connection.database.model.Database;
import com.oceanbase.odc.service.connection.model.ConnectionConfig;
+import com.oceanbase.odc.service.datasecurity.model.ScanningModeType;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo.ScanningTaskStatus;
import com.oceanbase.odc.service.datasecurity.model.SensitiveLevel;
@@ -107,7 +108,8 @@ public static void tearDown() {
public void test_start_groovyRule_OBMySQL() {
List databases = createDatabases(ConnectType.OB_MYSQL);
List rules = Arrays.asList(createGroovySensitiveRules());
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, mysqlConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, mysqlConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(2, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
@@ -117,7 +119,8 @@ public void test_start_groovyRule_OBMySQL() {
public void test_start_groovyRule_OBOracle() {
List databases = createDatabases(ConnectType.OB_ORACLE);
List rules = Arrays.asList(createGroovySensitiveRules());
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, oracleConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, oracleConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(2, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
@@ -127,7 +130,8 @@ public void test_start_groovyRule_OBOracle() {
public void test_start_pathRule_OBMySQL() {
List databases = createDatabases(ConnectType.OB_MYSQL);
List rules = Arrays.asList(createPathSensitiveRules());
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, mysqlConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, mysqlConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(20, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
@@ -137,7 +141,8 @@ public void test_start_pathRule_OBMySQL() {
public void test_start_pathRule_OBMOracle() {
List databases = createDatabases(ConnectType.OB_ORACLE);
List rules = Arrays.asList(createPathSensitiveRules());
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, oracleConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, oracleConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(20, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
@@ -147,7 +152,8 @@ public void test_start_pathRule_OBMOracle() {
public void test_start_RegexRule_OBMySQL() {
List databases = createDatabases(ConnectType.OB_MYSQL);
List rules = Arrays.asList(createRegexSensitiveRules(ConnectType.OB_MYSQL));
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, mysqlConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, mysqlConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(6, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
@@ -157,7 +163,8 @@ public void test_start_RegexRule_OBMySQL() {
public void test_start_RegexRule_OBOracle() {
List databases = createDatabases(ConnectType.OB_ORACLE);
List rules = Arrays.asList(createRegexSensitiveRules(ConnectType.OB_ORACLE));
- SensitiveColumnScanningTaskInfo taskInfo = manager.start(databases, rules, oracleConnectionConfig, null);
+ SensitiveColumnScanningTaskInfo taskInfo =
+ manager.start(databases, rules, ScanningModeType.RULES_ONLY, oracleConnectionConfig, null);
await().atMost(20, SECONDS)
.until(() -> manager.get(taskInfo.getTaskId()).getStatus() == ScanningTaskStatus.SUCCESS);
Assert.assertEquals(6, manager.get(taskInfo.getTaskId()).getSensitiveColumns().size());
diff --git a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ErrorCodes.java b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ErrorCodes.java
index d7954a985e..64e73603c4 100644
--- a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ErrorCodes.java
+++ b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ErrorCodes.java
@@ -334,6 +334,16 @@ public enum ErrorCodes implements ErrorCode {
ExtractFileFailed,
InvalidSignature,
+ /**
+ * AI Service
+ */
+ AIServiceNotAvailable,
+ AIConfigurationIncomplete,
+ AIClientNotInitialized,
+ AIInferenceServiceError,
+ AIResponseFormatError,
+ AIResponseCountMismatch,
+
;
diff --git a/server/odc-core/src/main/resources/i18n/ErrorMessages.properties b/server/odc-core/src/main/resources/i18n/ErrorMessages.properties
index 89ccb301f4..808054c177 100644
--- a/server/odc-core/src/main/resources/i18n/ErrorMessages.properties
+++ b/server/odc-core/src/main/resources/i18n/ErrorMessages.properties
@@ -212,4 +212,10 @@ com.oceanbase.odc.ErrorCodes.UpdateNotAllowed=Editing is not allowed in the curr
com.oceanbase.odc.ErrorCodes.PauseNotAllowed=Disabling is not allowed in the current state, please check if there are any records in execution.
com.oceanbase.odc.ErrorCodes.DeleteNotAllowed=Deletion is not allowed in the current state.
com.oceanbase.odc.ErrorCodes.ExtractFileFailed=Failed to extract the file. Please check whether the file is correct. Details: {0}
-com.oceanbase.odc.ErrorCodes.InvalidSignature=File verification failed. Do not modify exported files or check if the correct key was used.
\ No newline at end of file
+com.oceanbase.odc.ErrorCodes.InvalidSignature=File verification failed. Do not modify exported files or check if the correct key was used.
+com.oceanbase.odc.ErrorCodes.AIServiceNotAvailable=AI service is not available. Please contact administrator to enable AI service.
+com.oceanbase.odc.ErrorCodes.AIConfigurationIncomplete=AI configuration is incomplete. Please contact administrator to configure AI parameters.
+com.oceanbase.odc.ErrorCodes.AIClientNotInitialized=AI client is not initialized. Please check AI configuration and restart service.
+com.oceanbase.odc.ErrorCodes.AIInferenceServiceError=Failed to call AI inference service. Details: {0}
+com.oceanbase.odc.ErrorCodes.AIResponseFormatError=AI response format is invalid. Details: {0}
+com.oceanbase.odc.ErrorCodes.AIResponseCountMismatch=AI response count does not match expected count. Expected: {0}, Actual: {1}
\ No newline at end of file
diff --git a/server/odc-core/src/main/resources/i18n/ErrorMessages_zh_CN.properties b/server/odc-core/src/main/resources/i18n/ErrorMessages_zh_CN.properties
index 1a84987660..7bb054aa71 100644
--- a/server/odc-core/src/main/resources/i18n/ErrorMessages_zh_CN.properties
+++ b/server/odc-core/src/main/resources/i18n/ErrorMessages_zh_CN.properties
@@ -214,4 +214,11 @@ com.oceanbase.odc.ErrorCodes.UnsupportedSyncTableStructure=结构同步暂不支
com.oceanbase.odc.ErrorCodes.ScheduleIntervalTooShort=执行间隔配置过短,请重新配置。最小间隔为:{0} 秒
com.oceanbase.odc.ErrorCodes.ExtractFileFailed=提取文件失败,请确认文件是否正确,错误详情 {0}
-com.oceanbase.odc.ErrorCodes.InvalidSignature=文件验签不通过,请勿修改导出文件或检查密钥是否正确
\ No newline at end of file
+com.oceanbase.odc.ErrorCodes.InvalidSignature=文件验签不通过,请勿修改导出文件或检查密钥是否正确
+
+com.oceanbase.odc.ErrorCodes.AIServiceNotAvailable=AI 服务不可用,请联系管理员启用 AI 服务
+com.oceanbase.odc.ErrorCodes.AIConfigurationIncomplete=AI 配置不完整,请联系管理员配置 AI 参数
+com.oceanbase.odc.ErrorCodes.AIClientNotInitialized=AI 客户端未初始化,请检查 AI 配置并重启服务
+com.oceanbase.odc.ErrorCodes.AIInferenceServiceError=调用 AI 推理服务失败,错误详情:{0}
+com.oceanbase.odc.ErrorCodes.AIResponseFormatError=AI 响应格式无效,错误详情:{0}
+com.oceanbase.odc.ErrorCodes.AIResponseCountMismatch=AI 响应数量与预期不符,预期:{0},实际:{1}
\ No newline at end of file
diff --git a/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_20__alter_sensitive_rule.sql b/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_20__alter_sensitive_rule.sql
new file mode 100644
index 0000000000..163ce9a466
--- /dev/null
+++ b/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_20__alter_sensitive_rule.sql
@@ -0,0 +1,7 @@
+-- Add AI-related columns to table `data_security_sensitive_rule`
+alter table `data_security_sensitive_rule`
+ add column `ai_sensitive_types` text default null comment 'A list of sensitive data types for AI rules, stored as a JSON array string.';
+
+alter table `data_security_sensitive_rule`
+ add column `ai_custom_prompt` text default null comment 'User-defined custom prompt for AI rules.';
+
diff --git a/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_21__add_ai_system_config.sql b/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_21__add_ai_system_config.sql
new file mode 100644
index 0000000000..9219e0d17d
--- /dev/null
+++ b/server/odc-migrate/src/main/resources/migrate/common/V_4_3_4_21__add_ai_system_config.sql
@@ -0,0 +1,16 @@
+
+INSERT INTO config_system_configuration(`key`, `value`, `description`)
+VALUES('odc.ai.enabled', 'false', 'Whether AI feature is enabled, disabled by default')
+ON DUPLICATE KEY UPDATE `id`=`id`;
+
+INSERT INTO config_system_configuration(`key`, `value`, `description`)
+VALUES('odc.ai.api-key', '', 'AI API key, required when AI feature is enabled')
+ON DUPLICATE KEY UPDATE `id`=`id`;
+
+INSERT INTO config_system_configuration(`key`, `value`, `description`)
+VALUES('odc.ai.base-url', 'https://api.openai.com', 'AI API base URL, defaults to OpenAI official API endpoint')
+ON DUPLICATE KEY UPDATE `id`=`id`;
+
+INSERT INTO config_system_configuration(`key`, `value`, `description`)
+VALUES('odc.ai.model', 'gpt-3.5-turbo', 'AI model to use, defaults to gpt-3.5-turbo')
+ON DUPLICATE KEY UPDATE `id`=`id`;
\ No newline at end of file
diff --git a/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/AIController.java b/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/AIController.java
new file mode 100644
index 0000000000..57a2dd3639
--- /dev/null
+++ b/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/AIController.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.server.web.controller.v2;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.oceanbase.odc.core.authority.util.SkipAuthorize;
+import com.oceanbase.odc.service.common.response.Responses;
+import com.oceanbase.odc.service.common.response.SuccessResponse;
+import com.oceanbase.odc.service.datasecurity.ai.AIConfig;
+import com.oceanbase.odc.service.datasecurity.ai.AIInferenceService;
+import com.oceanbase.odc.service.datasecurity.ai.AIStatusResponse;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Api(tags = "AI")
+@RestController
+@RequestMapping("/api/v2/ai")
+public class AIController {
+
+ @Autowired
+ private AIConfig aiConfig;
+
+ @Autowired
+ private AIInferenceService aiInferenceService;
+
+
+ @ApiOperation(value = "Query the status of the AI function",
+ notes = "Return the status of whether the AI function is enabled and its configuration status")
+ @SkipAuthorize("AI status is safe to query for authenticated users")
+ @GetMapping("/status")
+ public SuccessResponse getAIStatus() {
+ AIStatusResponse response = new AIStatusResponse();
+ response.setEnabled(aiConfig.isEnabled());
+ response.setAvailable(aiInferenceService.isAIAvailable());
+ response.setModel(aiConfig.getModel());
+ response.setBaseUrl(aiConfig.getBaseUrl());
+ response.setApiKeyConfigured(aiConfig.getApiKey() != null && !aiConfig.getApiKey().trim().isEmpty());
+
+ return Responses.success(response);
+ }
+}
diff --git a/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/SensitiveColumnController.java b/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/SensitiveColumnController.java
index 9fd98510b3..dd7370ba16 100644
--- a/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/SensitiveColumnController.java
+++ b/server/odc-server/src/main/java/com/oceanbase/odc/server/web/controller/v2/SensitiveColumnController.java
@@ -35,12 +35,14 @@
import com.oceanbase.odc.service.common.response.Responses;
import com.oceanbase.odc.service.common.response.SuccessResponse;
import com.oceanbase.odc.service.datasecurity.SensitiveColumnService;
+import com.oceanbase.odc.service.datasecurity.SingleTableScanTaskManager;
import com.oceanbase.odc.service.datasecurity.model.DatabaseWithAllColumns;
import com.oceanbase.odc.service.datasecurity.model.QuerySensitiveColumnParams;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumn;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningReq;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnStats;
+import com.oceanbase.odc.service.datasecurity.model.SingleTableScanReq;
import com.oceanbase.odc.service.datasecurity.model.UpdateSensitiveColumnsReq;
import io.swagger.annotations.ApiOperation;
@@ -143,4 +145,25 @@ public SuccessResponse getScanningResults(@Path
return Responses.success(service.getScanningResults(projectId, taskId));
}
+ @ApiOperation(value = "stopScanning", notes = "Stop a sensitive column scanning task")
+ @RequestMapping(value = "/stopScanning", method = RequestMethod.POST)
+ public SuccessResponse stopScanning(@PathVariable Long projectId,
+ @RequestParam String taskId) {
+ return Responses.success(service.stopScanning(projectId, taskId));
+ }
+
+ @ApiOperation(value = "getSingleTableScanResult", notes = "Get single table scan result")
+ @RequestMapping(value = "/singleTableScan/{taskId}/result", method = RequestMethod.GET)
+ public SuccessResponse getSingleTableScanResult(
+ @PathVariable Long projectId,
+ @PathVariable String taskId) {
+ return Responses.success(service.getSingleTableScanResult(projectId, taskId));
+ }
+
+ @ApiOperation(value = "scanSingleTableAsync", notes = "Start an asynchronous single table scan")
+ @RequestMapping(value = "/scanSingleTableAsync", method = RequestMethod.POST)
+ public SuccessResponse scanSingleTableAsync(@PathVariable Long projectId,
+ @RequestBody SingleTableScanReq req) {
+ return Responses.success(service.scanSingleTableAsync(projectId, req));
+ }
}
diff --git a/server/odc-server/src/test/java/com/oceanbase/odc/supervisor/SupervisorApplicationTest.java b/server/odc-server/src/test/java/com/oceanbase/odc/supervisor/SupervisorApplicationTest.java
index 5fb40d6311..6946197185 100644
--- a/server/odc-server/src/test/java/com/oceanbase/odc/supervisor/SupervisorApplicationTest.java
+++ b/server/odc-server/src/test/java/com/oceanbase/odc/supervisor/SupervisorApplicationTest.java
@@ -15,14 +15,6 @@
*/
package com.oceanbase.odc.supervisor;
-/**
- * @author longpeng.zlp
- * @date 2024/12/9 15:59
- */
-/**
- * @author longpeng.zlp
- * @date 2024/12/9 15:59
- */
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
diff --git a/server/odc-service/pom.xml b/server/odc-service/pom.xml
index 5d28d6077c..e811f237ef 100644
--- a/server/odc-service/pom.xml
+++ b/server/odc-service/pom.xml
@@ -75,6 +75,14 @@
commons-beanutils
commons-beanutils
+
+ com.openai
+ openai-java
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk7
+
org.apache.commons
commons-compress
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/config/CommonSecurityProperties.java b/server/odc-service/src/main/java/com/oceanbase/odc/config/CommonSecurityProperties.java
index b2e8bbfb4f..86a78a66db 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/config/CommonSecurityProperties.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/config/CommonSecurityProperties.java
@@ -56,7 +56,8 @@ public class CommonSecurityProperties {
"/api/v2/internal/file/downloadImportFile",
"/api/v2/info",
"/api/v2/sso/state",
- "/api/v2/encryption/publicKey"};
+ "/api/v2/encryption/publicKey",
+ "/api/v2/ai/status"};
private static final String[] STATIC_RESOURCES = new String[] {
"/",
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/metadb/datasecurity/SensitiveRuleEntity.java b/server/odc-service/src/main/java/com/oceanbase/odc/metadb/datasecurity/SensitiveRuleEntity.java
index 405452ab90..23cd038918 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/metadb/datasecurity/SensitiveRuleEntity.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/metadb/datasecurity/SensitiveRuleEntity.java
@@ -113,4 +113,10 @@ public class SensitiveRuleEntity {
@Column(name = "update_time", nullable = false, insertable = false, updatable = false)
private Date updateTime;
+ @Convert(converter = JsonListConverter.class)
+ @Column(name = "ai_sensitive_types")
+ private List aiSensitiveTypes;
+
+ @Column(name = "ai_custom_prompt")
+ private String aiCustomPrompt;
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/MaskingAlgorithmService.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/MaskingAlgorithmService.java
index 6a256fa0c8..744ed3e3e3 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/MaskingAlgorithmService.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/MaskingAlgorithmService.java
@@ -207,6 +207,21 @@ public Long getDefaultAlgorithmIdByOrganizationId(@NonNull Long organizationId)
return entities.get(0).getId();
}
+ @SkipAuthorize("odc internal usages")
+ public Optional getAlgorithmIdByName(@NonNull String algorithmName, @NonNull Long organizationId) {
+ List entities =
+ algorithmRepository.findByNameAndOrganizationId(algorithmName, organizationId);
+ if (entities.isEmpty()) {
+ log.warn("No masking algorithm found with name: {} for organization: {}", algorithmName, organizationId);
+ return Optional.empty();
+ }
+ if (entities.size() > 1) {
+ log.warn("Multiple masking algorithms found with name: {} for organization: {}, using the first one",
+ algorithmName, organizationId);
+ }
+ return Optional.of(entities.get(0).getId());
+ }
+
@SkipAuthorize("odc internal usages")
public List getMaskingAlgorithmsByOrganizationId(@NonNull Long organizationId) {
return organizationId2Algorithms.get(organizationId);
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnRecognizer.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnRecognizer.java
deleted file mode 100644
index 6e7c4a979b..0000000000
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnRecognizer.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (c) 2023 OceanBase.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.oceanbase.odc.service.datasecurity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import com.oceanbase.odc.service.datasecurity.model.SensitiveLevel;
-import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
-import com.oceanbase.odc.service.datasecurity.recognizer.ColumnRecognizer;
-import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
-
-/**
- * @author gaoda.xy
- * @date 2023/5/30 10:30
- */
-public class SensitiveColumnRecognizer implements ColumnRecognizer {
-
- private Long sensitiveRuleId;
- private Long maskingAlgorithmId;
- private SensitiveLevel sensitiveLevel;
- private final List sensitiveRules;
- private final List recognizers;
-
- public SensitiveColumnRecognizer(List rules) {
- this.sensitiveRules = rules;
- this.recognizers = new ArrayList<>();
- for (SensitiveRule rule : this.sensitiveRules) {
- this.recognizers.add(ColumnRecognizerFactory.create(rule));
- }
- }
-
- public Long sensitiveRuleId() {
- return this.sensitiveRuleId;
- }
-
- public Long maskingAlgorithmId() {
- return this.maskingAlgorithmId;
- }
-
- public SensitiveLevel sensitiveLevel() {
- return this.sensitiveLevel;
- }
-
- @Override
- public boolean recognize(DBTableColumn column) {
- for (int i = 0; i < recognizers.size(); i++) {
- if (recognizers.get(i).recognize(column)) {
- SensitiveRule rule = sensitiveRules.get(i);
- this.sensitiveRuleId = rule.getId();
- this.maskingAlgorithmId = rule.getMaskingAlgorithmId();
- this.sensitiveLevel = rule.getLevel();
- return true;
- }
- }
- return false;
- }
-
-}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanner.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanner.java
new file mode 100644
index 0000000000..236c3550f8
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanner.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.oceanbase.odc.service.datasecurity.factory.ColumnRecognizerFactory;
+import com.oceanbase.odc.service.datasecurity.factory.ScanningStrategyFactory;
+import com.oceanbase.odc.service.datasecurity.model.ScanResult;
+import com.oceanbase.odc.service.datasecurity.model.ScanningModeType;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRuleType;
+import com.oceanbase.odc.service.datasecurity.recognizer.ColumnRecognizer;
+import com.oceanbase.odc.service.datasecurity.strategy.ScanningStrategy;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+public class SensitiveColumnScanner {
+
+ private final List basicRecognizers;
+ private final List aiRecognizers;
+ private final ScanningStrategyFactory strategyFactory;
+
+ public SensitiveColumnScanner(List rules, ScanningStrategyFactory strategyFactory) {
+ this.basicRecognizers = rules.stream()
+ .filter(r -> r.getType() != SensitiveRuleType.AI)
+ .map(ColumnRecognizerFactory::create)
+ .collect(Collectors.toList());
+ this.aiRecognizers = rules.stream()
+ .filter(r -> r.getType() == SensitiveRuleType.AI)
+ .map(ColumnRecognizerFactory::create)
+ .collect(Collectors.toList());
+ this.strategyFactory = strategyFactory;
+ }
+
+ public ScanResult scan(DBTableColumn column, ScanningModeType mode) {
+ ScanningStrategy strategy = strategyFactory.getStrategy(mode);
+ return strategy.scan(column, basicRecognizers, aiRecognizers);
+ }
+
+ public Map scanBatch(List columns, ScanningModeType mode) {
+ ScanningStrategy strategy = strategyFactory.getStrategy(mode);
+ return strategy.scanBatch(columns, basicRecognizers, aiRecognizers);
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTask.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTask.java
index 00d3ccc390..2dd234a21c 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTask.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTask.java
@@ -20,81 +20,201 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import com.oceanbase.odc.core.shared.constant.ErrorCodes;
+import com.oceanbase.odc.service.common.util.SpringContextUtil;
import com.oceanbase.odc.service.connection.database.model.Database;
+import com.oceanbase.odc.service.datasecurity.factory.ScanningStrategyFactory;
+import com.oceanbase.odc.service.datasecurity.model.DefaultSensitiveType;
+import com.oceanbase.odc.service.datasecurity.model.RecognitionResult;
+import com.oceanbase.odc.service.datasecurity.model.ScanResult;
+import com.oceanbase.odc.service.datasecurity.model.ScanningModeType;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumn;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnMeta;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo.ScanningTaskStatus;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnType;
import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRuleType;
import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+import lombok.extern.slf4j.Slf4j;
+
/**
* @author gaoda.xy
* @date 2023/5/25 14:43
*/
+@Slf4j
public class SensitiveColumnScanningTask implements Callable {
private final Database database;
- private final SensitiveColumnRecognizer recognizer;
+ private final SensitiveColumnScanner scanner;
+ private final ScanningModeType scanningMode;
private final SensitiveColumnScanningTaskInfo taskInfo;
private final Map> table2Columns;
private final Map> view2Columns;
private final Set existsSensitiveColumns;
+ private final Map ruleMap;
- public SensitiveColumnScanningTask(Database database, List rules,
+ public SensitiveColumnScanningTask(Database database, List rules, ScanningModeType scanningMode,
SensitiveColumnScanningTaskInfo taskInfo, List existsSensitiveColumns,
Map> table2Columns, Map> view2Columns) {
this.database = database;
- this.recognizer = new SensitiveColumnRecognizer(rules);
+ this.scanningMode = scanningMode;
+ ScanningStrategyFactory strategyFactory = new ScanningStrategyFactory();
+ this.scanner = new SensitiveColumnScanner(rules, strategyFactory);
this.table2Columns = table2Columns;
this.view2Columns = view2Columns;
this.taskInfo = taskInfo;
this.existsSensitiveColumns = new HashSet<>(existsSensitiveColumns);
+ this.ruleMap = rules.stream().collect(Collectors.toMap(SensitiveRule::getId, Function.identity()));
+ }
+
+ private String getColumnKey(DBTableColumn column) {
+ return String.format("%s.%s.%s",
+ column.getSchemaName() != null ? column.getSchemaName() : "unknown_schema",
+ column.getTableName() != null ? column.getTableName() : "unknown_table",
+ column.getName() != null ? column.getName() : "unknown_column");
}
@Override
- public Void call() throws Exception {
+ public Void call() {
try {
taskInfo.setStatus(ScanningTaskStatus.RUNNING);
scanColumns(table2Columns, SensitiveColumnType.TABLE_COLUMN);
+ if (taskInfo.isCancelled()) {
+ return null;
+ }
scanColumns(view2Columns, SensitiveColumnType.VIEW_COLUMN);
} catch (Exception e) {
- taskInfo.setCompleteTime(new Date());
- taskInfo.setStatus(ScanningTaskStatus.FAILED);
- taskInfo.setErrorCode(ErrorCodes.Unexpected);
- taskInfo.setErrorMsg(String.format("Some errors happen when scanning sensitive column, database=%s",
- database.getName()));
+ if (!taskInfo.isCancelled()) {
+ taskInfo.setStatus(ScanningTaskStatus.FAILED);
+ taskInfo.setErrorCode(ErrorCodes.Unexpected);
+ taskInfo.setErrorMsg(String.format("Error during sensitive column scanning on database=%s, reason=%s",
+ database.getName(), e.getMessage()));
+ taskInfo.setCompleteTime(new Date());
+ }
}
return null;
}
private void scanColumns(Map> object2Columns, SensitiveColumnType columnType) {
- for (String objectName : object2Columns.keySet()) {
- List sensitiveColumns = new ArrayList<>();
- for (DBTableColumn dbTableColumn : object2Columns.get(objectName)) {
- if (recognizer.recognize(dbTableColumn) && !existsSensitiveColumns
- .contains(new SensitiveColumnMeta(database.getId(), objectName, dbTableColumn.getName()))) {
- SensitiveColumn column = new SensitiveColumn();
- column.setType(columnType);
- column.setDatabase(database);
- column.setTableName(objectName);
- column.setColumnName(dbTableColumn.getName());
- column.setMaskingAlgorithmId(recognizer.maskingAlgorithmId());
- column.setSensitiveRuleId(recognizer.sensitiveRuleId());
- column.setLevel(recognizer.sensitiveLevel());
- sensitiveColumns.add(column);
- existsSensitiveColumns
- .add(new SensitiveColumnMeta(database.getId(), objectName, dbTableColumn.getName()));
+ if (object2Columns.isEmpty()) {
+ return;
+ }
+
+ List> tableFutures = object2Columns.entrySet().stream()
+ .map(entry -> CompletableFuture.runAsync(() -> {
+ String objectName = entry.getKey();
+ List columns = entry.getValue();
+
+ try {
+ if (taskInfo.isCancelled()) {
+ return;
+ }
+ Map scanResults = this.scanner.scanBatch(columns, this.scanningMode);
+ if (taskInfo.isCancelled()) {
+ return;
+ }
+
+ List sensitiveColumns = new ArrayList<>();
+ for (DBTableColumn dbTableColumn : columns) {
+ String columnKey = getColumnKey(dbTableColumn);
+ ScanResult scanResult = scanResults.get(columnKey);
+
+ if (scanResult != null) {
+ Optional finalResultOpt = scanResult
+ .getFinalResult(this.scanningMode);
+ finalResultOpt.ifPresent(finalResult -> {
+ SensitiveColumnMeta meta = new SensitiveColumnMeta(database.getId(), objectName,
+ dbTableColumn.getName());
+ synchronized (existsSensitiveColumns) {
+ if (!existsSensitiveColumns.contains(meta)) {
+ SensitiveColumn column = createSensitiveColumn(columnType, objectName,
+ dbTableColumn,
+ finalResult);
+ sensitiveColumns.add(column);
+ existsSensitiveColumns.add(meta);
+ }
+ }
+ });
+ }
+ }
+ if (!sensitiveColumns.isEmpty()) {
+ taskInfo.addSensitiveColumns(sensitiveColumns);
+ }
+ taskInfo.addFinishedTableCount();
+ } catch (Exception e) {
+ log.error("Failed to scan table {}: {}", objectName, e.getMessage(), e);
+ taskInfo.addFinishedTableCount();
+ }
+ }))
+ .collect(Collectors.toList());
+
+ CompletableFuture.allOf(tableFutures.toArray(new CompletableFuture[0])).join();
+ }
+
+ private SensitiveColumn createSensitiveColumn(SensitiveColumnType columnType, String objectName,
+ DBTableColumn dbTableColumn, RecognitionResult result) {
+ SensitiveColumn column = new SensitiveColumn();
+ column.setType(columnType);
+ column.setDatabase(database);
+ column.setTableName(objectName);
+ column.setColumnName(dbTableColumn.getName());
+ column.setSensitiveRuleId(result.getMatchedRuleId());
+ column.setLevel(result.getLevel());
+ Long maskingAlgorithmId = determineMaskingAlgorithmId(result);
+ column.setMaskingAlgorithmId(maskingAlgorithmId);
+
+ return column;
+ }
+
+ private Long determineMaskingAlgorithmId(RecognitionResult result) {
+ SensitiveRule matchedRule = this.ruleMap.get(result.getMatchedRuleId());
+ if (matchedRule == null) {
+ return getSystemDefaultAlgorithmId();
+ }
+
+ if (SensitiveRuleType.AI.equals(result.getSourceRuleType()) && result.getSensitiveType() != null) {
+ return handleAiRecognitionResult(result.getSensitiveType());
+ }
+
+ return matchedRule.getMaskingAlgorithmId();
+ }
+
+ private Long handleAiRecognitionResult(String sensitiveType) {
+ if (DefaultSensitiveType.isDefaultType(sensitiveType)) {
+ Optional algorithmNameOpt = DefaultSensitiveType.getAlgorithmNameBySensitiveType(sensitiveType);
+ if (algorithmNameOpt.isPresent()) {
+ try {
+ MaskingAlgorithmService algorithmService = SpringContextUtil.getBean(MaskingAlgorithmService.class);
+ Optional algorithmIdOpt = algorithmService.getAlgorithmIdByName(algorithmNameOpt.get(),
+ database.getOrganizationId());
+ if (algorithmIdOpt.isPresent()) {
+ return algorithmIdOpt.get();
+ }
+ } catch (Exception e) {
+ log.error("Failed to get algorithm ID by name: {}", e.getMessage(), e);
}
}
- taskInfo.addSensitiveColumns(sensitiveColumns);
- taskInfo.addFinishedTableCount();
}
+
+ return getSystemDefaultAlgorithmId();
}
+ private Long getSystemDefaultAlgorithmId() {
+ try {
+ MaskingAlgorithmService algorithmService = SpringContextUtil.getBean(MaskingAlgorithmService.class);
+ return algorithmService.getDefaultAlgorithmIdByOrganizationId(database.getOrganizationId());
+ } catch (Exception e) {
+ log.error("Failed to get default masking algorithm ID: {}", e.getMessage(), e);
+ return null;
+ }
+ }
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManager.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManager.java
index 72a93c1676..09102f78ca 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManager.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnScanningTaskManager.java
@@ -37,6 +37,7 @@
import com.oceanbase.odc.core.shared.constant.ResourceType;
import com.oceanbase.odc.service.connection.database.model.Database;
import com.oceanbase.odc.service.connection.model.ConnectionConfig;
+import com.oceanbase.odc.service.datasecurity.model.ScanningModeType;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnMeta;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo.ScanningTaskStatus;
@@ -65,6 +66,7 @@ public class SensitiveColumnScanningTaskManager {
private StatefulUuidStateIdGenerator statefulUuidStateIdGenerator;
public SensitiveColumnScanningTaskInfo start(List databases, List rules,
+ ScanningModeType scanningMode,
ConnectionConfig connectionConfig, Map> databaseId2SensitiveColumns) {
ConnectionSession session = new DefaultConnectSessionFactory(connectionConfig).generateSession();
try {
@@ -101,8 +103,8 @@ public SensitiveColumnScanningTaskInfo start(List databases, List()),
+ SensitiveColumnScanningTask subTask = new SensitiveColumnScanningTask(database, rules, scanningMode,
+ taskInfo, sensitiveColumns, database2Table2ColumnsList.getOrDefault(database, new HashMap<>()),
database2View2ColumnsList.getOrDefault(database, new HashMap<>()));
try {
executor.submit(subTask);
@@ -132,4 +134,17 @@ public SensitiveColumnScanningTaskInfo get(String taskId) {
return taskInfo;
}
+ public boolean stop(String taskId) {
+ SensitiveColumnScanningTaskInfo taskInfo = cache.get(taskId);
+ if (taskInfo != null && taskInfo.getStatus() == ScanningTaskStatus.RUNNING) {
+ taskInfo.setCancelled(true);
+ taskInfo.setStatus(ScanningTaskStatus.CANCELLED);
+ taskInfo.setCompleteTime(new Date());
+ log.info("Sensitive column scanning task stopped, taskId: {}", taskId);
+ return true;
+ }
+ return false;
+ }
+
}
+
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnService.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnService.java
index 6569b068d9..950fd57888 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnService.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveColumnService.java
@@ -23,7 +23,9 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
import javax.validation.Valid;
@@ -61,15 +63,22 @@
import com.oceanbase.odc.service.connection.database.model.Database;
import com.oceanbase.odc.service.connection.model.ConnectionConfig;
import com.oceanbase.odc.service.datasecurity.extractor.model.DBColumn;
+import com.oceanbase.odc.service.datasecurity.factory.ScanningStrategyFactory;
import com.oceanbase.odc.service.datasecurity.model.DatabaseWithAllColumns;
import com.oceanbase.odc.service.datasecurity.model.MaskingAlgorithm;
import com.oceanbase.odc.service.datasecurity.model.QuerySensitiveColumnParams;
+import com.oceanbase.odc.service.datasecurity.model.RecognitionResult;
+import com.oceanbase.odc.service.datasecurity.model.ScanResult;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumn;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnMeta;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningReq;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnScanningTaskInfo;
import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnStats;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveColumnType;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveLevel;
import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRuleType;
+import com.oceanbase.odc.service.datasecurity.model.SingleTableScanReq;
import com.oceanbase.odc.service.datasecurity.util.SensitiveColumnMapper;
import com.oceanbase.odc.service.db.browser.DBSchemaAccessors;
import com.oceanbase.odc.service.feature.VersionDiffConfigService;
@@ -115,6 +124,8 @@ public class SensitiveColumnService {
private HorizontalDataPermissionValidator permissionValidator;
@Autowired
private VersionDiffConfigService versionDiffConfigService;
+ @Autowired
+ private SingleTableScanTaskManager singleTableScanTaskManager;
@Transactional(rollbackFor = Exception.class)
@PreAuthenticate(hasAnyResourceRole = {"OWNER, DBA, SECURITY_ADMINISTRATOR"},
@@ -399,7 +410,8 @@ public SensitiveColumnScanningTaskInfo startScanning(@NotNull Long projectId,
PreConditions.notEmpty(rules, "sensitiveRules");
ConnectionConfig connectionConfig = databaseService.findDataSourceForConnectById(databases.get(0).getId());
Map> databaseId2SensitiveColumns = listExistSensitiveColumns(databaseIds);
- return scanningTaskManager.start(databases, rules, connectionConfig, databaseId2SensitiveColumns);
+ return scanningTaskManager.start(databases, rules, req.getScanningMode(), connectionConfig,
+ databaseId2SensitiveColumns);
}
@Transactional(rollbackFor = Exception.class)
@@ -416,6 +428,21 @@ public SensitiveColumnScanningTaskInfo getScanningResults(@NotNull Long projectI
return taskInfo;
}
+ @Transactional(rollbackFor = Exception.class)
+ @PreAuthenticate(hasAnyResourceRole = {"OWNER, DBA, SECURITY_ADMINISTRATOR"},
+ actions = {"OWNER", "DBA", "SECURITY_ADMINISTRATOR"}, resourceType = "ODC_PROJECT",
+ indexOfIdParam = 0)
+ @StatefulRoute(stateName = StateName.UUID_STATEFUL_ID, stateIdExpression = "#taskId")
+ public Boolean stopScanning(@NotNull Long projectId, @NotBlank String taskId) {
+ SensitiveColumnScanningTaskInfo taskInfo = scanningTaskManager.get(taskId);
+ if (!Objects.equals(taskInfo.getProjectId(), projectId)) {
+ String errorMsg = String.format("Sensitive column scanning task not exists, taskId=%s", taskId);
+ throw new NotFoundException(ErrorCodes.IllegalArgument, new Object[] {"taskId", errorMsg}, null);
+ }
+ return scanningTaskManager.stop(taskId);
+ }
+
+
@SkipAuthorize("odc internal usages")
public SensitiveColumnEntity nullSafeGet(@NotNull Long id) {
return repository.findById(id)
@@ -527,4 +554,126 @@ private Map> getFilteringExistColumns(Long databaseI
return filtered;
}
+ @Transactional(rollbackFor = Exception.class)
+ @PreAuthenticate(hasAnyResourceRole = {"OWNER, DBA, SECURITY_ADMINISTRATOR"}, actions = {"OWNER", "DBA",
+ "SECURITY_ADMINISTRATOR"}, resourceType = "ODC_PROJECT", indexOfIdParam = 0)
+ public String scanSingleTableAsync(@NotNull Long projectId, @NotNull @Valid SingleTableScanReq req) {
+ final Long currentUserId = authenticationFacade.currentUserId();
+ final Long currentOrganizationId = authenticationFacade.currentOrganizationId();
+ final String currentUserAccountName = authenticationFacade.currentUserAccountName();
+ String taskId = UUID.randomUUID().toString();
+ singleTableScanTaskManager.startTask(taskId, () -> {
+ try {
+ com.oceanbase.odc.service.iam.util.SecurityContextUtils.setCurrentUser(
+ currentUserId, currentOrganizationId, currentUserAccountName);
+
+ List result = performSingleTableScan(projectId, req);
+ singleTableScanTaskManager.setTaskResult(taskId, result);
+ } catch (Exception e) {
+ log.error("Single table scan failed for taskId: {}, projectId: {}, databaseId: {}, tableName: {}",
+ taskId, projectId, req.getDatabaseId(), req.getTableName(), e);
+ String errorMessage = e.getMessage() != null ? e.getMessage()
+ : "An unknown error occurred during the scanning process: " + e.getClass().getSimpleName();
+ singleTableScanTaskManager.setTaskError(taskId, errorMessage);
+ }
+ });
+ return taskId;
+ }
+
+ @PreAuthenticate(hasAnyResourceRole = {"OWNER, DBA, SECURITY_ADMINISTRATOR"}, actions = {"OWNER", "DBA",
+ "SECURITY_ADMINISTRATOR"}, resourceType = "ODC_PROJECT", indexOfIdParam = 0)
+ public SingleTableScanTaskManager.SingleTableScanTask getSingleTableScanResult(@NotNull Long projectId,
+ @NotBlank String taskId) {
+ return singleTableScanTaskManager.getTask(taskId);
+ }
+
+ private List performSingleTableScan(@NotNull Long projectId,
+ @NotNull @Valid SingleTableScanReq req) {
+ Database database = databaseService.detail(req.getDatabaseId());
+ PreConditions.notNull(database, "database");
+ checkProjectDatabases(projectId, Collections.singletonList(req.getDatabaseId()));
+ ConnectionConfig connectionConfig = connectionService
+ .getForConnectionSkipPermissionCheck(database.getDataSource().getId());
+ List tableColumns = getTableColumns(connectionConfig, database.getName(), req.getTableName());
+ if (CollectionUtils.isEmpty(tableColumns)) {
+ return Collections.emptyList();
+ }
+ List rules = getScanningRules(projectId, null);
+ if (CollectionUtils.isEmpty(rules)) {
+ return Collections.emptyList();
+ }
+ ScanningStrategyFactory strategyFactory = new ScanningStrategyFactory();
+ SensitiveColumnScanner scanner = new SensitiveColumnScanner(rules, strategyFactory);
+ Map scanResults = scanner.scanBatch(tableColumns, req.getScanningMode());
+
+ List results = new ArrayList<>();
+ for (DBTableColumn column : tableColumns) {
+ String columnKey = String.format("%s.%s.%s",
+ column.getSchemaName() != null ? column.getSchemaName() : "unknown_schema",
+ column.getTableName() != null ? column.getTableName() : "unknown_table",
+ column.getName() != null ? column.getName() : "unknown_column");
+
+ ScanResult scanResult = scanResults.get(columnKey);
+ if (scanResult != null) {
+ Optional finalResult = scanResult.getFinalResult(req.getScanningMode());
+
+ if (finalResult.isPresent()) {
+ RecognitionResult result = finalResult.get();
+ SensitiveColumn sensitiveColumn = new SensitiveColumn();
+ sensitiveColumn.setDatabase(database);
+ sensitiveColumn.setTableName(column.getTableName());
+ sensitiveColumn.setColumnName(column.getName());
+ sensitiveColumn.setType(SensitiveColumnType.TABLE_COLUMN);
+ sensitiveColumn.setEnabled(true);
+ sensitiveColumn.setSensitiveRuleId(result.getMatchedRuleId());
+ sensitiveColumn.setLevel(result.getLevel());
+ SensitiveRule matchedRule = rules.stream()
+ .filter(r -> r.getId().equals(result.getMatchedRuleId()))
+ .findFirst()
+ .orElse(null);
+ if (matchedRule != null && matchedRule.getMaskingAlgorithmId() != null) {
+ sensitiveColumn.setMaskingAlgorithmId(matchedRule.getMaskingAlgorithmId());
+ } else {
+ sensitiveColumn.setMaskingAlgorithmId(1L);
+ }
+ results.add(sensitiveColumn);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private List getTableColumns(ConnectionConfig connectionConfig, String databaseName,
+ String tableName) {
+ ConnectionSession session = new DefaultConnectSessionFactory(connectionConfig).generateSession();
+ try {
+ DBSchemaAccessor accessor = DBSchemaAccessors.create(session);
+ return accessor.listTableColumns(databaseName, tableName);
+ } finally {
+ session.expire();
+ }
+ }
+
+ private List getScanningRules(Long projectId, List sensitiveRuleIds) {
+ SensitiveRule defaultRule = createDefaultScanningRule();
+ return Collections.singletonList(defaultRule);
+ }
+
+ private SensitiveRule createDefaultScanningRule() {
+ SensitiveRule rule = new SensitiveRule();
+ rule.setId(-1L);
+ rule.setName("Single Table Scan Default Rule");
+ rule.setEnabled(true);
+ rule.setType(SensitiveRuleType.AI);
+ rule.setAiSensitiveTypes(null);
+ rule.setAiCustomPrompt(null);
+ Long organizationId = authenticationFacade.currentOrganizationId();
+ Long defaultAlgorithmId = algorithmService.getDefaultAlgorithmIdByOrganizationId(organizationId);
+ rule.setMaskingAlgorithmId(defaultAlgorithmId);
+ rule.setLevel(SensitiveLevel.MEDIUM);
+ rule.setBuiltin(true);
+ return rule;
+ }
}
+
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveRuleService.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveRuleService.java
index 0bc0fb9b05..22b97dd0bd 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveRuleService.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SensitiveRuleService.java
@@ -160,6 +160,8 @@ public SensitiveRule update(@NotNull Long projectId, @NotNull Long id, @NotNull
entity.setLevel(rule.getLevel());
entity.setMaskingAlgorithmId(algorithmId);
entity.setDescription(rule.getDescription());
+ entity.setAiSensitiveTypes(rule.getAiSensitiveTypes());
+ entity.setAiCustomPrompt(rule.getAiCustomPrompt());
ruleRepository.saveAndFlush(entity);
log.info("Sensitive rule has been updated, id={}, name={}", entity.getId(), entity.getName());
return detail(entity.getProjectId(), entity.getId());
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SingleTableScanTaskManager.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SingleTableScanTaskManager.java
new file mode 100644
index 0000000000..2472426bc6
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/SingleTableScanTaskManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Component;
+
+import com.oceanbase.odc.service.datasecurity.model.SensitiveColumn;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Single-table scan task manager
+ */
+@Slf4j
+@Component
+public class SingleTableScanTaskManager {
+
+ private final Map tasks = new ConcurrentHashMap<>();
+
+ @Autowired
+ @Qualifier("scanSensitiveColumnExecutor")
+ private ThreadPoolTaskExecutor executor;
+
+ public String startTask(String taskId, Runnable scanTask) {
+ SingleTableScanTask task = new SingleTableScanTask(taskId);
+ tasks.put(taskId, task);
+ executor.submit(() -> {
+ try {
+ task.setStatus(TaskStatus.RUNNING);
+ scanTask.run();
+ task.setStatus(TaskStatus.COMPLETED);
+ } catch (Exception e) {
+ log.error("Single table scan task failed, taskId={}", taskId, e);
+ task.setStatus(TaskStatus.FAILED);
+ task.setErrorMessage(e.getMessage());
+ }
+ });
+
+ return taskId;
+ }
+
+ public String startTask(Runnable scanTask) {
+ String taskId = UUID.randomUUID().toString();
+ return startTask(taskId, scanTask);
+ }
+
+ public SingleTableScanTask getTask(String taskId) {
+ return tasks.get(taskId);
+ }
+
+ public void setTaskResult(String taskId, List result) {
+ SingleTableScanTask task = tasks.get(taskId);
+ if (task != null) {
+ task.setResult(result);
+ }
+ }
+
+ public void setTaskError(String taskId, String errorMessage) {
+ SingleTableScanTask task = tasks.get(taskId);
+ if (task != null) {
+ task.setStatus(TaskStatus.FAILED);
+ task.setErrorMessage(errorMessage);
+ }
+ }
+
+ public void cleanupTask(String taskId) {
+ tasks.remove(taskId);
+ }
+
+ public enum TaskStatus {
+ PENDING, RUNNING, COMPLETED, FAILED
+ }
+
+ @Data
+ public static class SingleTableScanTask {
+ private final String taskId;
+ private TaskStatus status = TaskStatus.PENDING;
+ private List result;
+ private String errorMessage;
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIConfig.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIConfig.java
new file mode 100644
index 0000000000..a8a81a27b4
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.ai;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+import com.oceanbase.odc.core.shared.constant.ErrorCodes;
+import com.oceanbase.odc.core.shared.exception.BadRequestException;
+import com.openai.client.OpenAIClient;
+import com.openai.client.okhttp.OpenAIOkHttpClient;
+import com.openai.core.JsonBoolean;
+import com.openai.core.JsonNumber;
+import com.openai.core.JsonValue;
+
+import lombok.Data;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Data
+@Component
+public class AIConfig {
+ @Value("${odc.ai.enabled:false}")
+ private boolean enabled;
+
+ @Value("${odc.ai.api-key:}")
+ private String apiKey;
+
+ @Value("${odc.ai.base-url:https://api.openai.com}")
+ private String baseUrl;
+
+ @Value("${odc.ai.model:gpt-3.5-turbo}")
+ private String model;
+
+ private Boolean enableThinking = AIParam.DEFAULT_ENABLE_THINKING;
+
+ private Double temperature = AIParam.DEFAULT_TEMPERATURE;
+
+ private Double topP = AIParam.DEFAULT_TOP_P;
+
+ private Integer topK = AIParam.DEFAULT_TOP_K;
+
+ private Integer minP = AIParam.DEFAULT_MIN_P;
+
+ public Map loadAdditionalParams() {
+ Map params = new HashMap<>();
+ params.put("enable_thinking", JsonBoolean.from(this.enableThinking));
+ params.put("top_k", JsonNumber.from(this.topK));
+ params.put("min_p", JsonNumber.from(this.minP));
+ return params;
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "odc.ai.enabled", havingValue = "true")
+ public OpenAIClient openAIClient() {
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ throw new BadRequestException(ErrorCodes.AIConfigurationIncomplete,
+ new Object[] {"API key is not configured"},
+ "AI service is enabled but API key is not configured. Please set odc.ai.api-key configuration.");
+ }
+ return OpenAIOkHttpClient.builder()
+ .apiKey(this.apiKey)
+ .baseUrl(this.baseUrl)
+ .build();
+ }
+
+ public boolean isAIAvailable() {
+ return enabled && apiKey != null && !apiKey.trim().isEmpty();
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIInferenceService.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIInferenceService.java
new file mode 100644
index 0000000000..a1bf62e906
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIInferenceService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.ai;
+
+import java.util.Optional;
+
+import org.springframework.stereotype.Service;
+
+import com.oceanbase.odc.core.shared.constant.ErrorCodes;
+import com.oceanbase.odc.core.shared.exception.BadRequestException;
+import com.openai.client.OpenAIClient;
+import com.openai.models.chat.completions.ChatCompletion;
+import com.openai.models.chat.completions.ChatCompletionCreateParams;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Service
+public class AIInferenceService {
+
+ private final AIConfig aiConfig;
+ private final Optional openAIClient;
+
+ public AIInferenceService(AIConfig aiConfig, Optional openAIClient) {
+ this.aiConfig = aiConfig;
+ this.openAIClient = openAIClient;
+ }
+
+ private void checkAIAvailability() {
+ if (!aiConfig.isEnabled()) {
+ throw new BadRequestException(ErrorCodes.AIServiceNotAvailable, new Object[] {"AI service is not enabled"},
+ "AI service is not enabled. Please contact administrator to enable AI service.");
+ }
+ if (!aiConfig.isAIAvailable()) {
+ throw new BadRequestException(ErrorCodes.AIConfigurationIncomplete,
+ new Object[] {"AI configuration is incomplete"},
+ "AI configuration is incomplete. Please contact administrator to configure AI parameters.");
+ }
+ if (!openAIClient.isPresent()) {
+ throw new BadRequestException(ErrorCodes.AIClientNotInitialized,
+ new Object[] {"AI client is not initialized"},
+ "AI client is not initialized. Please check AI configuration and restart service.");
+ }
+ }
+
+ public ChatCompletion chat(String systemPrompt, String userPrompt) {
+ checkAIAvailability();
+
+ try {
+ ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
+ .addSystemMessage(systemPrompt)
+ .addUserMessage(userPrompt)
+ .model(aiConfig.getModel())
+ .temperature(aiConfig.getTemperature())
+ .topP(aiConfig.getTopP())
+ .additionalBodyProperties(aiConfig.loadAdditionalParams())
+ .build();
+ return openAIClient.get().chat().completions().create(params);
+ } catch (Exception e) {
+ throw new BadRequestException(ErrorCodes.AIInferenceServiceError, new Object[] {e.getMessage()},
+ "Failed to call AI inference service: " + e.getMessage(), e);
+ }
+ }
+
+ public boolean isAIAvailable() {
+ return aiConfig.isEnabled() && aiConfig.isAIAvailable() && openAIClient.isPresent();
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIParam.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIParam.java
new file mode 100644
index 0000000000..b5793e0664
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIParam.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.ai;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+public class AIParam {
+
+ /**
+ * Default values for AI configuration
+ */
+ public static final Boolean DEFAULT_ENABLE_THINKING = false;
+ public static final Double DEFAULT_TEMPERATURE = 0.1;
+ public static final Double DEFAULT_TOP_P = 1.0;
+ public static final Integer DEFAULT_TOP_K = 0;
+ public static final Integer DEFAULT_MIN_P = 0;
+
+ public static final Integer DEFAULT_BATCH_SIZE_IN_TABLE = 30;
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIStatusResponse.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIStatusResponse.java
new file mode 100644
index 0000000000..d5ed6d977b
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/AIStatusResponse.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.ai;
+
+import lombok.Data;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Data
+public class AIStatusResponse {
+ private boolean enabled;
+
+ private boolean available;
+
+ private String model;
+
+ private String baseUrl;
+
+ private boolean apiKeyConfigured;
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/PromptTemplateLoader.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/PromptTemplateLoader.java
new file mode 100644
index 0000000000..e36d837239
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ai/PromptTemplateLoader.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.ai;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+
+import org.springframework.stereotype.Component;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.var;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Slf4j
+@Component
+public class PromptTemplateLoader {
+ private static final String SYSTEM_TEMPLATE_PATH =
+ "/ai-prompt-template/sensitive_column_recognize_system_prompt.txt";
+ private static final String TYPES_PLACEHOLDER = "{sensitiveTypes}";
+ private static final String PROMPT_PLACEHOLDER = "{customPrompt}";
+
+ private String systemTemplate;
+
+ @PostConstruct
+ public void init() {
+ try (var inputStream = PromptTemplateLoader.class.getResourceAsStream(SYSTEM_TEMPLATE_PATH)) {
+ if (Objects.isNull(inputStream)) {
+ throw new IllegalStateException("AI system prompt template file not found: " + SYSTEM_TEMPLATE_PATH);
+ }
+ try (var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream))) {
+ this.systemTemplate = reader.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ } catch (Exception e) {
+ log.error("Failed to load AI system prompt template: {}", e.getMessage(), e);
+ throw new IllegalStateException("Failed to load AI system prompt template", e);
+ }
+ }
+
+ public String buildSystemPrompt(List sensitiveTypes, String customPrompt) {
+ if (this.systemTemplate == null || this.systemTemplate.isEmpty()) {
+ throw new IllegalStateException("System prompt template is not available. Check loading status.");
+ }
+
+ String formattedTypes = (sensitiveTypes == null || sensitiveTypes.isEmpty())
+ ? "No specified category."
+ : String.join(", ", sensitiveTypes);
+
+ String formattedPrompt = (customPrompt == null || customPrompt.trim().isEmpty())
+ ? "No supplementary rule."
+ : customPrompt;
+
+ return this.systemTemplate
+ .replace(TYPES_PLACEHOLDER, formattedTypes)
+ .replace(PROMPT_PLACEHOLDER, formattedPrompt);
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ColumnRecognizerFactory.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ColumnRecognizerFactory.java
similarity index 79%
rename from server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ColumnRecognizerFactory.java
rename to server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ColumnRecognizerFactory.java
index 50cfc35c7a..c38ca4034e 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/ColumnRecognizerFactory.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ColumnRecognizerFactory.java
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.oceanbase.odc.service.datasecurity;
+package com.oceanbase.odc.service.datasecurity.factory;
import com.oceanbase.odc.core.shared.constant.ErrorCodes;
import com.oceanbase.odc.core.shared.exception.UnsupportedException;
import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
+import com.oceanbase.odc.service.datasecurity.recognizer.AIColumnRecognizer;
import com.oceanbase.odc.service.datasecurity.recognizer.ColumnRecognizer;
import com.oceanbase.odc.service.datasecurity.recognizer.GroovyColumnRecognizer;
import com.oceanbase.odc.service.datasecurity.recognizer.PathColumnRecognizer;
@@ -34,16 +35,16 @@ public class ColumnRecognizerFactory {
public static ColumnRecognizer create(@NonNull SensitiveRule rule) {
switch (rule.getType()) {
case REGEX:
- return new RegexColumnRecognizer(rule.getDatabaseRegexExpression(), rule.getTableRegexExpression(),
- rule.getColumnRegexExpression(), rule.getColumnCommentRegexExpression());
+ return new RegexColumnRecognizer(rule);
case PATH:
- return new PathColumnRecognizer(rule.getPathIncludes(), rule.getPathExcludes());
+ return new PathColumnRecognizer(rule);
case GROOVY:
- return new GroovyColumnRecognizer(rule.getGroovyScript());
+ return new GroovyColumnRecognizer(rule);
+ case AI:
+ return new AIColumnRecognizer(rule);
default:
String errorMsg = String.format("Unsupported sensitive rule type: %s", rule.getType().name());
throw new UnsupportedException(ErrorCodes.BadArgument, new Object[] {errorMsg}, errorMsg);
}
}
-
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ScanningStrategyFactory.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ScanningStrategyFactory.java
new file mode 100644
index 0000000000..46bbba6109
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/factory/ScanningStrategyFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.factory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.stereotype.Component;
+
+import com.oceanbase.odc.service.datasecurity.model.ScanResult;
+import com.oceanbase.odc.service.datasecurity.model.ScanningModeType;
+import com.oceanbase.odc.service.datasecurity.strategy.AIOnlyStrategy;
+import com.oceanbase.odc.service.datasecurity.strategy.JointRecognitionStrategy;
+import com.oceanbase.odc.service.datasecurity.strategy.RulesOnlyStrategy;
+import com.oceanbase.odc.service.datasecurity.strategy.ScanningStrategy;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Component
+public class ScanningStrategyFactory {
+
+ private final Map strategies = new HashMap<>();
+
+ public ScanningStrategyFactory() {
+ strategies.put(ScanningModeType.RULES_ONLY, new RulesOnlyStrategy());
+ strategies.put(ScanningModeType.JOINT_RECOGNITION, new JointRecognitionStrategy());
+ strategies.put(ScanningModeType.AI_ONLY, new AIOnlyStrategy());
+ }
+
+ public ScanningStrategy getStrategy(ScanningModeType mode) {
+ ScanningStrategy strategy = strategies.get(mode);
+ if (strategy == null) {
+ return new NoOpStrategy();
+ }
+ return strategy;
+ }
+
+ private static class NoOpStrategy implements ScanningStrategy {
+ @Override
+ public ScanResult scan(DBTableColumn column,
+ java.util.List basicRecognizers,
+ java.util.List aiRecognizers) {
+ return new ScanResult(Optional.empty(), Optional.empty());
+ }
+
+ @Override
+ public Map scanBatch(java.util.List columns,
+ java.util.List basicRecognizers,
+ java.util.List aiRecognizers) {
+ Map results = new HashMap<>();
+ for (DBTableColumn column : columns) {
+ String columnKey = String.format("%s.%s.%s",
+ column.getSchemaName() != null ? column.getSchemaName() : "unknown_schema",
+ column.getTableName() != null ? column.getTableName() : "unknown_table",
+ column.getName() != null ? column.getName() : "unknown_column");
+ results.put(columnKey, new ScanResult(Optional.empty(), Optional.empty()));
+ }
+ return results;
+ }
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/DefaultSensitiveType.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/DefaultSensitiveType.java
new file mode 100644
index 0000000000..7fd990f5df
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/DefaultSensitiveType.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.model;
+
+import java.util.Optional;
+
+/**
+ * 13 Default Sensitivity Types Identified by AI
+ *
+ * @author fenyf
+ * @date 2025/8/1
+ */
+public enum DefaultSensitiveType {
+
+ PERSONAL_NAME_CHINESE("${com.oceanbase.odc.builtin-resource.masking-algorithm.personal-name-chinese.name}"),
+
+ PERSONAL_NAME_ALPHABET("${com.oceanbase.odc.builtin-resource.masking-algorithm.personal-name-alphabet.name}"),
+
+ NICKNAME("${com.oceanbase.odc.builtin-resource.masking-algorithm.nickname.name}"),
+
+ EMAIL("${com.oceanbase.odc.builtin-resource.masking-algorithm.email.name}"),
+
+ ADDRESS("${com.oceanbase.odc.builtin-resource.masking-algorithm.address.name}"),
+
+ PHONE_NUMBER("${com.oceanbase.odc.builtin-resource.masking-algorithm.phone-number.name}"),
+
+ FIXED_LINE_PHONE_NUMBER("${com.oceanbase.odc.builtin-resource.masking-algorithm.fixed-line-phone-number.name}"),
+
+ CERTIFICATE_NUMBER("${com.oceanbase.odc.builtin-resource.masking-algorithm.certificate-number.name}"),
+
+ BANK_CARD_NUMBER("${com.oceanbase.odc.builtin-resource.masking-algorithm.bank-card-number.name}"),
+
+ LICENSE_PLATE_NUMBER("${com.oceanbase.odc.builtin-resource.masking-algorithm.license-plate-number.name}"),
+
+ DEVICE_ID("${com.oceanbase.odc.builtin-resource.masking-algorithm.device-id.name}"),
+
+ IP("${com.oceanbase.odc.builtin-resource.masking-algorithm.ip.name}"),
+
+ MAC("${com.oceanbase.odc.builtin-resource.masking-algorithm.mac.name}");
+
+ private final String algorithmName;
+
+ DefaultSensitiveType(String algorithmName) {
+ this.algorithmName = algorithmName;
+ }
+
+ public String getAlgorithmName() {
+ return algorithmName;
+ }
+
+ public static boolean isDefaultType(String sensitiveType) {
+ return findBestMatch(sensitiveType).isPresent();
+ }
+
+ /**
+ * Retrieve the corresponding algorithm name based on the name of the sensitive type.
+ */
+ public static Optional getAlgorithmNameBySensitiveType(String sensitiveType) {
+ return findBestMatch(sensitiveType).map(DefaultSensitiveType::getAlgorithmName);
+ }
+
+ public static Optional getByDisplayName(String sensitiveType) {
+ return findBestMatch(sensitiveType);
+ }
+
+ /**
+ * Match sensitive type
+ */
+ public static Optional findBestMatch(String sensitiveType) {
+ if (sensitiveType == null || sensitiveType.trim().isEmpty()) {
+ return Optional.empty();
+ }
+
+ String normalized = sensitiveType.toLowerCase().trim();
+
+ for (DefaultSensitiveType type : values()) {
+ if (type.name().toLowerCase().equals(normalized)) {
+ return Optional.of(type);
+ }
+ }
+
+ for (DefaultSensitiveType type : values()) {
+ String hyphenFormat = type.name().toLowerCase().replace("_", "-");
+ if (hyphenFormat.equals(normalized)) {
+ return Optional.of(type);
+ }
+ }
+
+ for (DefaultSensitiveType type : values()) {
+ String enumName = type.name().toLowerCase();
+ String hyphenFormat = enumName.replace("_", "-");
+ if (enumName.contains(normalized) || normalized.contains(enumName.replace("_", "")) ||
+ hyphenFormat.contains(normalized) || normalized.contains(hyphenFormat.replace("-", ""))) {
+ return Optional.of(type);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/RecognitionResult.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/RecognitionResult.java
new file mode 100644
index 0000000000..e3c004a132
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/RecognitionResult.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class RecognitionResult {
+ // 基础信息
+ private boolean matched;
+ private Long matchedRuleId;
+ private SensitiveLevel level;
+ private SensitiveRuleType sourceRuleType;
+
+ // AI 规则
+ private String sensitiveType; // AI 判断出的具体敏感类型
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanResult.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanResult.java
new file mode 100644
index 0000000000..9fe6da5253
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanResult.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.model;
+
+import java.util.Optional;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author fenyf
+ * @date 2025/7/18 17:52
+ */
+@Getter
+@AllArgsConstructor
+public class ScanResult {
+ private final Optional basicRuleResult;
+ private final Optional aiRuleResult;
+
+ public Optional getFinalResult(ScanningModeType scanningMode) {
+ switch (scanningMode) {
+ case RULES_ONLY:
+ return basicRuleResult;
+ case AI_ONLY:
+ return aiRuleResult;
+ case JOINT_RECOGNITION:
+ return basicRuleResult.isPresent() ? basicRuleResult : aiRuleResult;
+ default:
+ return Optional.empty();
+ }
+ }
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanningModeType.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanningModeType.java
new file mode 100644
index 0000000000..5355078bf1
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/ScanningModeType.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.model;
+
+/**
+ * @author fenyf
+ * @date 2025/7/18 17:52
+ */
+public enum ScanningModeType {
+
+ RULES_ONLY,
+
+ JOINT_RECOGNITION,
+
+ AI_ONLY;
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningReq.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningReq.java
index fd77c7f642..ba36f1c087 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningReq.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningReq.java
@@ -35,4 +35,6 @@ public class SensitiveColumnScanningReq {
@NotNull
private Boolean allSensitiveRules;
private List sensitiveRuleIds;
+ @NotNull
+ private ScanningModeType scanningMode = ScanningModeType.JOINT_RECOGNITION;
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningTaskInfo.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningTaskInfo.java
index 9c38d88b29..68cfb2f262 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningTaskInfo.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveColumnScanningTaskInfo.java
@@ -41,6 +41,7 @@ public class SensitiveColumnScanningTaskInfo {
private Date completeTime;
private ErrorCode errorCode;
private String errorMsg;
+ private volatile boolean cancelled = false;
public SensitiveColumnScanningTaskInfo(@NonNull String taskId, @NonNull Long projectId,
@NonNull Integer allTableCount) {
@@ -81,14 +82,23 @@ public synchronized void setErrorMsg(String msg) {
this.errorMsg = msg;
}
+ public synchronized void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ public boolean isCancelled() {
+ return this.cancelled;
+ }
+
public enum ScanningTaskStatus {
CREATED,
RUNNING,
SUCCESS,
- FAILED;
+ FAILED,
+ CANCELLED;
public boolean isCompleted() {
- return this == SUCCESS || this == FAILED;
+ return this == SUCCESS || this == FAILED || this == CANCELLED;
}
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRule.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRule.java
index 770038c56a..b812eb59d5 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRule.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRule.java
@@ -53,7 +53,6 @@ public class SensitiveRule implements SecurityResource, SingleOrganizationResour
@JsonProperty(access = Access.READ_ONLY)
private Long projectId;
- @NotNull
private SensitiveRuleType type;
private String databaseRegexExpression;
@@ -70,6 +69,10 @@ public class SensitiveRule implements SecurityResource, SingleOrganizationResour
private List pathExcludes = new ArrayList<>();
+ private List aiSensitiveTypes = new ArrayList<>();
+
+ private String aiCustomPrompt;
+
@NotNull
private Long maskingAlgorithmId;
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRuleType.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRuleType.java
index 0673cbbf95..54ba6a4be0 100644
--- a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRuleType.java
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SensitiveRuleType.java
@@ -33,5 +33,10 @@ public enum SensitiveRuleType {
/**
* Path expression fuzzy match
*/
- PATH
+ PATH,
+
+ /**
+ * AI-based sensitive data detection
+ */
+ AI
}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SingleTableScanReq.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SingleTableScanReq.java
new file mode 100644
index 0000000000..04dbf36e76
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/model/SingleTableScanReq.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.model;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+import lombok.Data;
+
+/**
+ * Single-table sensitive column scan request
+ *
+ * @author fenyf
+ * @date 2025/8/18 17:52
+ */
+@Data
+public class SingleTableScanReq {
+
+ @NotNull
+ private Long databaseId;
+
+ @NotBlank
+ private String tableName;
+
+ @NotNull
+ private ScanningModeType scanningMode = ScanningModeType.JOINT_RECOGNITION;
+
+}
diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/recognizer/AIColumnRecognizer.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/recognizer/AIColumnRecognizer.java
new file mode 100644
index 0000000000..9870d29d00
--- /dev/null
+++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/datasecurity/recognizer/AIColumnRecognizer.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2023 OceanBase.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.oceanbase.odc.service.datasecurity.recognizer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.oceanbase.odc.core.shared.constant.ErrorCodes;
+import com.oceanbase.odc.core.shared.exception.BadRequestException;
+import com.oceanbase.odc.service.common.util.SpringContextUtil;
+import com.oceanbase.odc.service.datasecurity.ai.AIInferenceService;
+import com.oceanbase.odc.service.datasecurity.ai.AIParam;
+import com.oceanbase.odc.service.datasecurity.ai.PromptTemplateLoader;
+import com.oceanbase.odc.service.datasecurity.model.RecognitionResult;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveLevel;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRule;
+import com.oceanbase.odc.service.datasecurity.model.SensitiveRuleType;
+import com.oceanbase.tools.dbbrowser.model.DBTableColumn;
+import com.openai.models.chat.completions.ChatCompletion;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author fenyf
+ * @date 2025/8/10 12:41
+ */
+@Slf4j
+public class AIColumnRecognizer implements ColumnRecognizer {
+
+ private final SensitiveRule aiRule;
+ private static final int BATCH_SIZE = AIParam.DEFAULT_BATCH_SIZE_IN_TABLE;
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ private static final Pattern JSON_PATTERN = Pattern
+ .compile("(?s)```json\\s*([\\{\\[].*[\\}\\]])\\s*```|([\\{\\[].*[\\}\\]])");
+
+ public AIColumnRecognizer(SensitiveRule rule) {
+ this.aiRule = rule;
+ }
+
+ @Override
+ public Optional recognize(DBTableColumn column) {
+ Map> batchResult = recognizeBatch(Collections.singletonList(column));
+ String columnKey = getColumnKey(column);
+ return batchResult.getOrDefault(columnKey, Optional.empty());
+ }
+
+ /**
+ * If the data in the table columns is too large, scan them in batches.
+ *
+ * @param columns list of columns {@link DBTableColumn}
+ * @return
+ */
+ @Override
+ public Map> recognizeBatch(List columns) {
+ if (columns == null || columns.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ PromptTemplateLoader promptTemplateLoader = SpringContextUtil.getBean(PromptTemplateLoader.class);
+ AIInferenceService aiService = SpringContextUtil.getBean(AIInferenceService.class);
+
+ Map> finalAiResults = new HashMap<>();
+
+ if (columns.size() > BATCH_SIZE) {
+ List> batches = Lists.partition(columns, BATCH_SIZE);
+ try {
+ for (List batch : batches) {
+ processBatch(batch, promptTemplateLoader, aiService, finalAiResults);
+ }
+ } catch (BadRequestException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("Failed to process AI column recognition batch", e);
+ return finalAiResults;
+ }
+ } else {
+ try {
+ processBatch(columns, promptTemplateLoader, aiService, finalAiResults);
+ } catch (BadRequestException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("Failed to process AI column recognition", e);
+ return finalAiResults;
+ }
+ }
+ return finalAiResults;
+ }
+
+ /**
+ * Process a single batch of column data
+ */
+ private void processBatch(List batch, PromptTemplateLoader promptTemplateLoader,
+ AIInferenceService aiService, Map> finalAiResults) throws IOException {
+ String systemPrompt = promptTemplateLoader.buildSystemPrompt(aiRule.getAiSensitiveTypes(),
+ aiRule.getAiCustomPrompt());
+ String userPrompt = buildUserPrompt(batch);
+ ChatCompletion completion = aiService.chat(systemPrompt, userPrompt);
+ String rawContent = completion.choices().get(0).message().content().orElse("[]");
+
+ Matcher matcher = JSON_PATTERN.matcher(rawContent);
+ String jsonArrayResponse = null;
+ if (matcher.find()) {
+ jsonArrayResponse = Optional.ofNullable(matcher.group(1)).orElse(matcher.group(2));
+ }
+
+ if (jsonArrayResponse == null) {
+ throw new BadRequestException(ErrorCodes.AIResponseFormatError,
+ new Object[] {"No valid JSON array found in AI response"},
+ "AI response does not contain valid JSON format: " + rawContent);
+ }
+
+ List batchResults;
+ try {
+ batchResults = objectMapper.readValue(jsonArrayResponse,
+ new TypeReference>() {});
+ } catch (Exception e) {
+ throw new BadRequestException(ErrorCodes.AIResponseFormatError,
+ new Object[] {"Failed to parse JSON: " + e.getMessage()},
+ "Failed to parse AI response JSON: " + jsonArrayResponse, e);
+ }
+ int maxIndex = Math.min(batch.size(), batchResults.size());
+ for (int i = 0; i < maxIndex; i++) {
+ DBTableColumn column = batch.get(i);
+ String columnKey = getColumnKey(column);
+ AiResponseDto dto = batchResults.get(i);
+
+ if (dto.isSensitive()) {
+ RecognitionResult result = RecognitionResult.builder()
+ .matched(true)
+ .matchedRuleId(this.aiRule.getId())
+ .level(dto.getRiskLevel())
+ .sourceRuleType(SensitiveRuleType.AI)
+ .sensitiveType(dto.getSensitiveCategory())
+ .build();
+ finalAiResults.put(columnKey, Optional.of(result));
+ } else {
+ finalAiResults.put(columnKey, Optional.empty());
+ }
+ }
+ if (batchResults.size() != batch.size()) {
+ log.warn("AI response count ({}) does not match input column count ({})",
+ batchResults.size(), batch.size());
+ }
+ }
+
+ /**
+ * Build user prompt (JSON array of column data)
+ */
+ private String buildUserPrompt(List batch) throws IOException {
+ List