├── .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 | [](https://github.com/0xbug/Hawkeye/issues)
4 | [](https://github.com/0xbug/Hawkeye/network)
5 | [](https://github.com/0xbug/Hawkeye/stargazers)
6 | [](https://www.python.org/)
7 | [](https://raw.githubusercontent.com/0xbug/Hawkeye/master/LICENSE)
8 |
9 | ## 简介
10 |
11 | > 监控github代码库,及时发现员工托管公司代码到GitHub行为并预警,降低代码泄露风险。
12 |
13 | ## 截图
14 |
15 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 首页
10 | {{$route.meta.name}}
11 |
12 |
13 |
14 |
15 |
16 |
17 | 2016-{{year}} © 0xbug
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/components/NavMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 | GitHub 监控平台
13 |
14 |
15 | 概览
16 |
17 |
18 | 配置
19 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
泄露总数(个)
9 |
{{trend.all.total}}
10 |
今日:{{trend.today.total}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
已确认(个)
21 |
{{trend.all.risk}}
22 |
今日:{{trend.today.risk}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
已忽略(个)
33 |
{{trend.all.ignore}}
34 |
今日:{{trend.today.ignore}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
查询引擎
45 |
{{trend.engine.status?'正常':'离线'}}
46 |
最近:{{trend.engine.last*1000|timeAgo}}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
98 |
99 |
118 |
--------------------------------------------------------------------------------
/client/src/views/result/Detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 | {{leakageInfo.project}}
15 | –
16 | {{leakageInfo.filename}}
17 |
18 |
19 |
27 |
28 |
29 | {{leakageInfo.language}}
30 |
31 |
32 | {{leakageInfo.datetime|dateFormat}}
33 |
34 |
35 |
36 | {{leakageInfo.tag}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 安全
53 | 涉密
54 |
55 |
56 |
57 |
58 |
59 | 忽略
60 | 监控
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 确认
70 | 快速排查
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
85 |
89 |
90 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
105 |
106 | {{code}}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
192 |
193 |
210 |
--------------------------------------------------------------------------------
/client/src/views/result/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
21 | {{item._id}} {{item.value}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
36 |
40 | {{item._id}} {{item.value}}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
49 |
50 | 不限
51 |
52 |
53 | 待审
54 |
55 |
56 | 确认
57 |
58 |
59 | 误报
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
282 |
283 |
312 |
--------------------------------------------------------------------------------
/client/src/views/setting/Blacklist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
17 |
18 | 添加
19 |
23 |
27 |
28 |
32 |
33 | 删除
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
102 |
103 |
115 |
--------------------------------------------------------------------------------
/client/src/views/setting/Github.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 | 添加
14 |
15 |
16 |
17 |
23 |
27 |
28 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 | 删除
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
120 |
--------------------------------------------------------------------------------
/client/src/views/setting/Notice.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
邮件
10 |
状态:{{smtp_server.enabled?'开启':'关闭'}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
24 |
26 |
27 |
钉钉
28 |
企业微信
29 |
{{webhook.webhook.split('=')[1].slice(0,8)}}
30 |
状态:{{webhook.enabled?'开启':'关闭'}}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
添加钉钉/微信告警
44 |
oapi.dingtalk.com qyapi.weixin.qq.com
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
64 |
65 |
66 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
84 |
85 |
86 |
90 |
91 |
92 |
93 |
94 |
98 |
99 |
101 |
102 | webhook 配置
103 | 目前支持 钉钉/企业微信
104 |
105 |
106 |
107 |
108 |
109 |
111 |
112 |
113 |
115 |
116 |
117 |
121 |
122 |
123 |
124 | 发送一条测试消息
126 |
127 |
128 |
129 |
130 |
134 |
135 |
136 |
143 |
144 | 添加
145 |
149 |
153 |
154 |
157 |
158 | 删除
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
364 |
376 |
377 |
--------------------------------------------------------------------------------
/client/src/views/setting/Rule.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 | 若存在相同名称,会覆盖已有值
13 |
14 |
15 |
16 |
17 |
18 | 熟悉
24 | GitHub 搜索语法可以提高监控效率: OR/AND/NOT
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
44 |
45 |
46 | 添加
47 |
48 |
49 |
50 |
51 |
52 | {{scope.row.tag}}
53 |
54 |
55 |
56 |
57 |
61 |
62 | {{scope.row.keyword}}
66 |
67 |
68 |
75 |
76 |
77 | {{ scope.row.last * 1000|timeAgo }}
78 |
79 |
80 |
86 |
87 |
93 |
94 |
100 |
101 |
106 |
107 |
108 |
109 |
110 |
114 |
115 |
116 |
117 |
118 | 编辑
123 |
124 | 删除
129 |
130 |
135 |
139 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
249 |
--------------------------------------------------------------------------------
/client/src/views/setting/Setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | 监控项
10 |
11 |
12 |
13 | 黑名单
14 |
15 |
16 |
17 | 告警
18 |
19 |
20 |
21 | 定时任务
22 |
23 |
24 |
25 | GitHub账号
26 |
27 |
28 |
29 |
30 |
57 |
86 |
--------------------------------------------------------------------------------
/client/src/views/setting/Task.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
18 |
19 |
20 | 确认
21 |
22 |
23 |
24 |
25 |
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 |
--------------------------------------------------------------------------------