SQL审核 | SQLE 如何开发一条自定义的规则

本文介绍了如何在SQL审核工具SQLE中开发自定义的SQL审核规则,通过登录、调用API接口进行SQL审计,详细阐述了新建规则的步骤,并展示了代码实现和单元测试。文章适合对数据库管理和Go语言感兴趣的读者。

作者:Jason

就职于捷信消费金融有限公司,担任 DBA 工作。先后从事过 Oracle 、Mongo 、MySQL 的 DBA ,以及大数据 ETL 的开发工作。对 NEWSQL 以及云原生分布式数据库具有浓厚的兴趣爱好。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


SQLE 是由上海爱可生信息技术股份有限公司 开发并开源,支持SQL审核、索引优化、事前审核、事后审核、支持标准化上线流程、原生支持 MySQL 审核且数据库类型可扩展的 SQL 审核工具。

官方主页 https://opensource.actionsky.com/sqle/

官方文档 Introduction · SQLE manual (actiontech.github.io)

(!!!事先声明:二次开发纯属于个人技术研究,不得进行任何商业盈利行为)

大家好! 本次来分享的是如何开发一条自定义的review rule 的规则(基于MySQL 5.7的语法)。

在开发代码前,我们先从后台API调用的角度梳理一下审核SQL的具体流程:

我们可以先用 POSTMAN 从调用一遍代码审核这个方法:

首先,先确定规则验证的API接口:
http://10.25.15.83:10000/v1/tasks/audits

与API相对应的源代码方法的入口是:
api.controller.v1.task.go --> func CreateAndAuditTask(c echo.Context)

由于系统做了JWT的登录验证, 我们需要先模拟一下登录:

1)调用登录的API 10.25.15.83:10000/v1/login,获得token: ****************************

默认的是 admin/admin ,登录成功后,我们可拿到 token

2)我们调用一下规则验证的API接口: http://10.25.15.83:10000/v1/tasks/audits

我们准备了一下简单的SQL语句:

create table test (id int not null, name varchar(20));

这个语句不符合审计的规则如下:
1)无主键
2)无列和表级别的comment
3)无innodb engine 指定

我们首先设置参数: 可以参考request的结构体。

type CreateAuditTaskReqV1 struct {
   InstanceName   string `json:"instance_name" form:"instance_name" example:"inst_1" valid:"required"`
   InstanceSchema string `json:"instance_schema" form:"instance_schema" example:"db1"`
   Sql            string `json:"sql" form:"sql" example:"alter table tb1 drop columns c1"`
}

参数1: InstanceName -》 DBA
参数2: instance_schema =》 testdb
参数3: sql =》 create table test (id int not null, name varchar(20));

我们还需要设置一下 token: 这个token 从登录的API返回值可以获取到

下面我们尝试调用一下审计接口:http://10.25.15.83:10000/v1/tasks/audits

我们来看一下返回给我们的response的含义 :

{
   "code": 0,
   "message": "ok", --表示api调用成功
   "data": {
       "task_id": 1,  --返回的审核结果保存在 task_is =1的任务中
       "instance_name": "DBA", --实例名
       "instance_schema": "testdb", --DB名
       "audit_level": "error", --ERROR 级别的错误
       "pass_rate": 0,  --通过率为0
       "status": "audited", --已经审计的状态
       "sql_source": "form_data" --直接获得SQL语句的方式
   }
}

我们要想知道具体的审计返回的结果需要查询数据库: (“task_id”: 1, --返回的审核结果保存在 task_is =1的任务中)

mysql> select  audit_result from execute_sql_detail where task_id=1\G
*************************** 1. row ***************************
audit_result: [notice]列建议添加注释
[error]表必须有主键
[notice]必须使用Innodb数据库引擎
[notice]表建议添加注释
1 row in set (0.00 sec)

从 response 的信息来看 是完全符合我们之前的预期的结果。

下面我们要代码的角度追踪一下这个审计的整个流程:
SQLE 采用的是类似数据库driver插件的开发模式,接口 driver.go 已经定义好了抽象的函数,具体的实现需要每一种具体的数据库来完成
例如 mysql.go, oracle.go

下面是如何添加一条新的rule 到审核平台的具体步骤:

New Rule的规则是:
我们想要在新建的TABLE 表中必须含有指定的列, 像是cdate cdate,creator,edate,editor 这几个审计字段。

首先找到对应的数据库里面rule的表是 rule:

mysql> select * from rules \G
*************************** 28. row ***************************
  name: ddl_check_object_name_using_keyword
db_type: mysql
  desc: 数据库对象命名禁止使用保留字
 level: error
  type: 命名规范
params: NULL
*************************** 29. row ***************************
  name: ddl_check_pk_name
db_type: mysql
  desc: 建议主键命名为"PK_表名"
 level: notice
  type: 命名规范
params: NULL

....

我们来手动插入一条新rule的记录:

mysql> INSERT INTO sqle.rules
   -> (name, db_type, `desc`, `level`, `type`, params)
   -> VALUES('ddl_check_audit_column', 'mysql', '建表语句需要包含4个审计列(cdate,creator,edate,editor)', 'notice', '命名规范', NULL);
Query OK, 1 row affected (0.00 sec)


mysql> commit;
Query OK, 0 rows affected (0.00 sec)

我们可以从页面看到这条新加的规则已经出现在了列表里面:

我们把这条新的规则追加到我们自定义的规则模板中:

最后一步,提交规则的模板信息。

下面我们从页面上测试一下我们新加的规则:(当然了,对应的规则要添加对应的后台代码,这个后面会有介绍~)

我们新建一个审核任务:

输入建表语句:

create table test (id int not null, name varchar(20));

点击审核按钮

我们可以看到我们自定义的审核规则已经生效:

最后我们来看看后台代码是如何开发实现这个规则的:

后台对应的这条规则的逻辑代码是:(入门级别的简单代码)这段代码加入到 sqle/driver/mysql/rule/rule.go中
用Map的结构体来判断,必要的列的名字是否存在review 语句的表中

//可以在linux下进行命令行的代码单元测试
// dlv test github.com/actiontech/sqle/sqle/driver/mysql -- -test.run ^TestCheckAuditColumn$
func checkAuditColumn(ctx *session.Context, rule driver.Rule, res *driver.AuditResult, node ast.Node) error {
   var auditCols = [4]string{"cdate","edate","creator","editor"}
   var set map[string]struct{}
   set = make(map[string]struct{})
   for _, value := range auditCols{
      set[value] = struct{}{}
   }
    var cnt int = 0
   switch stmt := node.(type) {
   case *ast.CreateTableStmt:
      for _,value := range stmt.Cols {
         fmt.Println(value.Name.Name)
         if _, ok := set[value.Name.Name.String()];ok {
            cnt++
         } 

      }
   if cnt != 4{
      addResult(res, rule, rule.Name)
   }  


   }


   return nil
}

同时我们添加rule的mapping 规则: sqle/driver/mysql/rule/rule.go中

{
   Rule: driver.Rule{
      Name:     DDLCheckAuditColumn,
      Desc:     "建表语句必须包含审计列(cdate,creator,edate,editor)",
      Level:    driver.RuleLevelError,
      Category: RuleTypeUsageSuggestion,
   },
   Message:      "建表语句必须包含审计列(cdate,creator,edate,editor)",
   AllowOffline: true,
   Func:         checkAuditColumn,
},

添加常量定义: sqle/driver/mysql/rule/rule.go中

DDLCheckAuditColumn                         = "ddl_check_audit_column"

我们还可以在 sqle/driver/mysql/audit_offline_test.go 进行unit testing:

func TestCheckAuditColumn(t *testing.T) {
   fmt.Println("start..............")
   runSingleRuleInspectCase(rulepkg.RuleHandlerMap[rulepkg.DDLCheckAuditColumn].Rule, t,
      "create table test (id int not null, name varchar(20));  ",
      DefaultMysqlInspectOffline(),
      `create table test (id int not null,
                                name varchar(20),
                                cdate datetime,
                                edate datetime,
                                creator1 varchar(20),
                                editor1 varchar(20));`,
      newTestResult().addResult(rulepkg.DDLCheckAuditColumn),
   )
}

最后测试通过后,我们即可提交代码,发布新的程序来从页面上验证我们自己定义的规则了。

总体来说,爱可生的代码是十分规范的,便于二次代码开发和维护。如果你是golang的开发爱好者,亦可作为学习项目的经典案例。

>nih ドキュメント名機能名(L2)版日付作成者 機能設計書受発注マスタ作成1.012025/8/18NEC 丁雨珊開発方式No.1開発方式No.2開発方式No.3開発方式No.4開発方式No.8開発方式No.9開発方式No.10開発方式No.13開発方式No.14(PostgreSQL非互換変換ルール 改版履歴:1.02) 処理仕様クラスIDクラス名 BJ_M_VIVID_LINKReadDaoImplリンクマスタ(全件)ReadDAOクラス ジョブステップIDBJ_M_VIVID_LINKReadDaoImpl.java システム名受発注システム クラス名ViViD_リンクマスタ DAO のクラス 機能概要ViViD_リンクマスタに対するデータベースアクセス機能を提供する. 備考著作権 Copyright © NEC Corporation 2025. All rights reserved. /* V1.01削除 * システム名 :受発注システムV1.01削除 * クラス名:ViViD_リンクマスタ DAO のクラスV1.01削除 * 機能概要:ViViD_リンクマスタに対するデータベースアクセス機能を提供する.V1.01削除 * V1.01削除 * Copyright(c) 2009 FamilyMart Co., Ltd. All Rights Reserved. V1.01削除 * V1.01削除 * 履歴:V1.01削除 * 日付 更新者 内容V1.01削除 * 2009/03/31 徐 利民 初版V1.01削除 * V1.01削除 */V1.01削除 package jp.co.family.bj.dao.impl; import java.sql.ResultSet; import java.sql.SQLException; import jp.co.family.bj.common.core.AbstractDao; import jp.co.family.bj.common.core.AbstractDto; import jp.co.family.bj.common.exception.DatabaseException; import jp.co.family.bj.common.util.Const; import jp.co.family.bj.common.util.MessageUtil; import jp.co.family.bj.dao.BJ_M_VIVID_LINKReadDao; import jp.co.family.bj.dao.VIVID_MST_ReadDao; import jp.co.family.bj.dto.BJ_M_LINKDto; /** * ViViD_リンクマスタに対するデータベースアクセス機能を提供する. * */ public class BJ_M_VIVID_LINKReadDaoImpl extends AbstractDao implements BJ_M_VIVID_LINKReadDao, VIVID_MST_ReadDao { // 発注地区 private static final String HCHIKU = "HCHIKU"; // 店番 private static final String TENBAN = "TENBAN"; // 取引先コード private static final String TRI_CD = "TRI_CD"; // 使用開始日 private static final String START_DATE = "START_DATE"; // 使用終了日 private static final String END_DATE = "END_DATE"; // 登録区分 private static final String UP_KBN = "UP_KBN"; // 店舗配信済みフラグ private static final String H_FLG = "H_FLG"; /** データ検索SQL文 */ private String checkLinkSql; // リンクマスタ作成用データ検索SQL文 private String selectSql; // 検索結果セット private ResultSet resultSet; //リンクDTO private BJ_M_LINKDto bj_M_LINKDto = new BJ_M_LINKDto(); /** データ検索SQL文 */ private String checkLinkDateSql; /** * 検索SQL文を設定する. * * @param checkLinkSql * SQL文 */ public void setCheckLinkSql(String checkLinkSql) { this.checkLinkSql = checkLinkSql; } /** * 検索SQL文を設定する. * * @param checkLinkDateSql * SQL文 */ public void setCheckLinkDateSql(String checkLinkDateSql) { this.checkLinkDateSql = checkLinkDateSql; } /** * @see BJ_M_VIVID_LINKReadDao#checkLink(String, String, String) */ public boolean checkLink(String hchiku, String tenban, String triCd) { int count = 0; Object[] args = { hchiku, tenban, triCd }; ResultSet rs = null; try { // 結果セットのオープン rs = executeQuery(checkLinkSql, args); rs.next(); count = rs.getInt(1); } catch (SQLException se) { Object[] args2 = {}; throw new DatabaseException(MessageUtil.getMessage(Const.E_BJ_0120, args2), se); } finally { // 結果セットをオープンしたら必ずクローズする if (rs != null) { try { rs.close(); } catch (SQLException se) { // 外のtry-catchのExceptionを生かすためにここでは敢えて握りつぶす } } } if (count == 0) { return false; } else { return true; } } /** * @see BJ_M_VIVID_LINKReadDao#checkLinkDate(String, String, String, String) */ public boolean checkLinkDate(String hchiku, String tenban, String triCd, String orderDate) { int count = 0; Object[] args = { hchiku, tenban, triCd, orderDate, orderDate }; ResultSet rs = null; try { // 結果セットのオープン rs = executeQuery(checkLinkDateSql, args); rs.next(); count = rs.getInt(1); } catch (SQLException se) { Object[] args2 = {}; throw new DatabaseException(MessageUtil.getMessage(Const.E_BJ_0120, args2), se); } finally { // 結果セットをオープンしたら必ずクローズする if (rs != null) { try { rs.close(); } catch (SQLException se) { // 外のtry-catchのExceptionを生かすためにここでは敢えて握りつぶす } } } if (count == 0) { return false; } else { return true; } } /** * データ検索SQL文を設定 * * @param selectSql * SQL文 */ public void setSelectSql(String selectSql) { this.selectSql = selectSql; } /** * * リンクマスタ作成用結果セットの生成を行う * * @param gyomuDate 業務日付 */ public void openMstData(String gyomuDate) { Object[] args = {gyomuDate,gyomuDate,gyomuDate,gyomuDate}; resultSet = executeQuery(selectSql, args); } /** * * リンクマスタ作成用結果セットの解放を行う * */ public void closeMstData() { try { if(resultSet!=null){ resultSet.close(); } } catch (SQLException sqle) { Object[] args = {}; //メッセージ:データベースアクセスで例外が発生しました。 throw new DatabaseException(MessageUtil.getMessage(Const.E_BJ_0120, args),sqle); } } /** * 結果セットからデータの取得し、リンクマスタDTOにセットする。 * * @return dto * AbstractDto */ public AbstractDto fetchMstData() { try{ if(resultSet.next()){ return mapRow(resultSet,bj_M_LINKDto); }else{ return null; } }catch (SQLException sqle){ Object[] args = {selectSql}; //メッセージ:レコードのフェッチに失敗しました。(SQL:selectShoSql) throw new DatabaseException(MessageUtil.getMessage(Const.E_BJ_0119, args),sqle); } } /** * データマッピング処理 * 結果セットのデータをリンクマスタDTOにセットする * * @param resultset ResultSet * @param dto BJ_M_LINKDto * @return dto リンクマスタDTO */ private BJ_M_LINKDto mapRow(ResultSet resultset, BJ_M_LINKDto dto){ //DTOをクリアー dto.clear(); try { // 発注地区を設定 dto.setHchiku(resultset.getString(HCHIKU)); // 店番を設定 dto.setTenban(resultset.getString(TENBAN)); // 取引先コードを設定 dto.setTriCd(resultset.getString(TRI_CD)); // 使用開始日を設定 dto.setStartDate(resultset.getString(START_DATE)); // 使用終了日を設定 dto.setEndDate(resultset.getString(END_DATE)); // 登録区分を設定 dto.setUpKbn(resultset.getString(UP_KBN)); // 店舗配信済みフラグを設定 dto.setHFlg(resultset.getString(H_FLG)); } catch (SQLException sqle) { Object[] args = {}; //メッセージ:データベースアクセスで例外が発生しました。 throw new DatabaseException(MessageUtil.getMessage(Const.E_BJ_0120, args),sqle); } return dto; } } 代码解释
最新发布
09-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值