用腾讯云AI代码助手开发一款数据库敏感信息检查工具

引言

作为一个优秀的 IT 技术民工,需要始终学习先进的技术并将技术转换为生产力,目前 AI 领域编码辅助工具层出不穷,开发者有必要或者说必须要掌握相关工具的使用,以提高编码效率,降低编码错误。这次我通过一个简单的项目和大家分享一下 AI 代码助手的使用。

AI 开发实践

开发环境介绍

首先我们从安装插件开始,打开 IntelliJ IDEA ,通过 Settings->Plugins->搜索 AI 代码助手,点击 Install

然后,右下角点击 icon 微信扫码登录

开发背景简要说明

开发数据库扫描工具的原因是因为我需要对公司内部所有运行在腾讯云上面的数据库进行检查。

信息安全:

要求不允许数据库中存放任何敏感信息,包括身份证号,手机号,银行卡号,邮件地址等信息

面临挑战:

由于数据库众多,数据量巨大,如果进行人工核验非常耗时耗力,最重要的是难免会有所遗漏

解决方案:

基于信息安全和挑战,决定采用 AI 来开发一个简单的工具来实现此功能,提升自己工作效率,数据库铭感信息检查工具主要包括以下功能:

面向多个数据库连接,工具可以一次性接入并完成所有数据库的扫描,以提升检查效率

可以自动获取一个数据库下的多个 DB,并进行数据扫描

能匹配数据格式是否是敏感信息

将识别到的敏感信息输出日志,以便后续反馈给相关人员进行处理

编码实现

首先定义多个数据库的连接方式

以下为 yaml demo 示例文件格式,这种定义方式可以将多个数据库信息写在一起,一次扫描所有数据库服务器

databases:  -host:192.168.1.2port:3306username: rootpassword:MysqlPasswd-host:192.168.1.3port:3306username: rootpassword:MysqlPasswd

然后,进行定义 class 类用于绑定 yaml,将 yaml 文件的数据库信息映射成 DatabaseInfo 对象,方便我后续的操作

package org.checkdb;publicclassDatabaseInfo{StringdatabaseHost;StringdatabasePort;StringdatabaseUser;StringdatabasePassword;// Getter and Setter methodspublicStringgetHost() {returndatabaseHost;    }    publicvoidsetHost(StringdatabaseHost) {this.databaseHost= databaseHost;    }    publicStringgetPort() {returndatabasePort;    }    publicvoidsetPort(StringdatabasePort) {this.databasePort= databasePort;    }    publicStringgetUsername() {returndatabaseUser;    }    publicvoidsetUsername(StringdatabaseUser) {this.databaseUser= databaseUser;    }    publicStringgetPassword() {returndatabasePassword;    }    publicvoidsetPassword(StringdatabasePassword) {this.databasePassword= databasePassword;    }}

在 mian 中绑定 yaml 和类,在工具启动时映射 dataeaseinfo 对象中,这部分代码我们使用 AI 代码助手帮我们生成一个简单的例子,我们简单修改完成。我们可以通过问答的方式获取相关例子

通过 AI 代码助手 AI 技术问答给出的例子,基本简单修改即可完成使用,但是注意别忘了添加包依赖

packageorg.checkdb.utils;importorg.checkdb.DatabaseInfo;importorg.yaml.snakeyaml.Yaml;importjava.io.InputStream;importjava.util.ArrayList;importjava.util.List;importjava.util.Map;publicclassDatabaseMapper{    privatestaticfinalYamlyaml =newYaml();    publicstaticListmapDatabasesFromYaml(StringyamlFilePath) {InputStreaminputStream =DatabaseMapper.class.getClassLoader().getResourceAsStream(yamlFilePath);Map>> databasesMap = yaml.load(inputStream);List databaseInfos =newArrayList<>();for(Map databaseMap : databasesMap.get("databases")) {DatabaseInfodatabaseInfo =newDatabaseInfo();            databaseInfo.setHost(databaseMap.get("host"));            databaseInfo.setPort(String.valueOf(databaseMap.get("port")));            databaseInfo.setUsername(databaseMap.get("username"));            databaseInfo.setPassword(databaseMap.get("password"));            databaseInfos.add(databaseInfo);        }returndatabaseInfos;    }}

在 main 方法中增加调用,这样我们就在 mian 方法中获取到了所有的数据库信息 List

publicstaticvoidmain(String[] args) {List databaseInfos =DatabaseMapper.mapDatabasesFromYaml("databases.yaml");for(DatabaseInfodatabaseInfo : databaseInfos) {List databases =DatabaseConnector.getAllDatabases(databaseInfo);        }    }

编写连接数据库的方法

获取单个 mysql 中的所有 database,然后我们把这个方法放到 main 中的,对 databaseInfos 进行循环,依次获取所有 mysql 数据库中的所有 database。

现在继续使用 AI 代码助手帮助我们提供示例

publicstaticListgetAllDatabases(DatabaseInfo databaseInfo) {List databases =newArrayList<>();try{Connectionconn =DriverManager.getConnection("jdbc:mysql://"+ databaseInfo.getHost() +":"+ databaseInfo.getPort()+"?useSSL=false&serverTimezone=UTC", databaseInfo.getUsername(), databaseInfo.getPassword());Statementstmt = conn.createStatement();ResultSetrs = stmt.executeQuery("SHOW DATABASES");while(rs.next()) {                databases.add(rs.getString(1));            }            conn.close();        }catch(SQLExceptione) {            e.printStackTrace();        }returndatabases;    }

在 main 方法中增加调用,现在我们可以依次获取所有 mysql 数据库中的所有 database

publicstaticvoidmain(String[] args) {List databaseInfos =DatabaseMapper.mapDatabasesFromYaml("databases.yaml");for(DatabaseInfodatabaseInfo : databaseInfos) {List databases =DatabaseConnector.getAllDatabases(databaseInfo);        }

获取单个 database 中的所有表,这次我们换一种新的方式使用 AI 代码助手,通过编写代码注释,让 AI 代码助手帮我们生成代码

publicstaticListgetAllTables(DatabaseInfo databaseInfo,StringdatabaseName) {List tables =newArrayList<>();try{Connectionconn =DriverManager.getConnection("jdbc:mysql://"+ databaseInfo.getHost() +":"+ databaseInfo.getPort()+"?useSSL=false&serverTimezone=UTC", databaseInfo.getUsername(), databaseInfo.getPassword());Statementstmt = conn.createStatement();ResultSetrs = stmt.executeQuery("SHOW TABLES FROM "+ databaseName);while(rs.next()) {                tables.add(rs.getString(1));            }            conn.close();        }catch(SQLExceptione) {            e.printStackTrace();        }returntables;    }

在 main 方法中增加调用,现在我们可以依次获取所有 mysql 数据库中的所有 database 的所有 table

publicstaticvoidmain(String[] args) {List databaseInfos =DatabaseMapper.mapDatabasesFromYaml("databases.yaml");for(DatabaseInfodatabaseInfo : databaseInfos) {List databases =DatabaseConnector.getAllDatabases(databaseInfo);for(Stringdatabase : databases) {List tables =DatabaseConnector.getAllTables(databaseInfo,database);            }        }    }

数据格式校验方法

现在已经有了所有 database以及 table,我继续编写效验身份证格式,电话号格式和银行卡格式的方法,也继续使用通过注释的方式生成代码

以下为 AI 生成的代码,达到了 100% 可用

package org.checkdb.utils;publicclassCheckRE{// 判断数据格式是否是身份证号publicstaticbooleanisIdCard(StringidCard) {returnidCard.matches("^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");    }// 判断数据格式是否是手机号publicstaticbooleanisMobileNO(Stringmobiles) {returnmobiles.matches("^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$");    }// 判断数据格式是否是银行卡号publicstaticbooleanisBankCard(StringbankCard) {returnbankCard.matches("^[1-9]\\d{9,29}$");    }}

注:这些判断方法肯定不是非常严格的验证,但是用于判断数据库中是否存在这些敏感类型的数据是足够了

扫描逻辑

最后,通过编写工具扫描逻辑,由于数据表中的数据非常多,全表扫描存在性能问题,因此并不可取,本次,我们以每次获取每张表的前 500 条数据进行判断,如果存在敏感数据,我们记录到文件中,依然 AI 代码助手生成代码

packageorg.checkdb.utils;importorg.checkdb.DatabaseInfo;importjava.io.BufferedWriter;importjava.io.FileWriter;importjava.io.IOException;importjava.sql.*;importjava.util.ArrayList;importjava.util.Objects;importstaticorg.checkdb.utils.CheckRE.*;publicclassScanDatabase{    publicstaticvoidScanTable(DatabaseInfodatabaseInfo,StringdatabaseName,StringtableName){if(Objects.equals(databaseName,"mysql") ||Objects.equals(databaseName,"sys") ||Objects.equals(databaseName,"performance_schema") ||Objects.equals(databaseName,"information_schema")) {return;        }Connectionconn =null;Statementstmt =null;ResultSetrs =null;try{// 连接数据库conn =DriverManager.getConnection("jdbc:mysql://"+ databaseInfo.getHost() +":"+ databaseInfo.getPort()+"/"+databaseName+"?useSSL=false&serverTimezone=UTC", databaseInfo.getUsername(), databaseInfo.getPassword());// 获取表的所有字段信息DatabaseMetaDatametaData = conn.getMetaData();ResultSetfieldsRS = metaData.getColumns(null,null, tableName,null);// 存储字段名ArrayList fields =newArrayList<>();while(fieldsRS.next()) {                fields.add(fieldsRS.getString("COLUMN_NAME"));            }// 执行SQL查询,获取前500条数据Stringsql ="SELECT * FROM "+ tableName +" LIMIT 500";            stmt = conn.createStatement();            rs = stmt.executeQuery(sql);// 判断每个字段是否包含敏感信息并写入文件BufferedWriterwriter =newBufferedWriter(newFileWriter("sensitive_data.txt",true));while(rs.next()) {for(Stringfield : fields) {Stringvalue = rs.getString(field);if(value ==null) {continue;                    }if(isIdCard(value)) {                        writer.write("数据库IP: "+ databaseInfo.getHost() +"数据库名称: "+ databaseName +"表名称:"+ tableName +"字段名: "+ field +"存在敏感数据: "+ value +"\n");break;                    }if(isMobileNO(value)) {                        writer.write("数据库IP: "+ databaseInfo.getHost() +"数据库名称: "+ databaseName +"表名称:"+ tableName +"字段名: "+ field +"存在敏感数据: "+ value +"\n");break;                    }if(isBankCard(value)) {                        writer.write("数据库IP: "+ databaseInfo.getHost() +"数据库名称: "+ databaseName +"表名称:"+ tableName +"字段名: "+ field +"存在敏感数据: "+ value +"\n");break;                    }                }            }            writer.close();        }catch(SQLException|IOExceptione) {            e.printStackTrace();        }finally{// 关闭资源try{if(rs !=null) rs.close();if(stmt !=null) stmt.close();if(conn !=null) conn.close();            }catch(SQLExceptione) {                e.printStackTrace();            }        }    }}

在 main 方法中增加调用

publicclassMain{    publicstaticvoidmain(String[] args) {List databaseInfos =DatabaseMapper.mapDatabasesFromYaml("databases.yaml");for(DatabaseInfodatabaseInfo : databaseInfos) {List databases =DatabaseConnector.getAllDatabases(databaseInfo);for(Stringdatabase : databases) {List tables =DatabaseConnector.getAllTables(databaseInfo,database);for(Stringtable : tables){ScanDatabase.ScanTable(databaseInfo,database,table);                }            }        }    }}

注:1、要排除 Mysql 自带库;2、要考虑查询值空指针

 Code Review

现在我们代码逻辑和主要功能都已经实现,但是还是需要进行 code review,以便检查出代码中存在的潜在问题,还好 AI 代码助手提供了代码优化功能,我们使用一下看看效果,此处主要的两个功能是,代码优化和缺陷检查

通过代码优化,对 scanDatabase 进行优化

该功能给出了一下优化建议,以及示例代码,我们可以参考和建议进行修改

使用 try-with-resources 语句自动关闭资源,避免潜在的资源泄露。

将敏感信息的检测逻辑封装成方法,提高代码的可读性和可维护性。

使用预编译语句(PreparedStatement)来执行 SQL 查询,提高代码的安全性和性能。

避免在循环中频繁地打开和关闭文件,可以考虑使用 try-with-resources 语句来管理文件资源。

通过缺陷检查,我们对 scanDatabase 进行缺陷检查

该功能给出的建议如下

SQL 注入风险:原代码中直接拼接 SQL 查询语句,存在 SQL 注入的风险。为了避免这个问题,应该使用 PreparedStatement 来代替 Statement。

资源关闭异常:在 finally 块中关闭资源时,如果任何一个资源关闭失败,后续的资源关闭操作将不会被执行。应该分别捕获每个资源的关闭异常。

文件写入异常处理:在写入文件时,如果发生异常,可能会导致文件没有被正确关闭。应该使用 try-with-resources 语句来确保文件在异常发生时也能被正确关闭。

数据库连接字符串硬编码:数据库连接字符串中的 useSSL = false&serverTimezone = UTC 可能不适用于所有情况,应该允许通过参数传递。

文件写入路径硬编码:写入文件的路径被硬编码为 sensitive_data.txt,这可能导致文件被覆盖或写入到不期望的位置。应该允许通过参数传递文件路径。

Code Review小结

通过使用代码优化、代码缺陷检查,可以帮助我快速发现解决问题,也是对日常编码过程的一些宝贵建议,让我的思路可以更开阔,但 AI 给出的建议并非是一定要采纳的,需要根据情况具体分析,比如存在 sql 注入风险,所有 sql 均在代码内部,并不接收任何参数作为 sql 的一部分,则可暂不考虑优化。

工具效果

通过创建了两个 MySQL 数据库,创造一些促五示例数据,并进行扫描检测,可看到工具已查询出存在敏感字段的内容

注:为了验证和企业内部数据的安全性,这里通过在一台服务器上部署创建两个测试的数据库服务器来验证。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容