├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── CHANGELOG.MD ├── Dockerfile ├── LICENSE ├── README.MD ├── client ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── babel.config.js ├── dist │ ├── css │ │ ├── app.3e0479d6.css │ │ ├── chunk-0cb8a21e.cadfa055.css │ │ ├── chunk-1b5e6468.30b07c32.css │ │ ├── chunk-23e0191c.c91eea29.css │ │ ├── chunk-29d116de.8f86470e.css │ │ ├── chunk-3ad7cbfa.b60f58e2.css │ │ ├── chunk-78d8f7d4.f8312f18.css │ │ ├── chunk-88e65a68.552fa373.css │ │ ├── chunk-f603ab4a.fdc0c732.css │ │ └── chunk-vendors.46f94e1b.css │ ├── favicon.ico │ ├── fonts │ │ ├── element-icons.535877f5.woff │ │ └── element-icons.732389de.ttf │ ├── img │ │ └── github.58993451.svg │ ├── index.html │ └── js │ │ ├── app.ff842fe3.js │ │ ├── chunk-0cb8a21e.9054a38e.js │ │ ├── chunk-1b5e6468.9b76e4aa.js │ │ ├── chunk-23e0191c.b2c34538.js │ │ ├── chunk-29d116de.809f9ffa.js │ │ ├── chunk-2d0e5db1.aac3ca3f.js │ │ ├── chunk-2d0e8ba4.ea6cb7b3.js │ │ ├── chunk-3ad7cbfa.5e293c91.js │ │ ├── chunk-78d8f7d4.c7406151.js │ │ ├── chunk-88e65a68.9c08552b.js │ │ ├── chunk-ebb8050c.2243400d.js │ │ ├── chunk-f603ab4a.e93a6b4d.js │ │ └── chunk-vendors.c2087aaf.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── favicon.ico │ │ ├── github.svg │ │ └── no.png │ ├── components │ │ ├── Divider.vue │ │ └── NavMenu.vue │ ├── config │ │ └── api.js │ ├── main.js │ ├── router.js │ ├── style │ │ └── app.css │ ├── utils │ │ └── index.js │ └── views │ │ ├── result │ │ ├── Dashboard.vue │ │ ├── Detail.vue │ │ ├── Index.vue │ │ └── ResultsTable.vue │ │ └── setting │ │ ├── Blacklist.vue │ │ ├── Github.vue │ │ ├── Notice.vue │ │ ├── Rule.vue │ │ ├── Setting.vue │ │ └── Task.vue └── vue.config.js ├── deploy ├── apt │ └── sources.list ├── nginx │ ├── Hawkeye.conf │ ├── nginx.conf │ └── pubkey.gpg ├── pyenv │ ├── pip.conf │ └── requirements.txt └── supervisor │ ├── hawkeye.conf │ ├── huey.conf │ ├── openresty.conf │ └── redis.conf ├── docker-entrypoint.sh └── server ├── .tld_set ├── api.py ├── config └── database.py ├── controllers ├── account.py ├── health.py ├── result.py ├── setting.py └── statistic.py ├── task.py └── utils ├── asset.py ├── date.py ├── hash.py ├── log.py └── notice.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 描述 2 | 3 | ### 部署方式(Docker或其它) 4 | 5 | ### 错误日志 6 | 7 | 贴上 `docker exec xxxx tail -f /var/log/supervisor/* ` 结果 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | develop-eggs/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask stuff: 56 | instance/ 57 | .webassets-cache 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # Jupyter Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # SageMath parsed files 78 | *.sage.py 79 | 80 | # dotenv 81 | .env 82 | 83 | # config 84 | config.ini 85 | *.ini 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | deploy/supervisor.conf 105 | deploy/Hawkeye.conf 106 | client/node_modules 107 | .idea 108 | .vscode 109 | .DS_Store 110 | 111 | logs -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | ## 更新记录 2 | - 2018-10-12 v3.0.0 非兼容性更新 3 | - 搜索方式切换成API,支持添加多GitHub用户,API配额可视化 4 | - 配置可视化,去除配置文件 5 | - crontab 切换成轻量级的任务队列 Huey ,任务周期可自定义 6 | - 支持 Docker 部署 7 | - 支持批量忽略 8 | - 爬虫任务状态、结果记录展示 9 | - 优化解析受影响资产 10 | - 重构邮件 告警,添加钉钉告警 11 | 12 | - 2018-04-19 v2.0.0 13 | - 添加Basic 认证, 必须按照新的 `config.ini.example` 重新进行相关配置 14 | - 用户体验优化,支持多种过滤方式 15 | - 爬虫任务状态、结果记录展示 16 | - 解析受影响资产 17 | - 添加开关,控制是否抓取 18 | - 邮件 告警聚合 19 | - 2018.03.14 20 | - 由于 spider.py 中一处正则匹配问题,导致近日开始登录失败,请大家 pull 下新代码 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-stretch 2 | LABEL MAINTAINER=0xbug 3 | ENV TZ=Asia/Shanghai 4 | EXPOSE 80 5 | RUN apt-get update 6 | RUN apt-get install --no-install-recommends -y curl gnupg git redis-server supervisor software-properties-common wget 7 | RUN curl https://openresty.org/package/pubkey.gpg | apt-key add - 8 | RUN add-apt-repository -y "deb http://openresty.org/package/debian stretch openresty" 9 | RUN apt-get update 10 | RUN apt-get install -y openresty 11 | COPY ./deploy /Hawkeye/deploy 12 | RUN pip install --upgrade pip setuptools==45.2.0 13 | RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r /Hawkeye/deploy/pyenv/requirements.txt -U 14 | RUN cp /Hawkeye/deploy/nginx/*.conf /usr/local/openresty/nginx/conf/ 15 | RUN cp /Hawkeye/deploy/supervisor/*.conf /etc/supervisor/conf.d/ 16 | COPY ./client/dist /Hawkeye/client/dist 17 | COPY ./server /Hawkeye/server 18 | WORKDIR /Hawkeye/server 19 | COPY ./docker-entrypoint.sh ./ 20 | RUN chmod +x docker-entrypoint.sh 21 | CMD ["./docker-entrypoint.sh"] 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Hawkeye 2 | 3 | [![GitHub issues](https://img.shields.io/github/issues/0xbug/Hawkeye.svg)](https://github.com/0xbug/Hawkeye/issues) 4 | [![GitHub forks](https://img.shields.io/github/forks/0xbug/Hawkeye.svg)](https://github.com/0xbug/Hawkeye/network) 5 | [![GitHub stars](https://img.shields.io/github/stars/0xbug/Hawkeye.svg)](https://github.com/0xbug/Hawkeye/stargazers) 6 | [![Python 3.x](https://img.shields.io/badge/python-3.x-yellow.svg)](https://www.python.org/) 7 | [![GitHub license](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://raw.githubusercontent.com/0xbug/Hawkeye/master/LICENSE) 8 | 9 | ## 简介 10 | 11 | > 监控github代码库,及时发现员工托管公司代码到GitHub行为并预警,降低代码泄露风险。 12 | 13 | ## 截图 14 | 15 | ![Hawkeye](https://user-images.githubusercontent.com/12611275/46849889-0d2d0980-ce24-11e8-832e-35f6f935bf3b.png) 16 | ## **最近更新** 17 | 18 | - 2020-11-20 19 | 由于 GitHub 官方限制了API的账号密码认证,导致在配置 GitHub 账号时,需要账号输入框输入生成的 [token,不需要勾选多余的权限](https://github.com/settings/tokens),**密码输入框先输入空格然后删除空格**,最后点击添加 20 | 21 | - 2019-07-02 v3.0.1 22 | - 添加健康检查接口 /api/health 23 | - 添加企业微信告警 24 | - 支持翻页刷新 25 | 26 | - 2018-10-12 v3.0.0 非兼容性更新,需配置新数据库 27 | - 搜索方式切换成API,支持添加多GitHub用户,API配额可视化 28 | - 配置可视化,去除配置文件 29 | - crontab 切换成轻量级的任务队列 Huey ,任务周期可自定义 30 | - 支持 Docker 部署 31 | - 支持批量忽略 32 | - 爬虫任务状态、结果记录展示 33 | - 优化解析受影响资产 34 | - 重构邮件 告警,添加钉钉告警 35 | 36 | 37 | ## 特性 38 | 39 | - 周期监测 40 | - web管理 41 | - 邮箱告警通知 42 | - 黑名单添加 43 | - 爬虫任务设置 44 | 45 | ## 依赖 46 | 47 | * Python 3.x 48 | * Flask 49 | * MongoDB >= 3.x 50 | 51 | ## 支持平台 52 | 53 | * Linux, macOS 54 | 55 | 56 | ## 安装(Docker 部署) 57 | ``` 58 | docker pull daocloud.io/0xbug/hawkeye 59 | ## mongodb 需认证 60 | docker run -ti -p 80:80 -e MONGODB_URI=mongodb://username:password@ip:27017/hawkeye -e MONGODB_USER= -e MONGODB_PASSWORD= -d daocloud.io/0xbug/hawkeye 61 | ## mongodb 无认证 62 | docker run -ti -p 80:80 -e MONGODB_URI=mongodb://ip:27017 -d daocloud.io/0xbug/hawkeye 63 | 64 | ``` 65 | 或者手动 build 66 | 67 | 克隆项目到本地 68 | 69 | ```bash 70 | git clone https://github.com/0xbug/Hawkeye.git --depth 1 71 | cd Hawkeye 72 | docker build -t hawkeye . 73 | ## mongodb 需认证 74 | docker run -ti -p 80:80 -e MONGODB_URI=mongodb://username:password@ip:27017/hawkeye -e MONGODB_USER= -e MONGODB_PASSWORD= -d hawkeye 75 | ## mongodb 无认证 76 | docker run -ti -p 80:80 -e MONGODB_URI=mongodb://ip:27017 -d hawkeye 77 | 78 | ``` 79 | 80 | 或者使用docker安装mongodb 81 | 82 | mongodb无认证,快速开始 83 | ```bash 84 | ## 启动mongodb 85 | docker run -itd --name mongo -p 27017:27017 mongo 86 | 87 | ## 启动hawkeye 88 | docker run -ti --link mongo:mongo -p 80:80 -e MONGODB_URI=mongodb://mongo:27017 -d daocloud.io/0xbug/hawkeye 89 | ``` 90 | 91 | 92 | Hawkeye 支持 Python *3.x* on Linux and macOS。(2.x兼容性 需自行修改测试) 93 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /client/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /client/dist/css/app.3e0479d6.css: -------------------------------------------------------------------------------- 1 | .search-result-item .el-card .el-button-group{float:right;margin-bottom:10px}.search-result-item .el-card .el-tag{float:left;margin-right:10px}.el-menu{border-radius:0!important}body{font-family:Haas Grot Text R Web,Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#24292e;margin:0;background-color:#f5f6f7}a{text-decoration:none!important;background-color:transparent!important}.el-message{border-radius:1px!important}.el-message--error,.el-message--success{border-color:#fff!important}.el-message--error,.el-message--success,.el-notification{background-color:#fff!important;-webkit-box-shadow:0 2px 4px rgba(0,0,0,.12),0 0 6px rgba(0,0,0,.04)!important;box-shadow:0 2px 4px rgba(0,0,0,.12),0 0 6px rgba(0,0,0,.04)!important}.el-notification{width:330px;padding:20px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:2px!important;position:fixed;right:16px;-webkit-transition:opacity .3s,right .3s,top .4s,-webkit-transform .3s;transition:opacity .3s,right .3s,top .4s,-webkit-transform .3s;transition:opacity .3s,transform .3s,right .3s,top .4s;transition:opacity .3s,transform .3s,right .3s,top .4s,-webkit-transform .3s;overflow:hidden}.float-right{padding-top:0;float:right!important}.text-gray{color:#586069!important}.f6{font-size:12px!important}.page{margin-top:20px;margin-bottom:20px;float:right}.el-tag{border:none!important;font-weight:400!important}.el-tag a{color:#1890ff}.el-header{padding:0 0 0 0!important}.el-footer{position:relative;text-align:center;bottom:0!important}.el-menu-item.is-disabled{opacity:.95!important;color:#f56c6c!important;cursor:pointer!important;margin-right:20px}.el-menu-item,.el-menu-item:hover{background-color:#24292e!important}.el-collapse,.el-collapse-item__header{border-bottom:none!important}.el-collapse{border-top:none!important}.el-collapse-item__wrap{border-bottom:none!important}.clearfix{margin:5px}.card-panel{border:1px #ebeef5;border-radius:4px;padding:5px;overflow:hidden;margin-bottom:5px}.hljs{background-color:#fff!important}a{font-size:14px;color:#1890ff!important;text-decoration:none}a:hover{text-decoration:underline}.el-alert__icon.el-icon-warning,.el-message-box__status.el-icon-warning{color:orange!important}.el-form-item__label{color:#666!important;font-weight:500;font-size:16px;text-align:right;vertical-align:middle;line-height:23px!important;display:inline-block;overflow:hidden;white-space:nowrap}.el-form-item__content{font-weight:400;margin:0;font-size:13px;line-height:23px!important}.avatar{display:inline-block;overflow:hidden;line-height:1;vertical-align:middle;border-radius:3px}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#409eff!important;background-color:#fff!important;border-color:#409eff;-webkit-box-shadow:-1px 0 0 0 #409eff;box-shadow:-1px 0 0 0 #409eff}.el-main{min-height:85vh}.breadcrumb{padding:20px 20px 20px 20px;background-color:#fff} -------------------------------------------------------------------------------- /client/dist/css/chunk-0cb8a21e.cadfa055.css: -------------------------------------------------------------------------------- 1 | .el-tabs__nav-next,.el-tabs__nav-prev{margin-left:5px;margin-right:5px}.tip a{text-decoration:none;line-height:36px;color:#409eff}.el-tab-pane{padding:20px 5px 10px 5px}.el-tabs--border-card{background:#fff;border:1px solid #ebeef5;border-radius:4px;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04);box-shadow:none!important}.el-tabs__nav i{margin-right:5px} -------------------------------------------------------------------------------- /client/dist/css/chunk-1b5e6468.30b07c32.css: -------------------------------------------------------------------------------- 1 | .input-new-mail[data-v-0e42e278]{margin-right:10px;width:233px;margin-top:15px;vertical-align:bottom}.input-new-mail .el-input__inner[data-v-0e42e278]{height:25px}.dashboard{margin-bottom:10px}.dashboard .el-card{margin-bottom:5px}.dashboard .dashboard-icon{width:60px;height:60px;background-color:#1890ff;border-radius:60px}.dashboard .dashboard-icon i{font-size:60px;text-align:center;color:#fff} -------------------------------------------------------------------------------- /client/dist/css/chunk-23e0191c.c91eea29.css: -------------------------------------------------------------------------------- 1 | .input-new-blacklist[data-v-4c04a198]{margin-right:10px;width:233px;margin-top:15px;vertical-align:bottom}.input-new-blacklist .el-input__inner[data-v-4c04a198]{height:25px} -------------------------------------------------------------------------------- /client/dist/css/chunk-29d116de.8f86470e.css: -------------------------------------------------------------------------------- 1 | .filter-panel[data-v-5d72b11d]{border-bottom:1px dashed #e8e8e8}.el-form-item__label[data-v-5d72b11d]{color:#313440}.el-select[data-v-5d72b11d]{width:233px}.el-tag[data-v-5d72b11d]{border-radius:2px!important;margin-right:1px;font-weight:700}h3[data-v-5d72b11d]{color:#313440}.tag-count[data-v-5d72b11d]{float:right;color:#313440} -------------------------------------------------------------------------------- /client/dist/css/chunk-3ad7cbfa.b60f58e2.css: -------------------------------------------------------------------------------- 1 | .dashboard{margin-bottom:10px}.dashboard .el-card{margin-bottom:5px}.dashboard .dashboard-icon{width:60px;height:60px;background-color:#1890ff;border-radius:60px}.dashboard .dashboard-icon i{font-size:60px;text-align:center;color:#fff} -------------------------------------------------------------------------------- /client/dist/css/chunk-78d8f7d4.f8312f18.css: -------------------------------------------------------------------------------- 1 | .el-menu-item a{text-decoration:none;display:block} -------------------------------------------------------------------------------- /client/dist/css/chunk-88e65a68.552fa373.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.project-info img{margin-right:5px}.card{background-color:#fff;padding:10px 10px 10px 30px;border:1px solid #ebeef5;border-radius:4px;margin-bottom:5px}.el-tag--danger a{color:#f56c6c!important} -------------------------------------------------------------------------------- /client/dist/css/chunk-f603ab4a.fdc0c732.css: -------------------------------------------------------------------------------- 1 | .ignore-btn{margin-top:10px}a{color:#0366d6;text-decoration:none}a:hover{text-decoration:underline}.repo-language-color{margin-right:5px}.search-result-item .el-card{padding-top:20px!important}.project-info img{margin-right:2px} -------------------------------------------------------------------------------- /client/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/client/dist/favicon.ico -------------------------------------------------------------------------------- /client/dist/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/client/dist/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /client/dist/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/client/dist/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /client/dist/img/github.58993451.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | Hawkeye
-------------------------------------------------------------------------------- /client/dist/js/app.ff842fe3.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var a,r,o=t[0],i=t[1],l=t[2],f=0,d=[];f-1?o("i",{staticClass:"iconfont icon-dingtalk",style:t.styles.dataItemImg}):t._e(),e.webhook.indexOf("weixin")>-1?o("i",{staticClass:"iconfont icon-wechat-fill",style:t.styles.dataItemImg}):t._e(),o("div",{style:t.styles.dataItemUnit},[e.webhook.indexOf("dingtalk")>-1?o("div",{style:t.styles.unitAmount},[t._v("钉钉")]):t._e(),e.webhook.indexOf("weixin")>-1?o("div",{style:t.styles.unitAmount},[t._v("企业微信")]):t._e(),o("div",{style:t.styles.unitFooter},[t._v(t._s(e.webhook.split("=")[1].slice(0,8)))]),o("div",{style:t.styles.unitFooter},[t._v("状态:"),o("span",{style:e.enabled?"color:#52c41a":"color:#f5222d"},[t._v(t._s(e.enabled?"开启":"关闭")+"\n ")])])])])])],1)}),o("el-col",{attrs:{xs:24,sm:12,md:12,lg:4,xl:4}},[o("el-card",{attrs:{shadow:"hover"}},[o("div",{style:t.styles.dataItem,on:{click:function(e){t.WebHookDialogFormVisible=!0,t.webhook_setting={}}}},[o("i",{staticClass:"iconfont icon-plus",style:t.styles.dataItemImg}),o("div",{style:t.styles.dataItemUnit},[o("div",{style:t.styles.unitAmountSmall},[t._v("添加钉钉/微信告警")]),o("div",{style:t.styles.unitFooter},[t._v("oapi.dingtalk.com qyapi.weixin.qq.com")])])])])],1)],2),o("el-dialog",{attrs:{title:"SMTP Server",visible:t.MailDialogFormVisible,width:t.mobileClient?"80%":"50%"},on:{"update:visible":function(e){t.MailDialogFormVisible=e}},model:{value:t.MailDialogFormVisible,callback:function(e){t.MailDialogFormVisible=e},expression:"MailDialogFormVisible"}},[o("el-form",{attrs:{model:t.smtp_server}},[o("el-form-item",{attrs:{label:"服务器地址"}},[o("el-input",{attrs:{"auto-complete":"on"},model:{value:t.smtp_server.host,callback:function(e){t.$set(t.smtp_server,"host",e)},expression:"smtp_server.host"}})],1),o("el-form-item",{attrs:{label:"服务器端口"}},[o("el-input-number",{attrs:{size:"small","controls-position":"right",max:65535,min:1,step:1},model:{value:t.smtp_server.port,callback:function(e){t.$set(t.smtp_server,"port",e)},expression:"smtp_server.port"}})],1),o("el-form-item",{attrs:{label:"发件人"}},[o("el-input",{attrs:{"auto-complete":"off"},model:{value:t.smtp_server.from,callback:function(e){t.$set(t.smtp_server,"from",e)},expression:"smtp_server.from"}})],1),o("el-form-item",{attrs:{label:"TLS加密"}},[o("el-switch",{attrs:{"active-color":"#13ce66","inactive-color":"#ff4949"},model:{value:t.smtp_server.tls,callback:function(e){t.$set(t.smtp_server,"tls",e)},expression:"smtp_server.tls"}})],1),o("el-form-item",{attrs:{label:"用户名"}},[o("el-input",{attrs:{"auto-complete":"off"},model:{value:t.smtp_server.username,callback:function(e){t.$set(t.smtp_server,"username",e)},expression:"smtp_server.username"}})],1),o("el-form-item",{attrs:{label:"密码"}},[o("el-input",{attrs:{"auto-complete":"off",type:"password"},model:{value:t.smtp_server.password,callback:function(e){t.$set(t.smtp_server,"password",e)},expression:"smtp_server.password"}})],1),o("el-form-item",{attrs:{label:"监控平台地址"}},[o("el-input",{attrs:{placeholder:t.origin},model:{value:t.smtp_server.domain,callback:function(e){t.$set(t.smtp_server,"domain",e)},expression:"smtp_server.domain"}})],1),o("el-form-item",{attrs:{label:"开启通知"}},[o("el-switch",{attrs:{"active-color":"#13ce66","inactive-color":"#ff4949"},model:{value:t.smtp_server.enabled,callback:function(e){t.$set(t.smtp_server,"enabled",e)},expression:"smtp_server.enabled"}})],1)],1),o("div",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[o("el-button",{attrs:{size:"mini",round:""},on:{click:function(e){t.MailDialogFormVisible=!1}}},[t._v("取 消")]),o("el-button",{attrs:{size:"mini",type:"primary",round:""},on:{click:t.setSMTPServer}},[t._v("确 定")])],1)],1),o("el-dialog",{attrs:{visible:t.WebHookDialogFormVisible,width:t.mobileClient?"80%":"50%"},on:{"update:visible":function(e){t.WebHookDialogFormVisible=e}},model:{value:t.WebHookDialogFormVisible,callback:function(e){t.WebHookDialogFormVisible=e},expression:"WebHookDialogFormVisible"}},[o("div",{style:t.styles.unitAmountSmall,attrs:{slot:"title"},slot:"title"},[t._v("\n webhook 配置\n 目前支持 钉钉/企业微信\n ")]),o("el-form",{attrs:{model:t.webhook_setting}},[o("el-form-item",{attrs:{label:"webhook"}},[o("el-input",{attrs:{placeholder:"https://oapi.dingtalk.com/robot/send?access_token=xxx 或 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"},model:{value:t.webhook_setting.webhook,callback:function(e){t.$set(t.webhook_setting,"webhook",e)},expression:"webhook_setting.webhook"}})],1),o("el-form-item",{attrs:{label:"监控平台地址"}},[o("el-input",{attrs:{placeholder:t.origin},model:{value:t.webhook_setting.domain,callback:function(e){t.$set(t.webhook_setting,"domain",e)},expression:"webhook_setting.domain"}})],1),o("el-form-item",{attrs:{label:"开启通知"}},[o("el-switch",{attrs:{"active-color":"#13ce66","inactive-color":"#ff4949"},model:{value:t.webhook_setting.enabled,callback:function(e){t.$set(t.webhook_setting,"enabled",e)},expression:"webhook_setting.enabled"}})],1),o("el-form-item",{attrs:{label:"测试"}},[o("el-button",{attrs:{size:"mini",round:"",disabled:!t.webhook_setting.hasOwnProperty("webhook")},on:{click:t.testWebHookSetting}},[t._v("发送一条测试消息\n ")])],1)],1),o("div",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[o("el-button",{attrs:{size:"mini",round:""},on:{click:function(e){t.WebHookDialogFormVisible=!1}}},[t._v("取 消")]),o("el-button",{attrs:{size:"mini",type:"primary",round:""},on:{click:t.setWebHookSetting}},[t._v("确 定")])],1)],1),t.smtp_server.enabled?o("div",[o("el-input",{staticClass:"input-new-mail",attrs:{size:"mini",placeholder:"邮箱格式:username@domain.com"},nativeOn:{keyup:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.handleInputNoticeConfirm(e)}},model:{value:t.inputValue,callback:function(e){t.inputValue=e},expression:"inputValue"}}),o("el-button",{attrs:{size:"mini",type:"primary"},on:{click:t.handleInputNoticeConfirm}},[t._v("添加")]),o("el-table",{staticStyle:{width:"100%"},attrs:{stripe:"","tooltip-effect":"dark",data:t.mails}},[o("el-table-column",{attrs:{prop:"mail",label:"邮箱"}}),o("el-table-column",{attrs:{label:"操作"},scopedSlots:t._u([{key:"default",fn:function(e){return[o("el-button",{attrs:{size:"mini",type:"danger",round:""},on:{click:function(o){return t.handleDeleteNotice(e.row)}}},[t._v("删除\n ")])]}}],null,!1,1341244012)})],1)],1):t._e()],1)},i=[],a=(o("0857"),{dataItem:{display:"flex",flexBasis:"50%",alignItems:"center"},dataItemImg:{color:"#1890ff",width:"30px",marginTop:"auto",marginBottom:"auto",fontSize:"50px",marginRight:"30px"},dataItemUnit:{height:"50px",display:"flex",flexBasis:"50%",flexDirection:"column",justifyContent:"space-between"},unitTitle:{color:"#666",fontSize:"12px"},unitAmount:{color:"#333",fontSize:"24px"},unitAmountSmall:{color:"#999",fontSize:"14px"},unitFooter:{color:"#999",fontSize:"12px"}}),n={data:function(){return{styles:a,origin:window.location.origin,inputValue:"",popoverVisible:!1,MailDialogFormVisible:!1,WebHookDialogFormVisible:!1,mails:[],formLabelWidth:"200",smtp_server:{},webhook_setting:{domain:window.location.origin},webhooks:[]}},computed:{mobileClient:function(){return document.documentElement.clientWidth1&&(r+="s"),["".concat(e," ").concat(r," ago"),"in ".concat(e," ").concat(r)]},l={en_US:a,zh_CN:i},s=function(e,t){l[e]=t};t.register=s;var c=function(e){return l[e]||a};t.getLocale=c},"5aa0":function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"format",{enumerable:!0,get:function(){return n.format}}),Object.defineProperty(t,"render",{enumerable:!0,get:function(){return o.render}}),Object.defineProperty(t,"cancel",{enumerable:!0,get:function(){return o.cancel}}),Object.defineProperty(t,"register",{enumerable:!0,get:function(){return i.register}}),t.version=void 0;var n=r("a51e"),o=r("408c3"),i=r("56f3"),a="4.0.0-beta.2";t.version=a},"8ce0":function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.nextInterval=t.diffSec=t.formatDiff=t.toDate=t.toInt=void 0;var n=[60,60,24,7,365/7/12,12],o=function(e){return parseInt(e)};t.toInt=o;var i=function(e){return e instanceof Date?e:!isNaN(e)||/^\d+$/.test(e)?new Date(o(e)):(e=(e||"").trim().replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/(\d)T(\d)/,"$1 $2").replace(/Z/," UTC").replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"),new Date(e))};t.toDate=i;var a=function(e,t){for(var r=0,i=e<0?1:0,a=e=Math.abs(e);e>=n[r]&&r(0===r?9:1)&&(r+=1),t(e,r,a)[i].replace("%s",e)};t.formatDiff=a;var l=function(e,t){return t=t?i(t):new Date,(t-i(e))/1e3};t.diffSec=l;var s=function(e){for(var t=1,r=0,o=Math.abs(e);e>=n[r]&&r-1?this.$message.success(t+e.reason):this.$message.warning(t+e.reason)},handleDeleteQuery:function(e,t){var r=this;this.axios.delete("".concat(this.api.settingQuery,"?_id=").concat(t._id,"&tag=").concat(t.tag)).then(function(e){r.$message.success(e.data.msg),r.dialogFormVisible=!1,r.rules=e.data.result}).catch(function(e){r.$message.error(e.toString()),r.dialogFormVisible=!1})},updateEnabled:function(e){var t=this,r={tag:e.tag,keyword:e.keyword,enabled:e.enabled};this.axios.post(this.api.settingQuery,r).then(function(e){t.$message.success(e.data.msg),t.dialogFormVisible=!1,t.rules=e.data.result}).catch(function(e){t.$message.error(e.toString()),t.dialogFormVisible=!1})},handleAddQuery:function(e){var t=this;this.axios.post(this.api.settingQuery,e).then(function(e){t.$message.success(e.data.msg),t.dialogFormVisible=!1,t.rules=e.data.result,t.form={tag:"",keyword:"",enabled:!0}}).catch(function(e){t.$message.error(e.toString()),t.dialogFormVisible=!1})}},mounted:function(){this.fetchQuery()}},l=a,s=r("17cc"),c=Object(s["a"])(l,n,o,!1,null,null,null);t["default"]=c.exports},f7f7:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getTimerId=t.saveTimerId=t.getDateAttribute=void 0;var n="timeago-tid",o="datetime",i=function(e,t){return e.getAttribute?e.getAttribute(t):e.attr?e.attr(t):void 0},a=function(e){return i(e,o)};t.getDateAttribute=a;var l=function(e,t){return e.setAttribute?e.setAttribute(n,t):e.attr?e.attr(n,t):void 0};t.saveTimerId=l;var s=function(e){return i(e,n)};t.getTimerId=s}}]); -------------------------------------------------------------------------------- /client/dist/js/chunk-f603ab4a.e93a6b4d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-f603ab4a"],{"4ea0":function(t,e,a){"use strict";a.r(e);var o=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",[a("el-row",{staticClass:"ignore-btn",attrs:{gutter:10}},[a("el-col",{attrs:{span:2}},[a("el-button",{directives:[{name:"show",rawName:"v-show",value:t.selections.length>0,expression:"selections.length > 0"}],attrs:{type:"danger",round:"",size:"mini"},on:{click:function(e){t.handleIgnore(t.selections.map(function(t){return t._id}))}}},[t._v("忽略\n ")])],1)],1),a("el-table",{ref:"multipleTable",staticStyle:{width:"100%"},attrs:{stripe:"","tooltip-effect":"dark",data:t.results},on:{"selection-change":t.handleSelectionChange}},[a("div",{staticStyle:{margin:"20px"},attrs:{slot:"empty"},slot:"empty"},[a("img",{attrs:{src:"data:image/png;base64,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",alt:""}})]),a("el-table-column",{attrs:{type:"selection",width:"40"}}),a("el-table-column",{attrs:{label:"发现时间",width:"200"},scopedSlots:t._u([{key:"default",fn:function(e){return[a("i",{staticClass:"iconfont icon-time_fill"}),t._v("\n "+t._s(t._f("dateFormat")(e.row.datetime))+"\n ")]}}])}),a("el-table-column",{attrs:{label:"项目",width:"200","show-overflow-tooltip":""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("img",{staticClass:"avatar flex-shrink-0 mr-2",attrs:{src:e.row.avatar_url,width:"32",height:"32",alt:"@"+e.row.username}}),a("a",{staticClass:"link-gray-dark no-underline text-bold wb-break-all",attrs:{href:e.row.project_url,target:"_blank"}},[t._v(" "+t._s(e.row.project))])]}}])}),a("el-table-column",{attrs:{prop:"language",label:"语言",width:"100","show-overflow-tooltip":""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("span",{staticClass:"repo-language-color ml-0",staticStyle:{"background-color":"#555555"}}),t._v("\n "+t._s(e.row.language?e.row.language:"未知")+"\n ")]}}])}),a("el-table-column",{attrs:{prop:"filename",label:"文件名",width:"150","show-overflow-tooltip":""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("router-link",{attrs:{to:"/view/leakage/"+e.row._id,target:"_blank"}},[t._v("\n "+t._s(e.row.filename)+" \n ")])]}}])}),a("el-table-column",{attrs:{label:"备注","show-overflow-tooltip":""},scopedSlots:t._u([{key:"default",fn:function(e){return e.row.desc?[a("el-tag",{attrs:{size:"mini",type:e.row.security?"success":"danger","disable-transitions":""}},[t._v("\n "+t._s(e.row.desc)+"\n ")])]:void 0}}],null,!0)}),a("el-table-column",{attrs:{label:"标签",width:"100","show-overflow-tooltip":""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("router-link",{attrs:{to:"/?tag="+e.row.tag}},[a("el-tag",{attrs:{size:"mini"}},[t._v(t._s(e.row.tag))])],1)]}}])}),a("el-table-column",{attrs:{label:"Star/Fork",width:"300"},scopedSlots:t._u([{key:"default",fn:function(t){return[a("div",{staticClass:"project-info"},[a("img",{attrs:{src:"https://img.shields.io/github/issues/"+t.row.project+".svg",alt:""}}),a("img",{attrs:{src:"https://img.shields.io/github/forks/"+t.row.project+".svg",alt:""}}),a("img",{attrs:{src:"https://img.shields.io/github/stars/"+t.row.project+".svg",alt:""}})])]}}])}),a("el-table-column",{attrs:{label:"操作",width:"300"},scopedSlots:t._u([{key:"default",fn:function(e){return[a("el-button-group",[a("el-button",{attrs:{round:"",size:"mini"},on:{click:function(a){return t.handleOpen("https://github.com/"+e.row.project+"/commits")}}},[a("i",{staticClass:"iconfont icon-github-fill"}),t._v("\n Commits\n ")]),a("el-button",{attrs:{round:"",size:"mini"},on:{click:function(a){return t.handleOpen("https://github.com/"+e.row.project+"/search?utf8=✓&q=pass OR password OR passwd OR pwd OR smtp OR database")}}},[a("i",{staticClass:"iconfont icon-flashlight"}),t._v("\n 快速排查\n ")])],1)]}}])})],1)],1)},n=[],s=(a("f763"),a("0857"),{props:["results"],name:"results-table",data:function(){return{selections:[]}},methods:{handleOpen:function(t){window.open(t,"_blank")},handleSelectionChange:function(t){this.selections=t},ignoreLeakage:function(t){var e=this,a={security:1,ignore:1,desc:"",id:t};this.axios.patch(this.api.leakage,a).then(function(t){e.$emit("change")}).catch(function(t){e.$message.error(t.toString())})},handleIgnore:function(t){var e=this;this.$confirm("此操作将忽略结果, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then(function(){t.forEach(function(t){e.ignoreLeakage(t)}),e.$message({type:"success",message:"处理成功"})}).catch(function(){e.$message({type:"error",message:"已取消"})})}}}),i=s,l=(a("eb42"),a("17cc")),r=Object(l["a"])(i,o,n,!1,null,null,null);e["default"]=r.exports},"4eeb":function(t,e,a){},eb42:function(t,e,a){"use strict";var o=a("4eeb"),n=a.n(o);n.a},f763:function(t,e,a){for(var o=a("dac5"),n=a("cfc7"),s=a("e5ef"),i=a("3754"),l=a("743d"),r=a("14fc"),c=a("8b37"),A=c("iterator"),g=c("toStringTag"),u=r.Array,d={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},h=n(d),p=0;p 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hawkeye 10 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | 40 | 51 | -------------------------------------------------------------------------------- /client/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/client/src/assets/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/client/src/assets/no.png -------------------------------------------------------------------------------- /client/src/components/Divider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/components/NavMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 42 | 43 | 49 | -------------------------------------------------------------------------------- /client/src/config/api.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | const apiUri = "/api"; 3 | const trend = `${apiUri}/trend`; 4 | const statistic = `${apiUri}/statistic`; 5 | const leakage = `${apiUri}/leakage`; 6 | const leakageInfo = `${apiUri}/leakage/info`; 7 | const leakageCode = `${apiUri}/leakage/code`; 8 | const settingBlacklist = `${apiUri}/setting/blacklist`; 9 | const settingQuery = `${apiUri}/setting/query`; 10 | const settingCron = `${apiUri}/setting/cron`; 11 | const settingNotice = `${apiUri}/setting/notice`; 12 | const settingMail = `${apiUri}/setting/mail`; 13 | const settingWebHook = `${apiUri}/setting/webhook`; 14 | const settingGithub = `${apiUri}/setting/github`; 15 | const api = { 16 | leakage, 17 | leakageCode, 18 | leakageInfo, 19 | settingBlacklist, 20 | settingQuery, 21 | settingNotice, 22 | settingCron, 23 | settingGithub, 24 | settingWebHook, 25 | settingMail, 26 | statistic, 27 | trend 28 | }; 29 | export default api; 30 | Vue.prototype.api = api; 31 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import "@/config/api"; 5 | import axios from "axios"; 6 | import VueHighlightJS from "vue-highlight.js"; 7 | import python from "highlight.js/lib/languages/python"; 8 | import Element from "element-ui"; 9 | import "element-ui/lib/theme-chalk/index.css"; 10 | import {b64Decode, timeAgo, toThousands, dateFormat} from "./utils"; 11 | 12 | Vue.use(VueHighlightJS, { 13 | languages: { 14 | python 15 | } 16 | }); 17 | Vue.filter("b64Decode", b64Decode); 18 | Vue.filter("timeAgo", timeAgo); 19 | Vue.filter("dateFormat", dateFormat); 20 | Vue.filter("toThousands", toThousands); 21 | Vue.config.productionTip = false; 22 | Vue.use(VueHighlightJS); 23 | Vue.use(Element); 24 | Vue.prototype.axios = axios; 25 | 26 | new Vue({ 27 | router, 28 | render: h => h(App) 29 | }).$mount("#app"); 30 | -------------------------------------------------------------------------------- /client/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | 4 | const LeakageData = () => import("@/views/result/Index"); 5 | const Detail = () => import("@/views/result/Detail"); 6 | const Setting = () => import("@/views/setting/Setting"); 7 | 8 | Vue.use(Router); 9 | const routes = [ 10 | { 11 | path: "/", name: "index", meta: {name: '首页'}, component: LeakageData 12 | }, 13 | {path: "/view/leakage/:id", name: 'detail', meta: {name: '详情'}, component: Detail}, 14 | {path: "/setting/:tab", name: "setting", meta: {name: '设置'}, component: Setting} 15 | ]; 16 | export default new Router({ 17 | mode: "history", 18 | routes 19 | }); 20 | -------------------------------------------------------------------------------- /client/src/style/app.css: -------------------------------------------------------------------------------- 1 | 2 | .search-result-item .el-card .el-button-group { 3 | float: right; 4 | margin-bottom: 10px; 5 | } 6 | 7 | .search-result-item .el-card .el-tag { 8 | float: left; 9 | margin-right: 10px; 10 | 11 | } 12 | 13 | .el-menu { 14 | border-radius: 0 !important; 15 | } 16 | 17 | body { 18 | font-family: "Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-size: 14px; 20 | line-height: 1.5; 21 | color: #24292e; 22 | margin: 0; 23 | background-color: rgb(245, 246, 247); 24 | } 25 | 26 | a { 27 | text-decoration: none !important; 28 | background-color: transparent !important; 29 | } 30 | 31 | .el-message { 32 | border-radius: 1px !important; 33 | } 34 | 35 | .el-message--success { 36 | background-color: #fff !important; 37 | border-color: #fff !important; 38 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04) !important; 39 | } 40 | 41 | .el-message--error { 42 | background-color: #fff !important; 43 | border-color: #fff !important; 44 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04) !important; 45 | } 46 | 47 | .el-notification { 48 | width: 330px; 49 | padding: 20px; 50 | box-sizing: border-box; 51 | border-radius: 2px !important; 52 | position: fixed; 53 | right: 16px; 54 | background-color: #fff !important; 55 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04) !important; 56 | transition: opacity 0.3s, transform 0.3s, right 0.3s, top 0.4s; 57 | overflow: hidden; 58 | } 59 | 60 | .float-right { 61 | padding-top: 0; 62 | float: right !important; 63 | } 64 | 65 | .text-gray { 66 | color: #586069 !important; 67 | } 68 | 69 | .f6 { 70 | font-size: 12px !important; 71 | } 72 | 73 | .page { 74 | margin-top: 20px; 75 | margin-bottom: 20px; 76 | float: right; 77 | } 78 | 79 | .el-tag { 80 | border: none !important; 81 | font-weight: normal !important; 82 | } 83 | 84 | .el-tag a { 85 | color: rgb(24, 144, 255) 86 | } 87 | 88 | .el-header { 89 | padding: 0 0 0 0 !important; 90 | 91 | } 92 | 93 | .el-footer { 94 | position: relative; 95 | text-align: center; 96 | bottom: 0 !important; 97 | } 98 | 99 | .el-menu-item.is-disabled { 100 | opacity: .95 !important; 101 | color: #F56C6C !important; 102 | cursor: pointer !important; 103 | margin-right: 20px; 104 | } 105 | 106 | .el-menu-item:hover { 107 | background-color: rgb(36, 41, 46) !important; 108 | } 109 | 110 | .el-menu-item { 111 | background-color: rgb(36, 41, 46) !important; 112 | } 113 | 114 | .el-collapse-item__header { 115 | border-bottom: none !important; 116 | } 117 | 118 | .el-collapse { 119 | border-bottom: none !important; 120 | border-top: none !important; 121 | } 122 | 123 | .el-collapse-item__wrap { 124 | border-bottom: none !important; 125 | 126 | } 127 | .clearfix { 128 | margin: 5px; 129 | } 130 | 131 | .card-panel { 132 | border: 1px #ebeef5; 133 | border-radius: 4px; 134 | padding: 5px; 135 | overflow: hidden; 136 | margin-bottom: 5px; 137 | 138 | } 139 | 140 | .hljs { 141 | background-color: #fff !important; 142 | } 143 | 144 | a { 145 | font-size: 14px; 146 | color: rgb(24, 144, 255) !important; 147 | text-decoration: none; 148 | } 149 | 150 | a:hover { 151 | text-decoration: underline; 152 | } 153 | 154 | .el-message-box__status.el-icon-warning, .el-alert__icon.el-icon-warning { 155 | color: orange !important; 156 | } 157 | 158 | .el-form-item__label { 159 | color: #666 !important; 160 | font-weight: 500; 161 | font-size: 16px; 162 | text-align: right; 163 | vertical-align: middle; 164 | line-height: 23px !important; 165 | display: inline-block; 166 | overflow: hidden; 167 | white-space: nowrap; 168 | } 169 | 170 | .el-form-item__content { 171 | font-weight: 400; 172 | margin: 0; 173 | font-size: 13px; 174 | line-height: 23px !important; 175 | } 176 | 177 | .avatar { 178 | display: inline-block; 179 | overflow: hidden; 180 | line-height: 1; 181 | vertical-align: middle; 182 | border-radius: 3px; 183 | } 184 | 185 | .el-radio-button__orig-radio:checked + .el-radio-button__inner { 186 | color: #409EFF !important; 187 | background-color: #ffffff !important; 188 | border-color: #409EFF; 189 | -webkit-box-shadow: -1px 0 0 0 #409EFF; 190 | box-shadow: -1px 0 0 0 #409EFF; 191 | } -------------------------------------------------------------------------------- /client/src/utils/index.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import "dayjs/locale/zh-cn"; 3 | import { Base64 } from "js-base64"; 4 | import relativeTime from "dayjs/plugin/relativeTime"; 5 | 6 | dayjs.extend(relativeTime); 7 | dayjs.locale("zh-cn"); 8 | 9 | export const timeAgo = time => { 10 | return dayjs().from(dayjs(time)); 11 | }; 12 | 13 | export const dateFormat = time => { 14 | return dayjs(time).format("YYYY-MM-DD HH:mm"); 15 | }; 16 | export const dateTimeFormat = time => { 17 | return dayjs(time).format("YYYY-MM-DD"); 18 | }; 19 | export const b64Decode = val => { 20 | return Base64.decode(val); 21 | }; 22 | export const toThousands = num => { 23 | return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, "$1,"); 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/views/result/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 98 | 99 | 118 | -------------------------------------------------------------------------------- /client/src/views/result/Detail.vue: -------------------------------------------------------------------------------- 1 | 113 | 192 | 193 | 210 | -------------------------------------------------------------------------------- /client/src/views/result/Index.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 80 | 282 | 283 | 312 | -------------------------------------------------------------------------------- /client/src/views/setting/Blacklist.vue: -------------------------------------------------------------------------------- 1 | 44 | 102 | 103 | 115 | -------------------------------------------------------------------------------- /client/src/views/setting/Github.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 120 | -------------------------------------------------------------------------------- /client/src/views/setting/Notice.vue: -------------------------------------------------------------------------------- 1 | 167 | 364 | 376 | 377 | -------------------------------------------------------------------------------- /client/src/views/setting/Rule.vue: -------------------------------------------------------------------------------- 1 | 150 | 151 | 249 | -------------------------------------------------------------------------------- /client/src/views/setting/Setting.vue: -------------------------------------------------------------------------------- 1 | 30 | 57 | 86 | -------------------------------------------------------------------------------- /client/src/views/setting/Task.vue: -------------------------------------------------------------------------------- 1 | 26 | 70 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeCompiler: true, 3 | 4 | css: undefined, 5 | 6 | chainWebpack: config => { 7 | config.module 8 | .rule("vue") 9 | .use("vue-loader") 10 | .loader("vue-loader") 11 | .tap(options => { 12 | return options; 13 | }); 14 | }, 15 | devServer: { 16 | proxy: { 17 | "/api/": { 18 | target: "http://0.0.0.0:8999", 19 | changeOrigin: true 20 | } 21 | } 22 | }, 23 | baseUrl: undefined, 24 | outputDir: undefined, 25 | assetsDir: undefined, 26 | filenameHashing: true, 27 | productionSourceMap: false, 28 | parallel: undefined 29 | }; 30 | -------------------------------------------------------------------------------- /deploy/apt/sources.list: -------------------------------------------------------------------------------- 1 | deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib 2 | deb-src http://mirrors.aliyun.com/debian/ stretch main non-free contrib 3 | deb http://mirrors.aliyun.com/debian-security stretch/updates main 4 | deb-src http://mirrors.aliyun.com/debian-security stretch/updates main 5 | deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib 6 | deb-src http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib 7 | deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib 8 | deb-src http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib -------------------------------------------------------------------------------- /deploy/nginx/Hawkeye.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | charset utf-8; 4 | location / { 5 | gzip on; 6 | gzip_min_length 1k; 7 | gzip_buffers 16 64k; 8 | gzip_http_version 1.1; 9 | gzip_comp_level 9; 10 | gzip_types text/plain text/javascript application/javascript image/jpeg image/gif image/png application/font-woff application/x-javascript text/css application/xml; 11 | gzip_vary on; 12 | root /Hawkeye/client/dist; 13 | try_files $uri /index.html; 14 | index index.html index.htm index.php; 15 | } 16 | location /api { 17 | gzip_min_length 1k; 18 | gzip_buffers 16 64k; 19 | gzip_http_version 1.1; 20 | gzip_comp_level 9; 21 | gzip_types text/plain text/javascript application/javascript image/jpeg image/gif image/png application/font-woff application/x-javascript text/css application/xml; 22 | gzip_vary on; 23 | proxy_pass http://127.0.0.1:8888; 24 | proxy_set_header Host $host; 25 | proxy_set_header X-Real-IP $remote_addr; 26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 27 | } 28 | location /static { 29 | gzip_min_length 1k; 30 | gzip_buffers 16 64k; 31 | gzip_http_version 1.1; 32 | gzip_comp_level 9; 33 | gzip_types text/plain text/javascript application/javascript image/jpeg image/gif image/png application/font-woff application/x-javascript text/css application/xml; 34 | gzip_vary on; 35 | root /Hawkeye/client/dist; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /deploy/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes 10; 3 | error_log logs/error.log; 4 | events { 5 | worker_connections 1024; 6 | } 7 | http { 8 | include mime.types; 9 | include Hawkeye.conf; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log logs/access.log main; 15 | client_max_body_size 50m; 16 | sendfile on; 17 | keepalive_timeout 65; 18 | gzip on; 19 | } 20 | -------------------------------------------------------------------------------- /deploy/nginx/pubkey.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFkg3CEBCADG5Vem2p+1p6yV2jZfNsbJBPY1KYzR9weF/K3hmLODcrTaWfiD 4 | EugHwKlAptGDGBtrMsERjUiOWUNUS8IHa+R6tzhnQePG6wO7/yGWBC4J82BkCT2x 5 | M7zCDgldtNYkNqoBc0UfE4ln+WR/RX1DuzPM+DTBZBXLqRJVJFyFtHVJn8I5HPO2 6 | hj51uYqHsewTyAkGzABV4gmSIETSmcU5KDisQ9Vt5OllE0ylh7+kakDFZklyBCHT 7 | 3IAuZhA18mw2qk1z5bnn/GpQ4fJi5w25lb9sqhhxta3ogwWWdJzXA+Nevb2dez8i 8 | bpzPeFnba9q0UVD2VJ25e99DpG15aPvNt+tbABEBAAG0JU9wZW5SZXN0eSBBZG1p 9 | biA8YWRtaW5Ab3BlbnJlc3R5LmNvbT6JATgEEwECACIFAlkg3CECGwMGCwkIBwMC 10 | BhUIAgkKCwQWAgMBAh4BAheAAAoJEJfbdEPV7et08k0H/iIOiZmDavSKE7NSPLxS 11 | ddRLh4+OGL3QW8JZh/+UaX1Z17G3q8kKSJwOemZBL/jIDkoMuHs1Hq0yp9vJ8BaS 12 | 5unX+FRmivwdS5yvkS9s3oA3iJbHagXw0KMnr+baDDNrUwo9MeO0m9muNF/eDoRz 13 | FZ9SpOrgxwhz0kOOt8j+gxWk3TaQ6/JonH4rm3XtP4GMKOKQuUo6l8+pMPEfM209 14 | nMv82kRPAxRV2TwRYToB+TithLTQytJTBytLA+ck5Ny8sGoO5PQWRyx6gj+Bhg0O 15 | rFXfg7/sP2/FEeiDZcF2qn/VMDPvnC7ux2EQdI05MMGFY/pkjVYtLJC2Nb17Bcqj 16 | DH25AQ0EWSDcIQEIAJOoTY0vf0mr+PGUbnv0KKtk65CTzKmICmWIAkCxZaTH+o/3 17 | Lt9ZDtANH1ot3xVTkKg+qBuexh53jnyXyIaIfNqavH1gm+9JusrApVOad2ruODT5 18 | XeVamz0blq37LTmJ7A4T1i8WvB0BQ3j1vh6XkVW6xq1URzVOYyhVqNNq2UIP9M9Y 19 | wtiIIans5i11qmDtZwqxcSYoqSjgz+03M6Dn0UPB1OQdHjOPx7GwHG8+6sVyr+8A 20 | 9G8SlKWre2/qdDyZNdgxalOi5ManCwWSURJRuY7s858qFUm0/5dLMAtWWEbYEmYc 21 | EUxbxQM2jPEaDmvvauZNup+a5DZXpRjpWcg19c0AEQEAAYkBHwQYAQIACQUCWSDc 22 | IQIbDAAKCRCX23RD1e3rdG0dB/9EWT8sTVPOlgFAF2WVZT3bFiqiIC9Dg6Wblt/K 23 | Id/p73gbDNTkeeTvGErAPPQwsKkbD1w2rIYoRzEJ1zVrLgaAbeH/frbQaYNu7c+3 24 | Wm93gxBxjL9Jyrs3jq5jwR4kJ5j+a/GEPtTDqtXzZHvyCP2PWDoQWANNAQDuTpYE 25 | LGHfDF9pmTVwuhkh2IFcH/ZBZUvcxP/w3jXqEiPti/rFN8wKSQtBgWI0pBpXGdrJ 26 | Tl3mIE4jLbPmkxidP1yUFx9wzEVu3soXViehMua9nOeotGOKF4DgekzCnFuXNnd3 27 | h2EiDJbMKk+QJcMPliIePZCP9JWj7n0ok9ccLg5XcNwiFEtn 28 | =U4Wk 29 | -----END PGP PUBLIC KEY BLOCK----- -------------------------------------------------------------------------------- /deploy/pyenv/pip.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | index-url = https://pypi.tuna.tsinghua.edu.cn/simple -------------------------------------------------------------------------------- /deploy/pyenv/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.2 2 | certifi==2018.8.24 3 | chardet==3.0.4 4 | Click==7.0 5 | Deprecated==1.2.3 6 | Flask==1.0.2 7 | Flask-RESTful==0.3.6 8 | gunicorn==19.9.0 9 | huey==1.10.2 10 | idna==2.7 11 | itsdangerous==0.24 12 | Jinja2==2.10 13 | MarkupSafe==1.0 14 | psutil==5.4.7 15 | PyGithub==1.43.2 16 | PyJWT==1.6.4 17 | pymongo==3.7.1 18 | pytz==2018.5 19 | redis==2.10.6 20 | requests==2.19.1 21 | requests-file==1.4.3 22 | six==1.11.0 23 | tldextract==2.2.0 24 | urllib3==1.23 25 | Werkzeug==0.14.1 26 | wrapt==1.10.11 27 | -------------------------------------------------------------------------------- /deploy/supervisor/hawkeye.conf: -------------------------------------------------------------------------------- 1 | [program:hawkeye] 2 | command=/usr/local/bin/gunicorn -w10 -b127.0.0.1:8888 api:app 3 | directory=/Hawkeye/server 4 | startsecs=5 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true -------------------------------------------------------------------------------- /deploy/supervisor/huey.conf: -------------------------------------------------------------------------------- 1 | [program:huey] 2 | command=/usr/local/bin/huey_consumer.py task.huey -k process -w 4 3 | directory=/Hawkeye/server 4 | startsecs=5 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true 8 | -------------------------------------------------------------------------------- /deploy/supervisor/openresty.conf: -------------------------------------------------------------------------------- 1 | [program:openresty] 2 | command=/usr/local/openresty/bin/openresty -g "daemon off;" 3 | directory=/tmp 4 | startsecs=5 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true -------------------------------------------------------------------------------- /deploy/supervisor/redis.conf: -------------------------------------------------------------------------------- 1 | [program:redis] 2 | command=/usr/bin/redis-server 3 | directory=/tmp 4 | startsecs=10 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf&&tail -f /var/log/supervisor/* -------------------------------------------------------------------------------- /server/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from flask import Flask 4 | from flask_restful import Api 5 | from controllers.result import Leakage, LeakageCode, LeakageInfo 6 | from controllers.setting import Blacklist, Cron, Notice, Query, GithubAccount, SMTPServer, WebHookNotice 7 | from controllers.statistic import Dashboard, Statistic 8 | from controllers.health import Status 9 | import re 10 | import sys 11 | 12 | if sys.hexversion > 0x03070000: 13 | re._pattern_type = re.Pattern 14 | 15 | app = Flask(__name__) 16 | api = Api(app) 17 | 18 | api.add_resource(Status, '/api/health') 19 | api.add_resource(Leakage, '/api/leakage') 20 | api.add_resource(Cron, '/api/setting/cron') 21 | api.add_resource(Query, '/api/setting/query') 22 | api.add_resource(Notice, '/api/setting/notice') 23 | api.add_resource(GithubAccount, '/api/setting/github') 24 | api.add_resource(WebHookNotice, '/api/setting/webhook') 25 | api.add_resource(SMTPServer, '/api/setting/mail') 26 | api.add_resource(Dashboard, '/api/trend') 27 | api.add_resource(Statistic, '/api/statistic') 28 | api.add_resource(LeakageCode, '/api/leakage/code') 29 | api.add_resource(LeakageInfo, '/api/leakage/info') 30 | api.add_resource(Blacklist, '/api/setting/blacklist') 31 | 32 | if __name__ == '__main__': 33 | app.run(host='0.0.0.0', threaded=True, use_reloader=True) 34 | -------------------------------------------------------------------------------- /server/config/database.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient, DESCENDING 2 | from redis import Redis 3 | import os 4 | 5 | if os.environ.get('MONGODB_URI'): 6 | MONGODB_URI = os.environ.get('MONGODB_URI') 7 | else: 8 | MONGODB_URI = 'mongodb://localhost:27017' 9 | 10 | client = MongoClient(MONGODB_URI, connect=False) 11 | db = client.get_database('hawkeye') 12 | 13 | if os.environ.get('MONGODB_USER'): 14 | MONGODB_USER = os.environ.get('MONGODB_USER') 15 | MONGODB_PASSWORD = os.environ.get('MONGODB_PASSWORD') 16 | db.authenticate(MONGODB_USER, MONGODB_PASSWORD) 17 | 18 | result_col = db.get_collection('result') 19 | query_col = db.get_collection('query') 20 | blacklist_col = db.get_collection('blacklist') 21 | task_col = db.get_collection('task') 22 | notice_col = db.get_collection('notice') 23 | github_col = db.get_collection('github') 24 | setting_col = db.get_collection('setting') 25 | 26 | if os.environ.get('REDIS_HOST'): 27 | REDIS_HOST = os.environ.get('REDIS_HOST') 28 | else: 29 | REDIS_HOST = 'localhost' 30 | if os.environ.get('REDIS_PORT'): 31 | REDIS_PORT = int(os.environ.get('REDIS_PORT')) 32 | else: 33 | REDIS_PORT = 6379 34 | result_cache = Redis(host=REDIS_HOST, port=REDIS_PORT, 35 | db=1, decode_responses=True) 36 | 37 | 38 | def create_indexes(): 39 | for filed in ['language', 'tag', 'datetime', 'security', 'desc', 'ignore', 'timestamp']: 40 | try: 41 | result_col.create_index(filed, background=True) 42 | except: 43 | pass 44 | -------------------------------------------------------------------------------- /server/controllers/account.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/server/controllers/account.py -------------------------------------------------------------------------------- /server/controllers/health.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask_restful import Resource 3 | from pymongo import MongoClient 4 | import os 5 | import requests 6 | 7 | 8 | class Status(Resource): 9 | def get(self): 10 | try: 11 | github = 'api.github.com' in requests.get('https://api.github.com/', timeout=30).text 12 | except Exception as error: 13 | github = str(error) 14 | try: 15 | if os.environ.get('MONGODB_URI'): 16 | MONGODB_URI = os.environ.get('MONGODB_URI') 17 | else: 18 | MONGODB_URI = 'mongodb://localhost:27017' 19 | 20 | client = MongoClient(MONGODB_URI, connect=False, socketTimeoutMS=50) 21 | db = client.get_database('hawkeye') 22 | if os.environ.get('MONGODB_USER'): 23 | MONGODB_USER = os.environ.get('MONGODB_USER') 24 | MONGODB_PASSWORD = os.environ.get('MONGODB_PASSWORD') 25 | db.authenticate(MONGODB_USER, MONGODB_PASSWORD) 26 | mongodb = db.last_status() 27 | except Exception as error: 28 | mongodb = str(error) 29 | data = { 30 | 'github': github, 31 | 'mongodb': mongodb, 32 | } 33 | return jsonify(data) 34 | -------------------------------------------------------------------------------- /server/controllers/result.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask_restful import Resource, reqparse 3 | from config.database import result_col, DESCENDING,create_indexes 4 | import json 5 | 6 | 7 | class Leakage(Resource): 8 | def get(self): 9 | parser = reqparse.RequestParser() 10 | parser.add_argument('status', type=str, help='') 11 | parser.add_argument('tag', type=str, help='') 12 | parser.add_argument('language', type=str, help='') 13 | parser.add_argument('limit', type=int, default=10, help='') 14 | parser.add_argument('from', type=int, default=1, help='') 15 | args = parser.parse_args() 16 | status = json.loads(args.get('status')) 17 | filters = status 18 | create_indexes() 19 | if args.get('tag'): 20 | filters = dict({'tag': args.get('tag')}, **filters) 21 | if args.get('language'): 22 | filters = dict({'language': args.get('language')}, **filters) 23 | results = list( 24 | result_col.find(filters, {'code': 0, 'affect': 0}).sort('datetime', DESCENDING).limit( 25 | args.get('limit')).skip( 26 | args.get('limit') * (args.get('from') - 1))) 27 | total = result_col.count(filters) 28 | if total: 29 | msg = '共 {} 条记录'.format(total) 30 | else: 31 | msg = '暂无数据' 32 | data = { 33 | 'msg': msg, 34 | 'status': 200, 35 | 'result': results, 36 | 'total': total 37 | } 38 | return jsonify(data) 39 | 40 | def patch(self): 41 | parser = reqparse.RequestParser() 42 | parser.add_argument('id', type=str, help='') 43 | parser.add_argument('project', type=str, help='') 44 | parser.add_argument('ignore', type=int, choices=(0, 1), help='') 45 | parser.add_argument('security', type=int, choices=(0, 1), help='') 46 | parser.add_argument('desc', type=str, default='', help='') 47 | args = parser.parse_args() 48 | desc = args.get('desc') 49 | result_col.update({'_id': args.get('id')}, {'$set': {'security': int( 50 | args.get('security')), 'ignore': int(args.get('ignore')), 'desc': desc}}) 51 | if not args.get('security'): 52 | if not args.get('ignore'): 53 | result_col.update_many({'project': args.get('project')}, { 54 | '$set': {'security': 0, 'ignore': 0, 'desc': desc}}) 55 | if args.get('security') and args.get('ignore'): 56 | result_col.update_many({'project': args.get('project')}, { 57 | '$set': {'security': 1, 'ignore': 1, 'desc': desc}}) 58 | data = {'status': 201, 'msg': '处理成功', 'result': []} 59 | return jsonify(data) 60 | 61 | 62 | class LeakageCode(Resource): 63 | def get(self): 64 | parser = reqparse.RequestParser() 65 | parser.add_argument('id', type=str, help='leakage_id') 66 | args = parser.parse_args() 67 | leakage_id = args.get('id') 68 | result = result_col.find_one( 69 | {'_id': leakage_id}, {'_id': 0, 'code': 1, 'affect': 1}) 70 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 71 | return jsonify(data) 72 | 73 | 74 | class LeakageInfo(Resource): 75 | def get(self): 76 | parser = reqparse.RequestParser() 77 | parser.add_argument('id', type=str, help='leakage_id') 78 | args = parser.parse_args() 79 | leakage_id = args.get('id') 80 | result = result_col.find_one( 81 | {'_id': leakage_id}, {'_id': 0, 'code': 0}) 82 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 83 | return jsonify(data) 84 | -------------------------------------------------------------------------------- /server/controllers/setting.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | from flask_restful import Resource, reqparse, inputs 3 | from urllib.parse import urlparse 4 | from config.database import blacklist_col, result_col, query_col, notice_col, github_col, setting_col 5 | from utils.hash import md5 6 | from utils.date import timestamp 7 | from github import Github, GithubException, BadCredentialsException 8 | import signal 9 | import os 10 | import requests 11 | 12 | 13 | class System(Resource): 14 | def get(self): 15 | result = setting_col.find({'key': 'system'}, {'_id': 0}) 16 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 17 | return jsonify(data) 18 | 19 | def post(self): 20 | parser = reqparse.RequestParser() 21 | parser.add_argument('argument', type=str, default=request.host, help='setting argument') 22 | parser.add_argument('value', type=str, default=request.host, help='setting value') 23 | parser.add_argument('unique', type=inputs.boolean, default=False, help='setting unique') 24 | args = parser.parse_args() 25 | value = args.get('value') 26 | argument = args.get('argument') 27 | 28 | setting_col.update_many({'key': 'system', 'argument': argument}, 29 | {'$set': {'key': 'system', 'argument': argument, 'value': value}}, upsert=True) 30 | 31 | result = list(setting_col.find({}, {'_id': 0})) 32 | data = {'status': 201, 'msg': '设置成功', 'result': result} 33 | return jsonify(data) 34 | 35 | 36 | class Cron(Resource): 37 | def get(self): 38 | result = setting_col.find_one({'key': 'task'}, {'_id': 0}) 39 | if result: 40 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 41 | else: 42 | data = {'status': 400, 'msg': '请配置查询页数和周期', 'result': result} 43 | return jsonify(data) 44 | 45 | def post(self): 46 | parser = reqparse.RequestParser() 47 | parser.add_argument('page', type=int, default=1, help='') 48 | parser.add_argument('minute', type=int, default=10, help='') 49 | args = parser.parse_args() 50 | page = args.get('page') 51 | minute = args.get('minute') 52 | setting_col.update_many({'key': 'task'}, {'$set': {'key': 'task', 'page': page, 'minute': minute}}, upsert=True) 53 | try: 54 | os.kill(setting_col.find_one({'key': 'task'}).get('pid'), signal.SIGHUP) 55 | except ProcessLookupError: 56 | pass 57 | result = list(setting_col.find({}, {'_id': 0})) 58 | data = {'status': 201, 'msg': '设置成功', 'result': result} 59 | return jsonify(data) 60 | 61 | 62 | class GithubAccount(Resource): 63 | def get(self): 64 | result = list(github_col.find({}, {'_id': 0, 'password': 0})) 65 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 66 | return jsonify(data) 67 | 68 | def post(self): 69 | parser = reqparse.RequestParser() 70 | parser.add_argument('username', type=str, help='') 71 | parser.add_argument('password', type=str, help='') 72 | args = parser.parse_args() 73 | username = args.get('username') 74 | password = args.get('password') 75 | try: 76 | g = Github(username, password) 77 | github_col.save({'_id': md5(username), 'username': username, 'password': password, 78 | 'mask_password': password.replace(''.join(password[2:-2]), '****'), 'addat': timestamp(), 79 | 'rate_limit': int(g.get_rate_limit().search.limit), 80 | 'rate_remaining': int(g.get_rate_limit().search.remaining)}) 81 | result = list(github_col.find({}, {'_id': 0})) 82 | data = {'status': 201, 'msg': '添加成功', 'result': result} 83 | except BadCredentialsException: 84 | data = {'status': 401, 'msg': '认证失败,请检查账号是否可用', 'result': []} 85 | return jsonify(data) 86 | 87 | def delete(self): 88 | parser = reqparse.RequestParser() 89 | parser.add_argument('username', type=str, help='') 90 | args = parser.parse_args() 91 | github_col.delete_many({'username': args.get('username')}) 92 | result = list(github_col.find({}, {'_id': 0})) 93 | data = {'status': 404, 'msg': '删除成功', 'result': result} 94 | return jsonify(data) 95 | 96 | 97 | class Blacklist(Resource): 98 | def get(self): 99 | result = list(blacklist_col.find({}, {'_id': 0})) 100 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 101 | return jsonify(data) 102 | 103 | def post(self): 104 | parser = reqparse.RequestParser() 105 | parser.add_argument('text', type=str, help='') 106 | args = parser.parse_args() 107 | text = args.get('text') 108 | text = text.strip().replace(' ', '') 109 | blacklist_col.save({'_id': md5(text), 'text': text}) 110 | result = list(blacklist_col.find({}, {'_id': 0})) 111 | data = {'status': 201, 'msg': '添加成功', 'result': result} 112 | return jsonify(data) 113 | 114 | def delete(self): 115 | parser = reqparse.RequestParser() 116 | parser.add_argument('text', type=str, help='') 117 | args = parser.parse_args() 118 | blacklist_col.delete_many({'text': args.get('text')}) 119 | result = list(blacklist_col.find({}, {'_id': 0})) 120 | data = {'status': 404, 'msg': '删除成功', 'result': result} 121 | return jsonify(data) 122 | 123 | 124 | class SMTPServer(Resource): 125 | def get(self): 126 | result = setting_col.find_one({'key': 'mail'}, {'_id': 0}) 127 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 128 | return jsonify(data) 129 | 130 | def post(self): 131 | parser = reqparse.RequestParser() 132 | parser.add_argument('from', type=str, help='From (sender email)') 133 | parser.add_argument('host', type=str, help='SMTPServer Host') 134 | parser.add_argument('port', type=int, help='SMTPServer Port') 135 | parser.add_argument('tls', type=inputs.boolean, default=False, help='Force TLS') 136 | parser.add_argument('username', type=str, help='Username') 137 | parser.add_argument('password', type=str, help='Password') 138 | parser.add_argument('domain', type=str, help='System URL Host') 139 | parser.add_argument('enabled', type=inputs.boolean, default=False, help='Enabled Mail Notice') 140 | parser.add_argument('test', type=inputs.boolean, default=False, help='Test Mail Notice') 141 | args = parser.parse_args() 142 | __setting = args 143 | setting_col.update_many({'key': 'mail'}, {'$set': dict({'key': 'mail'}, **__setting)}, upsert=True) 144 | result = setting_col.find_one({'key': 'mail'}, {'_id': 0}) 145 | data = {'status': 201, 'msg': '设置成功', 'result': result} 146 | return jsonify(data) 147 | 148 | 149 | class WebHookNotice(Resource): 150 | """ 151 | WebHook 通知 152 | """ 153 | 154 | def get(self): 155 | result = list(setting_col.find({'webhook': {'$exists': True}}, {'_id': 0})) 156 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 157 | return jsonify(data) 158 | 159 | def delete(self): 160 | parser = reqparse.RequestParser() 161 | parser.add_argument('webhook', type=str, required=True, help='WebHook URL') 162 | args = parser.parse_args() 163 | delete_result = setting_col.delete_one({'webhook': args.get('webhook')}) 164 | if delete_result.deleted_count == 1: 165 | data = {'status': 200, 'msg': '删除成功', 'result': []} 166 | else: 167 | data = {'status': 404, 'msg': '删除失败', 'result': []} 168 | return jsonify(data) 169 | 170 | def post(self): 171 | parser = reqparse.RequestParser() 172 | parser.add_argument('webhook', type=str, required=True, help='WebHook URL') 173 | parser.add_argument('domain', type=str, help='System URL Host') 174 | parser.add_argument('enabled', type=inputs.boolean, default=False, help='Enabled Notice') 175 | parser.add_argument('test', type=inputs.boolean, default=False, help='Test Notice') 176 | args = parser.parse_args() 177 | if urlparse(args.get('webhook')).netloc not in ['oapi.dingtalk.com', 'qyapi.weixin.qq.com'] or urlparse( 178 | args.get('webhook')).scheme != 'https': 179 | data = {'status': 400, 'msg': '错误的 webhook 地址', 'result': []} 180 | return jsonify(data) 181 | if args.get('test'): 182 | if urlparse(args.get('webhook')).netloc == 'oapi.dingtalk.com': 183 | test_content = { 184 | "msgtype": "markdown", 185 | "markdown": {"title": "GitHub泄露", 186 | "text": '### 规则名称: [WebHook告警测试]({})'.format(args.get('domain')) 187 | }, 188 | "at": { 189 | "atMobiles": [ 190 | 191 | ], 192 | "isAtAll": False 193 | } 194 | } 195 | else: 196 | test_content = { 197 | "msgtype": "markdown", 198 | "markdown": { 199 | "content": '### 规则名称: [WebHook告警测试]({})'.format(args.get('domain')) 200 | } 201 | } 202 | 203 | response = requests.post( 204 | args.get('webhook'), 205 | json=test_content) 206 | if response.ok: 207 | if response.json().get('errmsg') == 'ok': 208 | data = {'status': 201, 'msg': '已发送,请前往钉钉/企业微信群查看', 'result': []} 209 | else: 210 | data = {'status': 400, 'msg': '发送失败,WebHook 响应: {}'.format(response.json().get('errmsg')), 211 | 'result': []} 212 | return jsonify(data) 213 | else: 214 | data = {'status': 400, 'msg': '发送失败,请检查服务器网络', 'result': []} 215 | return jsonify(data) 216 | del args['test'] 217 | setting_col.update_one({'webhook': args.get('webhook')}, {'$set': args}, upsert=True) 218 | result = setting_col.count({'webhook': args.get('webhook')}) 219 | if result > 0: 220 | data = {'status': 201, 'msg': '设置成功', 'result': result} 221 | else: 222 | data = {'status': 400, 'msg': '设置失败', 'result': result} 223 | return jsonify(data) 224 | 225 | 226 | class Notice(Resource): 227 | def get(self): 228 | result = list(notice_col.find({}, {'_id': 0})) 229 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 230 | return jsonify(data) 231 | 232 | def post(self): 233 | parser = reqparse.RequestParser() 234 | parser.add_argument('mail', type=str, help='') 235 | args = parser.parse_args() 236 | mail = args.get('mail') 237 | mail = mail.strip().replace(' ', '') 238 | notice_col.insert_one({'_id': md5(mail), 'mail': mail}) 239 | result = list(notice_col.find({}, {'_id': 0})) 240 | data = {'status': 201, 'msg': '添加成功', 'result': result} 241 | return jsonify(data) 242 | 243 | def delete(self): 244 | parser = reqparse.RequestParser() 245 | parser.add_argument('mail', type=str, help='') 246 | args = parser.parse_args() 247 | notice_col.delete_many({'mail': args.get('mail')}) 248 | result = list(notice_col.find({}, {'_id': 0})) 249 | data = {'status': 404, 'msg': '删除成功', 'result': result} 250 | return jsonify(data) 251 | 252 | 253 | class Rule(Resource): 254 | def get(self): 255 | result = list(query_col.find({}).sort('enabled', -1)) 256 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 257 | return jsonify(data) 258 | 259 | def post(self): 260 | parser = reqparse.RequestParser() 261 | parser.add_argument('keyword', type=str, help='') 262 | parser.add_argument('tag', type=str, help='') 263 | parser.add_argument('enabled', type=inputs.boolean, default=True, help='') 264 | args = parser.parse_args() 265 | if query_col.count({'tag': args.get('tag')}): 266 | query_col.update_one({'tag': args.get('tag')}, {'$set': args}) 267 | msg = '更新成功' 268 | else: 269 | new_query = args 270 | new_query['_id'] = md5(''.join([str(v) for v in new_query.values()])) 271 | query_col.insert_one(new_query) 272 | msg = '添加成功' 273 | result = list(query_col.find({}).sort('enabled', -1)) 274 | data = {'status': 200, 'msg': msg, 'result': result} 275 | return jsonify(data) 276 | 277 | def delete(self): 278 | parser = reqparse.RequestParser() 279 | parser.add_argument('_id', type=str, help='') 280 | parser.add_argument('tag', type=str, help='') 281 | args = parser.parse_args() 282 | query_col.delete_many({'_id': args.get('_id')}) 283 | result_col.delete_many({'tag': args.get('tag')}) 284 | result = list(query_col.find({}).sort('enabled', -1)) 285 | data = {'status': 404, 'msg': '删除成功', 'result': result} 286 | return jsonify(data) 287 | 288 | 289 | class Query(Resource): 290 | def get(self): 291 | result = list(query_col.find({}).sort('enabled', -1)) 292 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 293 | return jsonify(data) 294 | 295 | def post(self): 296 | parser = reqparse.RequestParser() 297 | parser.add_argument('keyword', type=str, help='') 298 | parser.add_argument('tag', type=str, help='') 299 | parser.add_argument('enabled', type=inputs.boolean, default=True, help='') 300 | args = parser.parse_args() 301 | if query_col.count({'tag': args.get('tag')}): 302 | query_col.update_one({'tag': args.get('tag')}, {'$set': args}) 303 | msg = '更新成功' 304 | else: 305 | new_query = args 306 | new_query['_id'] = md5(''.join([str(v) for v in new_query.values()])) 307 | query_col.insert_one(new_query) 308 | msg = '添加成功' 309 | result = list(query_col.find({}).sort('enabled', -1)) 310 | data = {'status': 200, 'msg': msg, 'result': result} 311 | return jsonify(data) 312 | 313 | def delete(self): 314 | parser = reqparse.RequestParser() 315 | parser.add_argument('_id', type=str, help='') 316 | parser.add_argument('tag', type=str, help='') 317 | args = parser.parse_args() 318 | query_col.delete_many({'_id': args.get('_id')}) 319 | result_col.delete_many({'tag': args.get('tag')}) 320 | result = list(query_col.find({}).sort('enabled', -1)) 321 | data = {'status': 404, 'msg': '删除成功', 'result': result} 322 | return jsonify(data) 323 | -------------------------------------------------------------------------------- /server/controllers/statistic.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask_restful import Resource, reqparse 3 | from config.database import blacklist_col, result_col, query_col, notice_col, github_col, setting_col 4 | 5 | from utils.date import today_start 6 | import psutil 7 | 8 | 9 | class Dashboard(Resource): 10 | def get(self): 11 | parser = reqparse.RequestParser() 12 | parser.add_argument('tag', type=str, help='') 13 | args = parser.parse_args() 14 | tag = args.get('tag') 15 | if tag: 16 | total = {'total': result_col.count({'tag': tag}), 17 | 'ignore': result_col.count({'tag': tag, 'security': 1}), 18 | 'risk': result_col.count( 19 | {'tag': tag, 'security': 0, "desc": {"$exists": True}})} 20 | today = { 21 | 'total': result_col.count({'tag': tag, 'timestamp': {'$gte': today_start()}}), 22 | 'ignore': result_col.count({'tag': tag, 'timestamp': {'$gte': today_start()}, 'security': 1}), 23 | 'risk': result_col.count( 24 | {'tag': tag, 'timestamp': {'$gte': today_start()}, 'security': 0, "desc": {"$exists": True}}), 25 | } 26 | else: 27 | total = {'total': result_col.count(), 28 | 'ignore': result_col.count({'security': 1}), 29 | 'risk': result_col.count( 30 | {'security': 0, "desc": {"$exists": True}})} 31 | today = { 32 | 'total': result_col.count({'timestamp': {'$gte': today_start()}}), 33 | 'ignore': result_col.count({'timestamp': {'$gte': today_start()}, 'security': 1}), 34 | 'risk': result_col.count( 35 | {'timestamp': {'$gte': today_start()}, 'security': 0, "desc": {"$exists": True}}), 36 | } 37 | if setting_col.count({'key': 'task'}): 38 | status = psutil.pid_exists(int(setting_col.find_one({'key': 'task'}).get('pid'))) 39 | last = setting_col.find_one({'key': 'task'}).get('last') 40 | else: 41 | status = False 42 | last = 0 43 | engine = { 44 | 'status': status, 45 | 'last': last, 46 | } 47 | result = { 48 | 'all': total, 49 | 'today': today, 50 | 'engine': engine 51 | } 52 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 53 | return jsonify(data) 54 | 55 | 56 | class Statistic(Resource): 57 | def get(self): 58 | parser = reqparse.RequestParser() 59 | parser.add_argument('by', type=str, default='tag', help='') 60 | parser.add_argument('tag', type=str, help='') 61 | args = parser.parse_args() 62 | by = args.get('by') 63 | tag = args.get('tag') 64 | if len(tag): 65 | filters = {'tag': tag, 'security': 0} 66 | else: 67 | filters = {'security': 0} 68 | pipeline = [ 69 | {'$match': filters}, 70 | {'$group': {'_id': '${}'.format(by), 'value': {'$sum': 1}}}, 71 | ] 72 | 73 | result = list(result_col.aggregate(pipeline)) 74 | if not len(result): 75 | pipeline = [ 76 | {'$group': {'_id': '${}'.format(by), 'value': {'$sum': 0}}}, 77 | ] 78 | result = list(result_col.aggregate(pipeline)) 79 | data = {'status': 200, 'msg': '获取信息成功', 'result': result} 80 | return jsonify(data) 81 | -------------------------------------------------------------------------------- /server/task.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import re 4 | import random 5 | import requests 6 | import smtplib 7 | import time 8 | from ipaddress import ip_address 9 | import tldextract 10 | from github import Github 11 | from huey import RedisHuey, crontab 12 | from pymongo import errors, DESCENDING, ASCENDING 13 | from config.database import result_col, query_col, blacklist_col, notice_col, github_col, setting_col, REDIS_HOST, \ 14 | REDIS_PORT 15 | from utils.date import timestamp 16 | from utils.log import logger 17 | from utils.notice import mail_notice 18 | 19 | huey = RedisHuey('hawkeye', host=REDIS_HOST, port=int(REDIS_PORT)) 20 | base_path = os.path.split(os.path.realpath(__file__))[0] 21 | extract = tldextract.TLDExtract(cache_file='{}/.tld_set'.format(base_path)) 22 | 23 | if setting_col.count({'key': 'task', 'minute': {'$exists': True}, 'page': {'$exists': True}}): 24 | minute = int(setting_col.find_one({'key': 'task'}).get('minute')) 25 | setting_col.update_one({'key': 'task'}, {'$set': {'key': 'task', 'pid': os.getpid(), 'last': timestamp()}}, 26 | upsert=True) 27 | 28 | else: 29 | minute = 10 30 | setting_col.update_one({'key': 'task'}, 31 | {'$set': {'key': 'task', 'pid': os.getpid(), 'minute': 10, 'page': 3, 'last': timestamp()}}, 32 | upsert=True) 33 | 34 | 35 | @huey.task() 36 | def search(query, page, g, github_username): 37 | mail_notice_list = [] 38 | webhook_notice_list = [] 39 | logger.info('开始抓取: tag is {} keyword is {}, page is {}'.format( 40 | query.get('tag'), query.get('keyword'), page + 1)) 41 | try: 42 | repos = g.search_code(query=query.get('keyword'), 43 | sort="indexed", order="desc") 44 | github_col.update_one({'username': github_username}, 45 | {'$set': {'rate_remaining': int(g.get_rate_limit().search.remaining)}}) 46 | 47 | except Exception as error: 48 | logger.critical(error) 49 | logger.critical("触发限制啦") 50 | return 51 | try: 52 | for repo in repos.get_page(page): 53 | setting_col.update_one({'key': 'task'}, {'$set': {'key': 'task', 'pid': os.getpid(), 'last': timestamp()}}, 54 | upsert=True) 55 | if not result_col.count({'_id': repo.sha}): 56 | try: 57 | code = str(repo.content).replace('\n', '') 58 | except: 59 | code = '' 60 | leakage = { 61 | 'link': repo.html_url, 62 | 'project': repo.repository.full_name, 63 | 'project_url': repo.repository.html_url, 64 | '_id': repo.sha, 65 | 'language': repo.repository.language, 66 | 'username': repo.repository.owner.login, 67 | 'avatar_url': repo.repository.owner.avatar_url, 68 | 'filepath': repo.path, 69 | 'filename': repo.name, 70 | 'security': 0, 71 | 'ignore': 0, 72 | 'tag': query.get('tag'), 73 | 'code': code, 74 | } 75 | try: 76 | leakage['affect'] = get_affect_assets(repo.decoded_content) 77 | except Exception as error: 78 | logger.critical('{} {}'.format(error, leakage.get('link'))) 79 | leakage['affect'] = [] 80 | if int(repo.raw_headers.get('x-ratelimit-remaining')) == 0: 81 | logger.critical('剩余使用次数: {}'.format( 82 | repo.raw_headers.get('x-ratelimit-remaining'))) 83 | return 84 | last_modified = datetime.datetime.strptime( 85 | repo.last_modified, '%a, %d %b %Y %H:%M:%S %Z') 86 | leakage['datetime'] = last_modified 87 | leakage['timestamp'] = last_modified.timestamp() 88 | in_blacklist = False 89 | for blacklist in blacklist_col.find({}): 90 | if blacklist.get('text').lower() in leakage.get('link').lower(): 91 | logger.warning('{} 包含白名单中的 {}'.format( 92 | leakage.get('link'), blacklist.get('text'))) 93 | in_blacklist = True 94 | if in_blacklist: 95 | continue 96 | if result_col.count({"project": leakage.get('project'), "ignore": 1}): 97 | continue 98 | if not result_col.count( 99 | {"project": leakage.get('project'), "filepath": leakage.get("filepath"), "security": 0}): 100 | mail_notice_list.append( 101 | '上传时间:{} 地址: {}/{}'.format(leakage.get('datetime'), leakage.get('link'), 102 | leakage.get('project'), leakage.get('filename'))) 103 | webhook_notice_list.append( 104 | '[{}/{}]({}) 上传于 {}'.format(leakage.get('project').split('.')[-1], 105 | leakage.get('filename'), leakage.get('link'), 106 | leakage.get('datetime'))) 107 | try: 108 | result_col.insert_one(leakage) 109 | logger.info(leakage.get('project')) 110 | except errors.DuplicateKeyError: 111 | logger.info('已存在') 112 | 113 | logger.info('抓取关键字:{} {}'.format(query.get('tag'), leakage.get('link'))) 114 | except Exception as error: 115 | if 'Not Found' not in error.data: 116 | g, github_username = new_github() 117 | search.schedule( 118 | args=(query, page, g, github_username), 119 | delay=huey.pending_count() + huey.scheduled_count()) 120 | logger.critical(error) 121 | logger.error('抓取: tag is {} keyword is {}, page is {} 失败'.format( 122 | query.get('tag'), query.get('keyword'), page + 1)) 123 | 124 | return 125 | logger.info('抓取: tag is {} keyword is {}, page is {} 成功'.format( 126 | query.get('tag'), query.get('keyword'), page + 1)) 127 | query_col.update_one({'tag': query.get('tag')}, 128 | {'$set': {'last': int(time.time()), 'status': 1, 'reason': '抓取第{}页成功'.format(page), 129 | 'api_total': repos.totalCount, 130 | 'found_total': result_col.count({'tag': query.get('tag')})}}) 131 | if setting_col.count({'key': 'mail', 'enabled': True}) and len(mail_notice_list): 132 | main_content = '

规则名称: {}


{}'.format(query.get('tag'), '
'.join(mail_notice_list)) 133 | send_mail(main_content) 134 | logger.info(len(webhook_notice_list)) 135 | webhook_notice(query.get('tag'), webhook_notice_list) 136 | 137 | 138 | @huey.task() 139 | def webhook_notice(tag, results): 140 | if len(results): 141 | for webhook_setting in setting_col.find({'webhook': {'$exists': True}, 'enabled': True}, 142 | {'domain': 1, 'webhook': 1, '_id': 0}): 143 | hostname = webhook_setting.get('domain') 144 | webhook = webhook_setting.get('webhook') 145 | if 'oapi.dingtalk.com' in webhook: 146 | content = { 147 | "msgtype": "markdown", 148 | "markdown": {"title": "GitHub泄露", 149 | "text": '#### [规则名称: {}]({}/?tag={})\n\n- {}'.format(tag, hostname, tag, 150 | '\n- '.join(results)) 151 | }, 152 | "at": { 153 | "atMobiles": [ 154 | 155 | ], 156 | "isAtAll": False 157 | } 158 | } 159 | else: 160 | 161 | content = { 162 | "msgtype": "markdown", 163 | "markdown": { 164 | "content": '#### [规则名称: {}]({}/?tag={})\n\n- {}'.format(tag, hostname, tag, 165 | '\n- '.join(results)) 166 | } 167 | } 168 | try: 169 | requests.post( 170 | webhook, 171 | json=content) 172 | except Exception as error: 173 | logger.error(error) 174 | 175 | 176 | def get_domain(target): 177 | result = extract(target) 178 | if bool(len(result.suffix)) and bool(len(result.domain)): 179 | return '{}.{}'.format(result.domain, result.suffix) 180 | else: 181 | return False 182 | 183 | 184 | def get_affect_assets(code): 185 | """ 186 | 187 | :param code: 188 | :return: 189 | """ 190 | code = str(code) 191 | affect = [] 192 | domain_pattern = '(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}' 193 | ip_pattern = "(\d+\.\d+\.\d+\.\d+)" 194 | email_pattern = "[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?" 195 | affect_assets = { 196 | 'domain': list(set(re.findall(domain_pattern, code))), 197 | 'email': list(set(re.findall(email_pattern, code))), 198 | 'ip': list(set(re.findall(ip_pattern, code))), 199 | } 200 | for assets in affect_assets.keys(): 201 | if len(affect_assets.get(assets)) > 100: 202 | affect_assets[assets] = [] 203 | continue 204 | for asset in affect_assets.get(assets): 205 | if assets == 'ip': 206 | if not is_ip(asset): 207 | continue 208 | if assets == 'domain': 209 | if not get_domain(asset): 210 | continue 211 | if assets == 'email': 212 | if not get_domain(asset.split('@')[-1]): 213 | continue 214 | affect.append({'type': assets, 'value': asset.replace( 215 | "'", "").replace('"', '').replace("`", "").lower()}) 216 | return affect 217 | 218 | 219 | def is_ip(ip): 220 | """ 221 | 222 | :param ip: 223 | :return: 224 | """ 225 | try: 226 | ip_address(ip) 227 | return True 228 | except ValueError: 229 | return False 230 | 231 | 232 | @huey.task() 233 | def send_mail(content): 234 | smtp_config = setting_col.find_one({'key': 'mail'}) 235 | receivers = [data.get('mail') for data in notice_col.find({})] 236 | try: 237 | if mail_notice(smtp_config, receivers, content): 238 | logger.info('邮件发送成功') 239 | else: 240 | logger.critical('Error: 无法发送邮件') 241 | 242 | except smtplib.SMTPException as error: 243 | logger.critical('Error: 无法发送邮件 {}'.format(error)) 244 | 245 | 246 | @huey.periodic_task(crontab(minute='*/2')) 247 | def update_rate_remain(): 248 | for account in github_col.find(): 249 | github_username = account.get('username') 250 | github_password = account.get('password') 251 | try: 252 | g = Github(github_username, github_password) 253 | github_col.update_one({'username': github_username}, 254 | {'$set': {'rate_remaining': int(g.get_rate_limit().search.remaining), 255 | 'rate_limit': int(g.get_rate_limit().search.limit)}}) 256 | except Exception as error: 257 | logger.error(error) 258 | 259 | 260 | def new_github(): 261 | if github_col.count({'rate_remaining': {'$gt': 5}}): 262 | pass 263 | else: 264 | logger.error('请配置github账号') 265 | return 266 | github_account = random.choice(list(github_col.find({"rate_limit": {"$gt": 5}}).sort('rate_remaining', DESCENDING))) 267 | github_username = github_account.get('username') 268 | github_password = github_account.get('password') 269 | g = Github(github_username, github_password) 270 | return g, github_username 271 | 272 | 273 | @huey.periodic_task(crontab(minute='*/{}'.format(minute))) 274 | def check(): 275 | setting_col.update_one({'key': 'task'}, {'$set': {'key': 'task', 'pid': os.getpid()}}, upsert=True) 276 | query_count = query_col.count({'enabled': True}) 277 | logger.info('需要处理的关键词总数: {}'.format(query_count)) 278 | if query_count: 279 | logger.info('需要处理的关键词总数: {}'.format(query_count)) 280 | else: 281 | logger.warning('请添加关键词') 282 | return 283 | if github_col.count({'rate_remaining': {'$gt': 5}}): 284 | pass 285 | else: 286 | logger.error('请配置github账号') 287 | return 288 | 289 | if setting_col.count({'key': 'task', 'page': {'$exists': True}}): 290 | setting_col.update_one({'key': 'task'}, {'$set': {'pid': os.getpid()}}) 291 | page = int(setting_col.find_one({'key': 'task'}).get('page')) 292 | for p in range(0, page): 293 | for query in query_col.find({'enabled': True}).sort('last', ASCENDING): 294 | github_account = random.choice( 295 | list(github_col.find({"rate_limit": {"$gt": 5}}).sort('rate_remaining', DESCENDING))) 296 | github_username = github_account.get('username') 297 | github_password = github_account.get('password') 298 | rate_remaining = github_account.get('rate_remaining') 299 | logger.info(github_username) 300 | logger.info(rate_remaining) 301 | g = Github(github_username, github_password, 302 | user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36') 303 | search.schedule(args=(query, p, g, github_username), 304 | delay=huey.pending_count() + huey.scheduled_count()) 305 | else: 306 | logger.error('请在页面上配置任务参数') 307 | -------------------------------------------------------------------------------- /server/utils/asset.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbug/Hawkeye/3ef48c27c47c86c8bfe013b4a266e08534c821d4/server/utils/asset.py -------------------------------------------------------------------------------- /server/utils/date.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def timestamp(): 5 | return int(time.time()) 6 | 7 | 8 | def today_start(): 9 | zero_timestamp = time.mktime( 10 | time.strptime(time.strftime('%Y-%m-%d 00:00:00', time.localtime(time.time())), '%Y-%m-%d %H:%M:%S')) 11 | 12 | return int(zero_timestamp) 13 | -------------------------------------------------------------------------------- /server/utils/hash.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def sha256(data): 5 | return hashlib.sha256(data.encode('utf-8')).hexdigest() 6 | 7 | 8 | def sha1(data): 9 | return hashlib.sha1(data.encode('utf-8')).hexdigest() 10 | 11 | 12 | def md5(data): 13 | m = hashlib.md5() 14 | m.update(data.encode('utf-8')) 15 | results = m.hexdigest() 16 | return results 17 | -------------------------------------------------------------------------------- /server/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import syslog 4 | 5 | FORMAT = '%(asctime)s [%(name)s] [%(levelname)s] [%(pathname)s:%(lineno)s %(funcName)s] %(message)s' 6 | logging.basicConfig(filename='Hawkeye.log', level=logging.INFO, format=FORMAT) 7 | logger = logging.getLogger() 8 | -------------------------------------------------------------------------------- /server/utils/notice.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | from email.header import Header 3 | from email.mime.text import MIMEText 4 | 5 | 6 | class SMTPServer(object): 7 | def __init__(self, smtp_config): 8 | self.host = smtp_config.get('host') 9 | self.port = int(smtp_config.get('port')) 10 | self.tls = smtp_config.get('tls') 11 | self.username = smtp_config.get('username') 12 | self.password = smtp_config.get('password') 13 | try: 14 | if self.tls: 15 | self.smtp = smtplib.SMTP(self.host, self.port, timeout=300) 16 | else: 17 | self.smtp = smtplib.SMTP_SSL(self.host, self.port, timeout=300) 18 | except Exception as error: 19 | print(error) 20 | 21 | def login(self): 22 | try: 23 | if self.tls: 24 | self.smtp.starttls() 25 | self.smtp.login(self.username, self.password) 26 | except Exception as error: 27 | print(error) 28 | 29 | def sendmail(self, receivers, message): 30 | """ 31 | 32 | :param receivers: 33 | :param message: 34 | :return: 35 | """ 36 | message = message.as_string() 37 | self.smtp.sendmail(self.username, receivers, message) 38 | 39 | 40 | def mail_notice(smtp_config, receivers, content): 41 | """ 42 | :param receivers: 43 | :param smtp_config: 44 | :param content: 45 | :return: 46 | """ 47 | 48 | message = MIMEText(content, _subtype='html', _charset='utf-8') 49 | message['From'] = Header('{}<{}>'.format(smtp_config.get('from'), smtp_config.get('username')), 'utf-8') 50 | message['To'] = Header(';'.join(receivers), 'utf-8') 51 | message['Subject'] = Header('[GitHub] 监控告警', 'utf-8') 52 | try: 53 | smtp = SMTPServer(smtp_config) 54 | print('login') 55 | smtp.login() 56 | smtp.sendmail(receivers, message) 57 | return True 58 | 59 | except smtplib.SMTPException: 60 | return False 61 | --------------------------------------------------------------------------------