diff --git a/packages/shared/lib/api/sqle/service/common.enum.ts b/packages/shared/lib/api/sqle/service/common.enum.ts index c86fd54c0..b7a0f0f00 100644 --- a/packages/shared/lib/api/sqle/service/common.enum.ts +++ b/packages/shared/lib/api/sqle/service/common.enum.ts @@ -297,7 +297,11 @@ export enum MatchConditionReqV1TypeEnum { db_type = 'db_type', - sql_source = 'sql_source' + sql_source = 'sql_source', + + schema = 'schema', + + object_name = 'object_name' } export enum OperationRecordListStatusEnum { diff --git a/packages/sqle/src/components/RuleException/AuditResultExemptionPanel/index.tsx b/packages/sqle/src/components/RuleException/AuditResultExemptionPanel/index.tsx index 25d10a6b6..0b91df258 100644 --- a/packages/sqle/src/components/RuleException/AuditResultExemptionPanel/index.tsx +++ b/packages/sqle/src/components/RuleException/AuditResultExemptionPanel/index.tsx @@ -21,10 +21,7 @@ import ExemptedAuditResultWithActions from '../ExemptedAuditResultWithActions'; import CollapsibleExemptedSection from './CollapsibleExemptedSection'; import FullSqlExemptedResultItem from '../FullSqlExemptedResultItem'; import { AuditResultExemptionPanelStyleWrapper } from './style'; -import { - AuditResultExemptionPanelLayout, - AuditResultExemptionPanelProps -} from './types'; +import { AuditResultExemptionPanelProps } from './types'; import { IAuditResultItem } from '../../ReportDrawer/index.type'; export type { AuditResultExemptionPanelLayout } from './types'; diff --git a/packages/sqle/src/components/RuleExceptionMatchConditions/Form/index.tsx b/packages/sqle/src/components/RuleExceptionMatchConditions/Form/index.tsx index cd17af5c5..7fc5328a0 100644 --- a/packages/sqle/src/components/RuleExceptionMatchConditions/Form/index.tsx +++ b/packages/sqle/src/components/RuleExceptionMatchConditions/Form/index.tsx @@ -268,6 +268,32 @@ const MatchRowContentField: React.FC = ({ ); } + if ( + type === MatchConditionReqV1TypeEnum.schema || + type === MatchConditionReqV1TypeEnum.object_name + ) { + return ( + + + + + + + + + ); + } + return ( = { sql: t('ruleException.matchType.sql'), @@ -31,6 +33,10 @@ export const AuditWhitelistMatchTypeDirection: Record< [MatchConditionReqV1TypeEnum.db_type]: t('ruleException.matchType.db_type'), [MatchConditionReqV1TypeEnum.sql_source]: t( 'ruleException.matchType.sql_source' + ), + [MatchConditionReqV1TypeEnum.schema]: t('ruleException.matchType.schema'), + [MatchConditionReqV1TypeEnum.object_name]: t( + 'ruleException.matchType.object_name' ) }; @@ -74,6 +80,8 @@ export const AuditWhitelistExtendedMatchTypeOptions = [ MatchConditionReqV1TypeEnum.audit_task_id, MatchConditionReqV1TypeEnum.db_type, MatchConditionReqV1TypeEnum.sql_source, + MatchConditionReqV1TypeEnum.schema, + MatchConditionReqV1TypeEnum.object_name, MatchConditionReqV1TypeEnum.fp_sql, MatchConditionReqV1TypeEnum.sql ].map((value) => ({ @@ -87,7 +95,9 @@ export const AuditWhitelistAllMatchTypeOptions = [ MatchConditionReqV1TypeEnum.audit_task_type, MatchConditionReqV1TypeEnum.audit_task_id, MatchConditionReqV1TypeEnum.db_type, - MatchConditionReqV1TypeEnum.sql_source + MatchConditionReqV1TypeEnum.sql_source, + MatchConditionReqV1TypeEnum.schema, + MatchConditionReqV1TypeEnum.object_name ].map((value) => ({ label: AuditWhitelistMatchTypeDirection[ diff --git a/packages/sqle/src/locale/en-US/ruleException.ts b/packages/sqle/src/locale/en-US/ruleException.ts index 2f272d103..837fd7f4e 100644 --- a/packages/sqle/src/locale/en-US/ruleException.ts +++ b/packages/sqle/src/locale/en-US/ruleException.ts @@ -41,6 +41,9 @@ export default { audit_task_type: 'Scan task type', audit_task_id: 'Scan task', sql_source: 'Source', + schema: 'Schema', + object_name: 'Object name', + objectNameTip: 'Includes tables and views', sqlSource: { sql_audit_record: 'Quick audit', audit_plan: 'Scan task' diff --git a/packages/sqle/src/locale/zh-CN/ruleException.ts b/packages/sqle/src/locale/zh-CN/ruleException.ts index 45b80e9cf..eb5fcadec 100644 --- a/packages/sqle/src/locale/zh-CN/ruleException.ts +++ b/packages/sqle/src/locale/zh-CN/ruleException.ts @@ -41,6 +41,9 @@ export default { audit_task_type: '扫描任务类型', audit_task_id: '扫描任务', sql_source: '来源', + schema: '库名', + object_name: '对象名', + objectNameTip: '含表与视图', sqlSource: { sql_audit_record: '快捷审核', audit_plan: '扫描任务' diff --git a/packages/sqle/src/page/RuleException/__tests__/utils.test.ts b/packages/sqle/src/page/RuleException/__tests__/utils.test.ts index dc2d32b24..947c239c6 100644 --- a/packages/sqle/src/page/RuleException/__tests__/utils.test.ts +++ b/packages/sqle/src/page/RuleException/__tests__/utils.test.ts @@ -11,6 +11,7 @@ import { normalizeMatchRowsOrder, rowsToAuditWhitelistBody, rowsToBlacklistBody, + toSqlManageRuleExceptionRecord, validateAuditWhitelistMatchRows, validateMatchRows } from '../utils'; @@ -87,6 +88,181 @@ describe('sqle/page/RuleException/utils', () => { }); }); + it('buildBlacklistPrefillFromSqlManage prefers schema+object_name when preferSchemaObjectMatch', () => { + expect( + buildBlacklistPrefillFromSqlManage( + { + sql_fingerprint: 'CREATE TABLE `v_exc_target` (`id` int)', + instance_id: '123', + db_type: 'MySQL', + schema_name: 'sqle', + schema_meta_name: 'v_exc_target', + source: { + sql_source_type: 'mysql_schema_meta', + sql_source_ids: ['1'] + } + }, + { preferSchemaObjectMatch: true } + ) + ).toEqual({ + match_conditions: [ + { + type: MatchConditionReqV1TypeEnum.schema, + content: 'sqle' + }, + { + type: MatchConditionReqV1TypeEnum.object_name, + content: 'v_exc_target' + }, + { + type: MatchConditionReqV1TypeEnum.instance, + content: '123' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_type, + content: 'mysql_schema_meta' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_id, + content: '1' + }, + { + type: MatchConditionReqV1TypeEnum.sql_source, + content: 'audit_plan' + }, + { + type: MatchConditionReqV1TypeEnum.db_type, + content: 'MySQL' + } + ] + }); + }); + + it('buildBlacklistPrefillFromSqlManage falls back to fp_sql when preferSchemaObjectMatch lacks object (AC-001b)', () => { + expect( + buildBlacklistPrefillFromSqlManage( + { + sql_fingerprint: 'CREATE TABLE `t` (`id` int)', + instance_id: '123', + db_type: 'MySQL', + schema_name: 'sqle', + source: { + sql_source_type: 'mysql_schema_meta', + sql_source_ids: ['1'] + } + }, + { preferSchemaObjectMatch: true } + ) + ).toEqual({ + match_conditions: [ + { + type: MatchConditionReqV1TypeEnum.fp_sql, + content: 'CREATE TABLE `t` (`id` int)' + }, + { + type: MatchConditionReqV1TypeEnum.instance, + content: '123' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_type, + content: 'mysql_schema_meta' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_id, + content: '1' + }, + { + type: MatchConditionReqV1TypeEnum.sql_source, + content: 'audit_plan' + }, + { + type: MatchConditionReqV1TypeEnum.db_type, + content: 'MySQL' + } + ] + }); + }); + + it('buildBlacklistPrefillFromSqlManage falls back to fp_sql when preferSchemaObjectMatch lacks schema (AC-001b)', () => { + expect( + buildBlacklistPrefillFromSqlManage( + { + sql_fingerprint: 'CREATE TABLE `t` (`id` int)', + instance_id: '123', + db_type: 'MySQL', + schema_meta_name: 't', + source: { + sql_source_type: 'mysql_schema_meta', + sql_source_ids: ['1'] + } + }, + { preferSchemaObjectMatch: true } + ) + ).toEqual({ + match_conditions: [ + { + type: MatchConditionReqV1TypeEnum.fp_sql, + content: 'CREATE TABLE `t` (`id` int)' + }, + { + type: MatchConditionReqV1TypeEnum.instance, + content: '123' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_type, + content: 'mysql_schema_meta' + }, + { + type: MatchConditionReqV1TypeEnum.audit_task_id, + content: '1' + }, + { + type: MatchConditionReqV1TypeEnum.sql_source, + content: 'audit_plan' + }, + { + type: MatchConditionReqV1TypeEnum.db_type, + content: 'MySQL' + } + ] + }); + }); + + it('toSqlManageRuleExceptionRecord reads schema_name and info.schema_meta_name (AC-011 consume)', () => { + const withObject = { + sql_fingerprint: 'CREATE TABLE `v` (`id` int)', + schema_name: 'sqle', + info: { schema_meta_name: 'v_exc_target' } + }; + expect(toSqlManageRuleExceptionRecord(withObject)).toMatchObject({ + schema_name: 'sqle', + schema_meta_name: 'v_exc_target' + }); + + const emptyObjectName = { + sql_fingerprint: 'select 1', + schema_name: 'sqle', + info: { schema_meta_name: '' } + }; + expect( + toSqlManageRuleExceptionRecord(emptyObjectName)?.schema_meta_name + ).toBeUndefined(); + + const missingKey = { + sql_fingerprint: 'select 1', + info: {} + }; + expect( + toSqlManageRuleExceptionRecord(missingKey)?.schema_meta_name + ).toBeUndefined(); + + expect( + toSqlManageRuleExceptionRecord({ + sql_fingerprint: 'select 1' + })?.schema_meta_name + ).toBeUndefined(); + }); + it('buildBlacklistPrefillFromSqlManage skips audit task match for quick audit source', () => { expect( buildBlacklistPrefillFromSqlManage({ diff --git a/packages/sqle/src/page/RuleException/index.type.ts b/packages/sqle/src/page/RuleException/index.type.ts index 06d94c866..fff7af7b5 100644 --- a/packages/sqle/src/page/RuleException/index.type.ts +++ b/packages/sqle/src/page/RuleException/index.type.ts @@ -1,5 +1,4 @@ import { - IAuditResult, IAuditWhitelistResV1, ISkippedByRuleExceptionItem } from '@actiontech/shared/lib/api/sqle/service/common'; diff --git a/packages/sqle/src/page/RuleException/prefill.ts b/packages/sqle/src/page/RuleException/prefill.ts index de1e0ecdd..f5eb05a94 100644 --- a/packages/sqle/src/page/RuleException/prefill.ts +++ b/packages/sqle/src/page/RuleException/prefill.ts @@ -37,6 +37,10 @@ export type SqlManageRuleExceptionRecord = { instance_name?: string; audit_plan_db_type?: string; db_type?: string; + /** SQL管控行 schema_name;仅 preferSchemaObjectMatch 入口消费 */ + schema_name?: string; + /** SQL管控行 info.schema_meta_name(表/视图);仅 preferSchemaObjectMatch 入口消费 */ + schema_meta_name?: string; source?: ISqlManageRuleExceptionContext['source']; audit_result?: IAuditResult[] | null; }; @@ -45,6 +49,25 @@ type ISqlManageWithInstanceId = ISqlManage & { instance_id?: string; db_type?: string; audit_plan_db_type?: string; + /** AC-011 list透出;三种缺省(缺键/空串/省略 info)均视为无对象名 */ + info?: { schema_meta_name?: string } | null; +}; + +export const resolveSchemaMetaNameFromSqlManageInfo = ( + info?: { schema_meta_name?: string } | null +): string | undefined => { + if (!info || typeof info !== 'object') { + return undefined; + } + if (!Object.prototype.hasOwnProperty.call(info, 'schema_meta_name')) { + return undefined; + } + const raw = info.schema_meta_name; + if (typeof raw !== 'string') { + return undefined; + } + const trimmed = raw.trim(); + return trimmed || undefined; }; export const toSqlManageRuleExceptionRecord = ( @@ -61,6 +84,10 @@ export const toSqlManageRuleExceptionRecord = ( instance_name: record.instance_name, audit_plan_db_type: recordWithExtra.audit_plan_db_type, db_type: recordWithExtra.db_type, + schema_name: record.schema_name?.trim() || undefined, + schema_meta_name: resolveSchemaMetaNameFromSqlManageInfo( + recordWithExtra.info + ), source: record.source, audit_result: record.audit_result }; @@ -71,6 +98,8 @@ export type ScanTaskRuleExceptionRecordInput = { fingerprint?: string; sql?: string; instance_id?: string; + schema_name?: string; + schema_meta_name?: string; audit_result?: IAuditResult[] | null; }; @@ -111,6 +140,8 @@ export const toScanTaskRuleExceptionRecord = ( sql, instance_id: record.instance_id?.trim() || sourceContext?.instanceId, db_type: sourceContext?.instanceType, + schema_name: record.schema_name?.trim() || undefined, + schema_meta_name: record.schema_meta_name?.trim() || undefined, source: { sql_source_type: sourceContext?.auditPlanType, sql_source_ids: sourceContext?.auditPlanId @@ -126,6 +157,13 @@ export type BuildBlacklistPrefillFromSqlManageOptions = { ruleName?: string; /** Row action entry: specific scope without pre-selecting rules */ specificRuleScopeWithoutPreselect?: boolean; + /** + * SQL管控页「添加为审核SQL例外」专用(S1 §5.4)。 + * 库名+对象名均有值 → 预填 schema + object_name,跳过 fp_sql; + * 缺任一侧 → 指纹回退(AC-001b)。 + * 禁止传给扫描详情抽屉 / 快捷审核 / 工单 / SqlAnalyze(AC-001a / AC-012)。 + */ + preferSchemaObjectMatch?: boolean; }; const AUDIT_TASK_MATCH_PREFILL_SKIP_SOURCE_TYPES = new Set([ @@ -271,7 +309,22 @@ export const buildBlacklistPrefillFromSqlManage = ( const match_conditions: IMatchConditionReqV1[] = []; - if (context.sql_fingerprint) { + const objectName = record?.schema_meta_name?.trim(); + const schemaName = record?.schema_name?.trim(); + // AC-001:须库名与对象名均非空才走对象预填;缺一则 AC-001b 指纹回退 + const useSchemaObjectMatch = + !!options?.preferSchemaObjectMatch && !!schemaName && !!objectName; + + if (useSchemaObjectMatch) { + match_conditions.push({ + type: MatchConditionReqV1TypeEnum.schema, + content: schemaName + }); + match_conditions.push({ + type: MatchConditionReqV1TypeEnum.object_name, + content: objectName + }); + } else if (context.sql_fingerprint) { match_conditions.push({ type: MatchConditionReqV1TypeEnum.fp_sql, content: context.sql_fingerprint diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/RemediationDetailDrawer/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/RemediationDetailDrawer/index.tsx index a54806f58..80adfc7ab 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/RemediationDetailDrawer/index.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/RemediationDetailDrawer/index.tsx @@ -32,9 +32,13 @@ const RemediationDetailDrawerModal = () => { const handleOpenCreateException = useCallback( (params: OpenCreateAuditWhitelistExceptionParams) => { + // SQL管控抽屉加号:与列表同入口族,挂对象预填(S1 §5.4) openAuditWhitelistCreateWithPrefill( toSqlManageRuleExceptionRecord(selectedData ?? undefined), - { ruleName: params.auditResult?.rule_name } + { + ruleName: params.auditResult?.rule_name, + preferSchemaObjectMatch: true + } ); }, [openAuditWhitelistCreateWithPrefill, selectedData] diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/StatusDrawer/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/StatusDrawer/index.tsx index 6a53ec5dd..ff55f0123 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/StatusDrawer/index.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modal/StatusDrawer/index.tsx @@ -104,9 +104,13 @@ const StatusDrawer = () => { const handleOpenCreateException = useCallback( (params: OpenCreateAuditWhitelistExceptionParams) => { + // SQL管控抽屉加号:与列表同入口族,挂对象预填(S1 §5.4) openAuditWhitelistCreateWithPrefill( toSqlManageRuleExceptionRecord(selectedData ?? undefined), - { ruleName: params.auditResult?.rule_name } + { + ruleName: params.auditResult?.rule_name, + preferSchemaObjectMatch: true + } ); }, [openAuditWhitelistCreateWithPrefill, selectedData] diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx index 0f5b98910..a320ab682 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx @@ -320,9 +320,13 @@ const SQLEEIndex = () => { const onCreateWhitelist = useCallback( (record?: ISqlManage) => { + // SQL管控主路径:preferSchemaObjectMatch 仅本入口族(S1 §5.4) openAuditWhitelistCreateWithPrefill( toSqlManageRuleExceptionRecord(record), - { specificRuleScopeWithoutPreselect: true } + { + specificRuleScopeWithoutPreselect: true, + preferSchemaObjectMatch: true + } ); }, [openAuditWhitelistCreateWithPrefill] diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.tsx index ef056a99c..171cc50af 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.tsx +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.tsx @@ -183,6 +183,14 @@ const ScanTypeSqlCollection: React.FC = ({ fingerprint: record['fingerprint'], sql: record['sql'], instance_id: record['instance_id'], + schema_name: + typeof record['schema_name'] === 'string' + ? record['schema_name'] + : undefined, + schema_meta_name: + typeof record['schema_meta_name'] === 'string' + ? record['schema_meta_name'] + : undefined, audit_result: active }, scanTaskSourceContext @@ -212,9 +220,12 @@ const ScanTypeSqlCollection: React.FC = ({ (params: OpenCreateAuditWhitelistExceptionParams) => { closeRemediationDrawer(); setRemediationDrawerRecord(undefined); + // AC-001a:扫描详情撤回库表对象预填,恢复改前指纹路径(勿传 preferSchemaObjectMatch) openAuditWhitelistCreateWithPrefill( toScanTaskRecord(remediationDrawerRecord), - { ruleName: params.auditResult?.rule_name } + { + ruleName: params.auditResult?.rule_name + } ); }, [ @@ -228,9 +239,12 @@ const ScanTypeSqlCollection: React.FC = ({ const handleOpenCreateExceptionFromReport = useCallback( (params: OpenCreateAuditWhitelistExceptionParams) => { closeReportDrawer(); + // AC-001a:扫描详情撤回库表对象预填,恢复改前指纹路径(勿传 preferSchemaObjectMatch) openAuditWhitelistCreateWithPrefill( toScanTaskRecord(currentAuditResultRecord), - { ruleName: params.auditResult?.rule_name } + { + ruleName: params.auditResult?.rule_name + } ); }, [