不小心把包含敏感信息的代码提交到线上了怎么办...
为了避免这种情况再次发生,靠记忆来约束自己是靠不住的,所以实现一个快速扫描敏感代码的钩子,然后通过工具来约束自己就显得十分有必要。
下面是快速实现流程:
commitlint+husky规范git提交;- 触发git hooks钩子
pre-commit; - child_process进程获取commit信息;
- 获取提交文件内容;
- 代码过滤;
- 扫描出敏感信息终止commit。
一、commitlint+husky
这个网上太多资料,简单讲下流程。按顺序执行以下流程
1. 执行命令
npm install --save-dev husky @commitlint/cli @commitlint/config-conventional
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
2. 根目录新建commitlint.config.js
/*
规范commit日志
https://commitlint.js.org
*/
const types = [
'build', // 主要目的是修改项目构建系统(例如glup,webpack,rollup的配置等)的提交
'ci', // 修改项目的持续集成流程(Kenkins、Travis等)的提交
'chore', // 构建过程或辅助工具的变化
'docs', // 文档提交(documents)
'feat', // 新增功能(feature)
'fix', // 修复 bug
'pref', // 性能、体验相关的提交
'refactor', // 代码重构
'revert', // 回滚某个更早的提交
'style', // 不影响程序逻辑的代码修改、主要是样式方面的优化、修改
'test', // 测试相关的开发,
],
typeEnum = {
rules: {
'type-enum': [2, 'always', types],
},
value: () => {
return types;
},
};
module.exports = {
extends: ['@commitlint/config-conventional'],
/*
Level [0..2]: 0 disables the rule. For 1 it will be considered a warning for 2 an error.
https://commitlint.js.org/#/reference-rules
*/
rules: {
'type-enum': typeEnum.rules['type-enum'],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never'],
},
};
3. 验证
git add .
git commit -m "提交测试" # 错误
git commit -m "feat: 提交测试" # 正确
二、pre-commit钩子
上面已经实现了commit-msg的检测及约束,接下来是本次重点用到的钩子pre-commit来实现代码扫描功能。
1. 新增pre-commit的husky
创建对应husky文件。
npx husky add .husky/pre-commit 'npm run pre-commit'
2. package.json
上面的命令除了新增pre-commit的husky文件,还添加了内容,触发pre-commit的时候执行npm run pre-commit,所以要在scripts中新增以下代码
"pre-commit": "node pre-commit.js",
3. pre-commit.js
然后接下来就是创建pre-commit.js,并实现业务逻辑。
touch pre-commit.js
随便在pre-commit.js新增点内容如console.log('pre-commit');
执行git add . && git commit -m "asd"可以看到新增内容的输出及不符合提交规则的警告信息,表示想要实现的需求的基础条件都已具备了。
接下来实现commit相关文件内容的提取。
三、获取commit信息
获取git进程信息,可以直接用node的子进程器child_process来实现。
commit基本信息
新增以下代码
const childProcess = require('child_process');
// 提交记录
const commitStages = childProcess.execSync('git diff --name-status HEAD~3', {
encoding: 'utf8',
});
console.log('提交记录:', commitStages);
然后执行node pre-commit.js
可以看到控制台输出这次提交的相关文件列表。
% node pre-commit.js
M .gitignore
D pre-commit.js
D src/test copy.js
D src/test.js
获取到提交的文件列表,接下来的就好办了。
四、获取文件内容
根据控制台输出,可以看到被追踪的文件前都有一个大写字母+一段空格。
简单解释下大写字母的含义:M:修改,D:删除 A:新增
接下来处理文件问题
1. 获取符合文件类型的文件列表
因为git commit的时候还会有其他信息,所以要根据符合提交的文件类型过滤。
/**
* @description 获取类型清单
* @param {*} arr
* @param {*} type M:修改,D:删除 A:新增
* @returns
*/
const getArrList = (str, type) => {
const arr = str.split('\n');
return arr.filter(item => {
const regex = new RegExp(`[${type}].*`);
if (regex.test(item)) {
return item !== undefined;
}
});
};
2. 移除文件类型前缀及空格
// 格式化--移除 M [fileName].[fileType]之间的空格
const formatList = (arr, type) => {
return arr.map(item => {
return item.replace(/\s/, '').replace(type, '');
});
};
3. 获取过滤的文件列表
根据上面两个函数,可以过滤出修改和新增的文件,删除的不需要扫描,因此不过滤。。
// M:修改,D:删除 A:新增
const typeList = ['M', 'A'];
let fileList = [];
typeList.forEach(type => {
let arr = getArrList(commitStages, type);
arr = formatList(arr, type);
// 提交文件list
fileList = fileList.concat(arr);
});
接着执行node pre-commit.js
可以看到控制台输出提交的文件list及其中非删除文件的list。
% node pre-commit.js
M .gitignore
D pre-commit.js
D src/test copy.js
D src/test.js
非删除文件 .gitignore
拿到最终需要过滤的文件list,接下来往下走。
五、代码过滤
1. 代码扫描
-
fs模块解析文件
先使用
fs.readFileSync解析,获取文件代码内容const codes = fs.readFileSync(fileName, { encoding: 'utf-8' }); -
然后定义敏感词汇
const censetiveWords = ['hahahha'] -
接着扫描代码
/** * 扫描代码:是否有关键字 */ let flag = false; // 是否有关键字 for (const code of words.filter(str => str)) { flag = codes.includes(code); if (flag) { console.log(`${fileName} 检测到敏感代码 ${code}!!!`); break; } }
2. 终止commit程序
如果上面的程序扫描到敏感词汇,执行终止提交程序。
if (flag) {
// 退出进程
process.exit(1);
}
因为是node环境,可以直接使用process来终止进程。
接下来功能验证。
3. 验证
-
随便新建一个文件
src/test.js,添加内容console.log('这是需要检测的代码 baidu.com'); -
pre-commit.js添加敏感词汇// 敏感关键字 const censetiveWords = ['baidu.com']; -
提交commit
% git add . % git commit -m "feat: test" > hy-cli@0.0.1 pre-commit > node pre-commit.js src/test.js 检测到敏感代码 baidu.com!!! husky - pre-commit hook exited with code 1 (error)可以看到控制台已经输出敏感代码的警告信息,并退出
git commit的进程husky - pre-commit hook exited with code 1 (error)。至此实现完整功能,并自测通过。
六、.gitignore
上面实现了所有git追踪文件的过滤,但是在定义censetiveWords的时候,定义的当前文件也会被检测到,所以扫描代码的时候也要把当前文件剔除掉。
所以还有两点要注意
1. 不扫描当前文件
在扫描代码之前,添加判断
// 当前文件pre-commit.js--已取消git跟踪
const currentPath = __filename
// 扫描文件
const __path = __dirname + '/' + fileName
if (__path !== currentPath) {
// 代码扫描
}
2. 当前文件不追踪
当前文件不会提交到线上,定义的敏感词汇自然也就不会被上传
# .gitignore
# 敏感代码检测
pre-commit.js
总结
此次实现归根于一次代码上传失误事故,除了当前的实现,相信也有更好的方案规避这类问题~
人总是懒惰的,能依赖工具规避的问题,尽量不要靠人为去约束。
最后是实现源码,其实很简单:
const fs = require('fs');
const childProcess = require('child_process');
/**
* __dirname 当前文件夹路径
* __filename 当前文件路径
*/
// 提交记录
// PS: 如果是新项目第一次提交,可以 HEAD~3 换成 HEAD~1
const commitStages = childProcess.execSync('git diff --name-status HEAD~3', {
encoding: 'utf8',
});
/**
* @description 获取类型清单
* @param {*} arr
* @param {*} type M:修改,D:删除 A:新增
* @returns
*/
const getArrList = (str, type) => {
const arr = str.split('\n');
return arr.filter(item => {
const regex = new RegExp(`[${type}].*`);
if (regex.test(item)) {
return item !== undefined;
}
});
};
// 格式化--移除 M [fileName].[fileType]之间的空格
const formatList = (arr, type) => {
return arr.map(item => {
return item.replace(/\s/, '').replace(type, '');
});
};
/**
* @returns 获取commit files
* PS:不过滤删除的文件
*/
function commitFiles() {
const typeList = ['M', 'A'];
let fileList = [];
typeList.forEach(type => {
let arr = getArrList(commitStages, type);
arr = formatList(arr, type);
// 提交文件list
fileList = fileList.concat(arr);
});
return fileList;
}
// 敏感关键字扫描
function scaner(fileList = [], words = []) {
// const wordsStr = JSON.stringify(words)
// 当前文件--已取消git跟踪
const currentPath = __filename
// 文件扫描
for (const fileName of fileList) {
const __path = __dirname + '/' + fileName
if (__path !== currentPath) {
const codes = fs.readFileSync(__path, { encoding: 'utf-8' });
/**
* 扫描代码:是否有关键字
*/
let flag = false; // 是否有关键字
for (const code of words.filter(str => str)) {
flag = codes.includes(code);
if (flag) {
console.log(`${fileName} 检测到敏感代码 ${code}!!!`);
break;
}
}
if (flag) {
// 退出进程
process.exit(1);
}
}
}
}
// 敏感关键字
const censetiveWords = ['commit'];
scaner(commitFiles(), censetiveWords);