├── .gitignore ├── LICENSE ├── README.md ├── admin ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── dist.tar ├── jest.config.js ├── jsconfig.json ├── mock │ ├── index.js │ ├── mock-server.js │ ├── node.js │ ├── remote-search.js │ ├── risk.js │ ├── table.js │ ├── task.js │ └── user.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── dashboard.js │ │ ├── ipassets.js │ │ ├── ipset.js │ │ ├── node.js │ │ ├── port.js │ │ ├── remote-search.js │ │ ├── risk.js │ │ ├── schedule.js │ │ ├── scheduler.js │ │ ├── settings.js │ │ ├── table.js │ │ ├── task.js │ │ ├── ticket.js │ │ ├── user.js │ │ └── web.js │ ├── assets │ │ ├── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ │ └── logo.png │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ ├── PanThumb │ │ │ └── index.vue │ │ └── SvgIcon │ │ │ └── index.vue │ ├── directive │ │ ├── clipboard │ │ │ ├── clipboard.js │ │ │ └── index.js │ │ ├── el-drag-dialog │ │ │ ├── drag.js │ │ │ └── index.js │ │ ├── el-table │ │ │ ├── adaptive.js │ │ │ └── index.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ ├── sticky.js │ │ └── waves │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── 404.svg │ │ │ ├── assets.svg │ │ │ ├── bug.svg │ │ │ ├── chart.svg │ │ │ ├── clipboard.svg │ │ │ ├── component.svg │ │ │ ├── dashboard.svg │ │ │ ├── documentation.svg │ │ │ ├── drag.svg │ │ │ ├── edit.svg │ │ │ ├── education.svg │ │ │ ├── email.svg │ │ │ ├── example.svg │ │ │ ├── excel.svg │ │ │ ├── exit-fullscreen.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── form.svg │ │ │ ├── fullscreen.svg │ │ │ ├── guide.svg │ │ │ ├── icon.svg │ │ │ ├── international.svg │ │ │ ├── ip.svg │ │ │ ├── language.svg │ │ │ ├── link.svg │ │ │ ├── list.svg │ │ │ ├── lock.svg │ │ │ ├── mdashboard.svg │ │ │ ├── message.svg │ │ │ ├── money.svg │ │ │ ├── nested.svg │ │ │ ├── password.svg │ │ │ ├── pdf.svg │ │ │ ├── people.svg │ │ │ ├── peoples.svg │ │ │ ├── qq.svg │ │ │ ├── risk.svg │ │ │ ├── search.svg │ │ │ ├── settings.svg │ │ │ ├── shopping.svg │ │ │ ├── size.svg │ │ │ ├── skill.svg │ │ │ ├── star.svg │ │ │ ├── tab.svg │ │ │ ├── table.svg │ │ │ ├── task.svg │ │ │ ├── theme.svg │ │ │ ├── tree-table.svg │ │ │ ├── tree.svg │ │ │ ├── user.svg │ │ │ ├── vuln.svg │ │ │ ├── wechat.svg │ │ │ └── zip.svg │ │ └── svgo.yml │ ├── layout │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ └── index.js │ │ ├── index.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── main.js │ ├── permission.js │ ├── router │ │ └── index.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ ├── settings.js │ │ │ └── user.js │ ├── styles │ │ ├── element-ui.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── auth.js │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── request.js │ │ ├── scroll-to.js │ │ └── validate.js │ └── views │ │ ├── 404.vue │ │ ├── assets │ │ ├── ipassets.vue │ │ └── ipset.vue │ │ ├── dashboard │ │ ├── admin │ │ │ ├── components │ │ │ │ ├── LineChart.vue │ │ │ │ ├── PanelGroup.vue │ │ │ │ ├── ReportStatusPie.vue │ │ │ │ ├── RiskBar.vue │ │ │ │ ├── UnreportedPie.vue │ │ │ │ └── mixins │ │ │ │ │ └── resize.js │ │ │ └── index.vue │ │ ├── editor │ │ │ └── index.vue │ │ └── index.vue │ │ ├── login │ │ └── index.vue │ │ ├── risk │ │ ├── port.vue │ │ └── web.vue │ │ ├── settings │ │ ├── node.vue │ │ └── system.vue │ │ └── task │ │ ├── components │ │ ├── job.vue │ │ ├── schedule.vue │ │ └── trigger.vue │ │ ├── schedule.vue │ │ ├── scheduler.vue │ │ └── worker.vue ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── components │ │ ├── Breadcrumb.spec.js │ │ ├── Hamburger.spec.js │ │ └── SvgIcon.spec.js │ │ └── utils │ │ ├── formatTime.spec.js │ │ ├── parseTime.spec.js │ │ └── validate.spec.js └── vue.config.js ├── pic ├── AnSecNote.jpg └── Wechat_PMon.JPG ├── server ├── Pipfile ├── Pipfile.lock ├── README.md ├── doc │ └── how_to_make_APScheduler_single.md ├── pmon │ ├── __init__.py │ ├── apscheduler │ │ ├── __init__.py │ │ └── api.py │ ├── backend │ │ ├── __init__.py │ │ ├── port.py │ │ ├── task.py │ │ └── web.py │ ├── initialize.py │ ├── jobs │ │ ├── __init__.py │ │ ├── count.py │ │ └── task.py │ ├── mongo.py │ ├── nodes │ │ └── __init__.py │ ├── resource │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dashboard.py │ │ ├── ipassets.py │ │ ├── ipset.py │ │ ├── login.py │ │ ├── node.py │ │ ├── port.py │ │ ├── schedule.py │ │ ├── settings.py │ │ ├── task.py │ │ ├── ticket.py │ │ └── web.py │ ├── settings.py │ └── work.py ├── scheduler.lock └── wsgi.py └── work ├── .gitignore ├── Pipfile ├── README.md ├── doc ├── logger_config.md ├── redis_demo.md ├── scan.md └── single_process_demo.md ├── log └── .gitkeep └── work ├── __init__.py ├── celery.py ├── lib ├── __init__.py ├── masscan.py ├── sockets.py └── webcheck.py ├── port.py ├── task.py └── web.py /.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 | build/ 12 | develop-eggs/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | #lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | .flaskenv 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypyf 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | bin/ 131 | 132 | work/.idea/ 133 | server/.idea/ 134 | 135 | *.rdb 136 | 137 | 138 | .DS_Store 139 | admin/node_modules/ 140 | npm-debug.log* 141 | yarn-debug.log* 142 | yarn-error.log* 143 | package-lock.json 144 | tests/**/coverage/ 145 | 146 | # Editor directories and files 147 | .idea 148 | .vscode 149 | *.suo 150 | *.ntvs* 151 | *.njsproj 152 | *.sln 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PMon 边界网络端口持续监控 2 | 3 | ## 1. 概述 4 | PMon是为了持续监控网络端口而设计,主要适用外网边界端口持续监控。可以在有效资源内,实现端口发现、监控、动态更新等基础安全风险动态管理能力。 5 | 关于系统设计思路理念,可以关注微信公众号"安小记",查看公众号文章:"如何做好边界端口持续监控". 6 | 7 | 8 | ## 2. 简要说明 9 | 10 | 整个监控系统分为三大组件,分别是: 11 | - server: 后台,负责任务下发调度、后台API 12 | - work:执行扫描任务Agent,负责执行具体扫描工作,并将结果返回给`server` 13 | - admin:前端展示,使用 `vue-element-admin`前端框架 14 | 15 | 16 | ## 3. 组件列表 17 | 18 | - 数据库: 19 | - Mongodb: 系统主要数据库; 20 | 21 | - 后台服务 - `server`: 22 | 23 | - Flask:Python轻量级Web开发框架,提供基础Web功能; 24 | 25 | - flask-restful: Flask的Restful Api插件,实现后台Api与节点之间的通讯; 26 | 27 | - APScheduler:Python任务调度模块,实现后台任务管理、节点任务定时下发功能; 28 | 29 | - gunicorn:Python WSGI服务,用于部署Flask应用; 30 | - Supervisor: 后台进程管理服务,用于保证后台服务持续运行; 31 | 32 | - 任务节点 - `work`: 33 | 34 | - Celery: 分布式任务队列,实现节点任务执行; 35 | 36 | - Flower: Celery组件,实现Celery节点监控以及任务下发和监控的Web Api; 37 | - Redis: Key-Value数据库,Celery任务管理存储调度; 38 | 39 | - 前端框架 - `admin`: 40 | - vue-element-admin: 基于Vue和element的前端开发框架,实现系统可视化展示; 41 | 42 | 43 | ## 4. 部署 44 | 45 | 由于各部分是相互独立的,请进各自目录,完成组件安装,详见各目录下`README.md`; 46 | 47 | 另外,`admin`目录是前端框架,是基于vue-element-admin二次开发而成,这里就直接使用README的文档。 48 | 感谢[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)提供那么简单易用的框架。 49 | 50 | **注意: 因系统环境差异大,请根据实际情况部署。个人只是安全运营人员,非专业开发,出现问题可能需要使用人员有一定看代码和解决问题能力。** 51 | 52 | 53 | 54 | ## 5. 更多 55 | 56 | 由于是个人业余开发,精力有效,程序逻辑较为复杂,建议使用人员具备一定的python编程经验,最好能够熟练上述各开源组件,如有疑问,我尽力解答哈。 57 | 58 | 也是第一次尝试开源自己做的东西,文档什么的也没准备的太好,还请各位大佬见谅! 59 | 60 | 61 | 如有建议、指导,欢迎关注公众号`安小记`,或加微信,期待和各位大佬交流; 62 | 63 |  64 | 65 | 另外,为了更方便交流,新建了该项目的微信群,欢迎各位扫描加入: 66 | 67 | 68 | 69 | 70 | ## 6. 鸣谢 71 | 72 | 2020.08.11 感谢 [yolylight](https://github.com/yolylight) 完善部署文档和修改小BUG; 73 | -------------------------------------------------------------------------------- /admin/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /admin/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/dev-api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /admin/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /admin/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /admin/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /admin/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /admin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /admin/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /admin/dist.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/admin/dist.tar -------------------------------------------------------------------------------- /admin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /admin/mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { param2Obj } from '../src/utils' 3 | 4 | import user from './user' 5 | import table from './table' 6 | import search from './remote-search' 7 | import task from './task' 8 | import node from './node' 9 | import risk from './risk' 10 | 11 | const mocks = [ 12 | ...user, 13 | ...table, 14 | ...search, 15 | ...task, 16 | ...node, 17 | ...risk 18 | ] 19 | 20 | // for front mock 21 | // please use it cautiously, it will redefine XMLHttpRequest, 22 | // which will cause many of your third-party libraries to be invalidated(like progress event). 23 | export function mockXHR() { 24 | // mock patch 25 | // https://github.com/nuysoft/Mock/issues/300 26 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 27 | Mock.XHR.prototype.send = function() { 28 | if (this.custom.xhr) { 29 | this.custom.xhr.withCredentials = this.withCredentials || false 30 | 31 | if (this.responseType) { 32 | this.custom.xhr.responseType = this.responseType 33 | } 34 | } 35 | this.proxy_send(...arguments) 36 | } 37 | 38 | function XHR2ExpressReqWrap(respond) { 39 | return function(options) { 40 | let result = null 41 | if (respond instanceof Function) { 42 | const { body, type, url } = options 43 | // https://expressjs.com/en/4x/api.html#req 44 | result = respond({ 45 | method: type, 46 | body: JSON.parse(body), 47 | query: param2Obj(url) 48 | }) 49 | } else { 50 | result = respond 51 | } 52 | return Mock.mock(result) 53 | } 54 | } 55 | 56 | for (const i of mocks) { 57 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 58 | } 59 | } 60 | 61 | // for mock server 62 | const responseFake = (url, type, respond) => { 63 | return { 64 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 65 | type: type || 'get', 66 | response(req, res) { 67 | console.log('request invoke:' + req.path) 68 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 69 | } 70 | } 71 | } 72 | 73 | export default mocks.map(route => { 74 | return responseFake(route.url, route.type, route.response) 75 | }) 76 | -------------------------------------------------------------------------------- /admin/mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | 6 | const mockDir = path.join(process.cwd(), 'mock') 7 | 8 | function registerRoutes(app) { 9 | let mockLastIndex 10 | const { default: mocks } = require('./index.js') 11 | for (const mock of mocks) { 12 | app[mock.type](mock.url, mock.response) 13 | mockLastIndex = app._router.stack.length 14 | } 15 | const mockRoutesLength = Object.keys(mocks).length 16 | return { 17 | mockRoutesLength: mockRoutesLength, 18 | mockStartIndex: mockLastIndex - mockRoutesLength 19 | } 20 | } 21 | 22 | function unregisterRoutes() { 23 | Object.keys(require.cache).forEach(i => { 24 | if (i.includes(mockDir)) { 25 | delete require.cache[require.resolve(i)] 26 | } 27 | }) 28 | } 29 | 30 | module.exports = app => { 31 | // es6 polyfill 32 | require('@babel/register') 33 | 34 | // parse app.body 35 | // https://expressjs.com/en/4x/api.html#req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })) 40 | 41 | const mockRoutes = registerRoutes(app) 42 | var mockRoutesLength = mockRoutes.mockRoutesLength 43 | var mockStartIndex = mockRoutes.mockStartIndex 44 | 45 | // watch files, hot reload mock server 46 | chokidar.watch(mockDir, { 47 | ignored: /mock-server/, 48 | ignoreInitial: true 49 | }).on('all', (event, path) => { 50 | if (event === 'change' || event === 'add') { 51 | try { 52 | // remove mock routes stack 53 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 54 | 55 | // clear routes cache 56 | unregisterRoutes() 57 | 58 | const mockRoutes = registerRoutes(app) 59 | mockRoutesLength = mockRoutes.mockRoutesLength 60 | mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 63 | } catch (error) { 64 | console.log(chalk.redBright(error)) 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /admin/mock/node.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | name: '@name()', 7 | 'status|1': ['enable', 'disable'], 8 | ip: '@ip()', 9 | 'scope|1-1': ['192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12'], 10 | remark: '@name()' 11 | }] 12 | }) 13 | 14 | export default [ 15 | { 16 | url: '/monitor-admin/node/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | }, 29 | { 30 | url: '/monitor-admin/node/create', 31 | type: 'post', 32 | response: _ => { 33 | return { 34 | code: 20000, 35 | data: 'success' 36 | } 37 | } 38 | }, 39 | 40 | { 41 | url: '/monitor-admin/node/update', 42 | type: 'post', 43 | response: _ => { 44 | return { 45 | code: 20000, 46 | data: 'success' 47 | } 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /admin/mock/remote-search.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const NameList = [] 4 | const count = 100 5 | 6 | for (let i = 0; i < count; i++) { 7 | NameList.push(Mock.mock({ 8 | name: '@first' 9 | })) 10 | } 11 | NameList.push({ name: 'mock-Pan' }) 12 | 13 | export default [ 14 | // username search 15 | { 16 | url: '/monitor-admin/search/user', 17 | type: 'get', 18 | response: config => { 19 | const { name } = config.query 20 | const mockNameList = NameList.filter(item => { 21 | const lowerCaseName = item.name.toLowerCase() 22 | return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0) 23 | }) 24 | return { 25 | code: 20000, 26 | data: { items: mockNameList } 27 | } 28 | } 29 | }, 30 | 31 | // transaction list 32 | { 33 | url: '/monitor-admin/transaction/list', 34 | type: 'get', 35 | response: _ => { 36 | return { 37 | code: 20000, 38 | data: { 39 | total: 20, 40 | 'items|20': [{ 41 | order_no: '@guid()', 42 | timestamp: +Mock.Random.date('T'), 43 | username: '@name()', 44 | price: '@float(1000, 15000, 0, 2)', 45 | 'status|1': ['success', 'pending'] 46 | }] 47 | } 48 | } 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /admin/mock/risk.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const portList = [ 4 | { ip: '', host: '', port: '80', owner: 'Anna.Wang', business: 'BU-A', discover_time: 1464117240475, ticket_info: { id: '1234567890', status: 'reported', owner: 'william.chen', remark: 'xxx', timestamp: 1464117240475, type: 'ECP' }} 5 | ] 6 | export default [ 7 | 8 | { 9 | url: '/monitor-admin/risk/port/list', 10 | type: 'get', 11 | response: _ => { 12 | return { 13 | code: 20000, 14 | data: { 15 | total: 20, 16 | 'items|20': [{ 17 | ip: '@ip()', 18 | host: '@ip()', 19 | port: '@integer(1, 65535)', 20 | owner: '@name()', 21 | sys_code: '@guid()', 22 | business: '@name()', 23 | discover_time: Mock.Random.date('T'), 24 | update_time: Mock.Random.date('T'), 25 | ticket_info: { 26 | 'status|1': ['reported', 'unreported'], 27 | id: '@id()', 28 | owner: '@name()', 29 | remark: Mock.Random.csentence(20, 30), 30 | timestamp: Mock.Random.date('T'), 31 | 'source|1': ['ByHandle', 'ECP', 'ITSM', 'OTHER'] 32 | } 33 | }] 34 | } 35 | } 36 | } 37 | }, 38 | 39 | { 40 | url: '/monitor-admin/risk/port/ticket/update', 41 | type: 'post', 42 | response: _ => { 43 | return { 44 | code: 20000, 45 | data: 'success' 46 | } 47 | } 48 | }, 49 | 50 | { 51 | url: '/monitor-admin/risk/service', 52 | type: 'get', 53 | response: _ => { 54 | return { 55 | code: 20000, 56 | data: { 57 | total: 20, 58 | 'items|20': [{ 59 | ip: '@ip()', 60 | host: '@ip()', 61 | service: '@integer(1, 65535)', 62 | owner: '@name()', 63 | sys_code: '@guid()', 64 | discover_time: +Mock.Random.date('T'), 65 | update_time: +Mock.Random.date('T'), 66 | ticket_info: { 67 | ticket_id: '@id()', 68 | owner: '@name()', 69 | remark: Mock.Random.csentence(20, 30) 70 | } 71 | }] 72 | } 73 | } 74 | } 75 | }, 76 | { 77 | url: '/monitor-admin/risk/vuln', 78 | type: 'get', 79 | response: _ => { 80 | return { 81 | code: 20000, 82 | data: { 83 | total: 20, 84 | 'items|20': [{ 85 | ip: '@ip()', 86 | host: '@ip()', 87 | port: '@integer(1, 65535)', 88 | owner: '@name()', 89 | sys_code: '@guid()', 90 | discover_time: +Mock.Random.date('T'), 91 | update_time: +Mock.Random.date('T'), 92 | ticket_info: { 93 | ticket_id: '@id()', 94 | owner: '@name()', 95 | remark: Mock.Random.csentence(20, 30) 96 | } 97 | }] 98 | } 99 | } 100 | } 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /admin/mock/table.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | export default [ 15 | { 16 | url: '/monitor-admin/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /admin/mock/task.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const NameList = [] 4 | const count = 100 5 | 6 | for (let i = 0; i < count; i++) { 7 | NameList.push(Mock.mock({ 8 | name: '@first' 9 | })) 10 | } 11 | NameList.push({ name: 'mock-Pan' }) 12 | 13 | export default [ 14 | 15 | // task list 16 | { 17 | url: '/monitor-admin/task/list', 18 | type: 'get', 19 | response: _ => { 20 | return { 21 | code: 20000, 22 | data: { 23 | total: 20, 24 | 'items|20': [{ 25 | task_id: '@guid()', 26 | task_name: 'task' + '@name()', 27 | // start_time: +Mock.Random.date('T'), 28 | // end_time: +Mock.Random.date('T'), 29 | start_time: '@datetime', 30 | end_time: '@datetime', 31 | node_name: '@name()', 32 | // price: '@float(1000, 15000, 0, 2)', 33 | 'status|1': ['SUCCESS', 'STARTED', 'FAILED', 'PENDING'] 34 | }] 35 | } 36 | } 37 | } 38 | }, 39 | 40 | { 41 | url: '/monitor-admin/task/result', 42 | type: 'get', 43 | response: _ => { 44 | return { 45 | code: 20000, 46 | data: { 47 | resultData: [ 48 | { ip: '@ip()', result: '1024' }, 49 | { ip: '@ip()', result: '8080' }, 50 | { ip: '@ip()', result: '22' }, 51 | { ip: '@ip()', result: '5566' } 52 | ] 53 | } 54 | } 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /admin/mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | export default [ 27 | // user login 28 | { 29 | url: '/monitor-admin/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/monitor-admin/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/monitor-admin/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pmon-admin", 3 | "version": "0.0.1", 4 | "description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features", 5 | "author": "Pan ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build:prod": "vue-cli-service build", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "preview": "node build/index.js --preview", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit", 15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 16 | "new": "plop" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged" 21 | } 22 | }, 23 | "lint-staged": { 24 | "src/**/*.{js,vue}": [ 25 | "eslint --fix", 26 | "git add" 27 | ] 28 | }, 29 | "keywords": [ 30 | "vue", 31 | "admin", 32 | "dashboard", 33 | "element-ui", 34 | "boilerplate", 35 | "admin-template", 36 | "management-system" 37 | ], 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/chiww/PMon.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/chiww/PMon/issues" 44 | }, 45 | "dependencies": { 46 | "axios": "0.18.1", 47 | "clipboard": "2.0.4", 48 | "codemirror": "5.45.0", 49 | "colorette": "^1.2.1", 50 | "driver.js": "0.9.5", 51 | "dropzone": "5.5.1", 52 | "echarts": "4.2.1", 53 | "element-ui": "2.13.0", 54 | "file-saver": "2.0.1", 55 | "fuse.js": "3.4.4", 56 | "js-cookie": "2.2.0", 57 | "jsonlint": "1.6.3", 58 | "jszip": "3.2.1", 59 | "normalize.css": "7.0.0", 60 | "nprogress": "^0.2.0", 61 | "path-to-regexp": "2.4.0", 62 | "screenfull": "4.2.0", 63 | "showdown": "^1.9.1", 64 | "sortablejs": "1.8.4", 65 | "tui-editor": "1.3.3", 66 | "vue": "2.6.10", 67 | "vue-count-to": "1.0.13", 68 | "vue-router": "3.0.2", 69 | "vue-splitpane": "1.0.4", 70 | "vuedraggable": "2.20.0", 71 | "vuex": "3.1.0", 72 | "xlsx": "0.14.1" 73 | }, 74 | "devDependencies": { 75 | "@babel/core": "7.0.0", 76 | "@babel/register": "7.0.0", 77 | "@vue/cli-plugin-babel": "3.5.3", 78 | "@vue/cli-plugin-eslint": "^3.9.1", 79 | "@vue/cli-plugin-unit-jest": "3.5.3", 80 | "@vue/cli-service": "3.5.3", 81 | "@vue/test-utils": "1.0.0-beta.29", 82 | "autoprefixer": "^9.5.1", 83 | "babel-core": "7.0.0-bridge.0", 84 | "babel-eslint": "10.0.1", 85 | "babel-jest": "23.6.0", 86 | "chalk": "2.4.2", 87 | "chokidar": "2.1.5", 88 | "connect": "3.6.6", 89 | "eslint": "5.15.3", 90 | "eslint-plugin-vue": "5.2.2", 91 | "html-webpack-plugin": "3.2.0", 92 | "husky": "1.3.1", 93 | "lint-staged": "8.1.5", 94 | "mockjs": "1.0.1-beta3", 95 | "node-sass": "^4.9.0", 96 | "plop": "2.3.0", 97 | "runjs": "^4.3.2", 98 | "sass-loader": "^7.1.0", 99 | "script-ext-html-webpack-plugin": "2.1.3", 100 | "script-loader": "0.7.2", 101 | "serve-static": "^1.13.2", 102 | "svg-sprite-loader": "4.1.3", 103 | "svgo": "1.2.0", 104 | "vue-template-compiler": "2.6.10" 105 | }, 106 | "engines": { 107 | "node": ">=8.9", 108 | "npm": ">= 3.0.0" 109 | }, 110 | "browserslist": [ 111 | "> 1%", 112 | "last 2 versions" 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/admin/public/favicon.ico -------------------------------------------------------------------------------- /admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 12 | We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue. 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /admin/src/api/dashboard.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchDashboard(params) { 4 | return request({ 5 | url: '/admin/dashboard', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function fetchPanelCount(params) { 12 | return request({ 13 | url: '/admin/panel_count', 14 | method: 'get', 15 | params 16 | }) 17 | } 18 | 19 | export function fetchHisLine(params) { 20 | return request({ 21 | url: '/admin/his_line', 22 | method: 'get', 23 | params 24 | }) 25 | } 26 | 27 | export function fetchRiskBar(params) { 28 | return request({ 29 | url: '/admin/risk_bar', 30 | method: 'get', 31 | params 32 | }) 33 | } 34 | 35 | export function fetchReportPie(params) { 36 | return request({ 37 | url: '/admin/report_pie', 38 | method: 'get', 39 | params 40 | }) 41 | } 42 | 43 | export function fetchUnreportedPie(params) { 44 | return request({ 45 | url: '/admin/unreported_pie', 46 | method: 'get', 47 | params 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /admin/src/api/ipassets.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchIPAssets(params) { 4 | return request({ 5 | url: '/admin/ipassets', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function modifyIPAssets(data) { 12 | return request({ 13 | url: '/admin/ipassets', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function deleteIPAssets(data) { 20 | return request({ 21 | url: '/admin/ipassets', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /admin/src/api/ipset.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchIPSet(params) { 4 | return request({ 5 | url: '/admin/ipset', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function modifyIPSet(data) { 12 | return request({ 13 | url: '/admin/ipset', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/api/node.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchNode(params) { 4 | return request({ 5 | url: '/admin/node', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function modifyNode(data) { 12 | return request({ 13 | url: '/admin/node', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function deleteNode(data) { 20 | return request({ 21 | url: '/admin/node', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /admin/src/api/port.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchPortList(query) { 4 | return request({ 5 | url: '/admin/ports', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /admin/src/api/remote-search.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function searchUser(name) { 4 | return request({ 5 | url: '/admin/search/user', 6 | method: 'get', 7 | params: { name } 8 | }) 9 | } 10 | 11 | export function transactionList(query) { 12 | return request({ 13 | url: '/admin/transaction/list', 14 | method: 'get', 15 | params: query 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/api/risk.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchPortList(query) { 4 | return request({ 5 | url: '/admin/risk/port/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function updatePortTicket(data) { 12 | return request({ 13 | url: '/admin/risk/port/ticket/update', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function fetchService(query) { 20 | return request({ 21 | url: '/admin/risk/service', 22 | method: 'get', 23 | params: query 24 | }) 25 | } 26 | 27 | export function fetchVuln(query) { 28 | return request({ 29 | url: '/admin/risk/vuln', 30 | method: 'get', 31 | params: query 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /admin/src/api/schedule.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchTrigger(params) { 4 | return request({ 5 | url: '/admin/schedule/trigger', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function modifyTrigger(data) { 12 | return request({ 13 | url: '/admin/schedule/trigger', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function deleteTrigger(data) { 20 | return request({ 21 | url: '/admin/schedule/trigger', 22 | method: 'delete', 23 | data 24 | }) 25 | } 26 | 27 | export function fetchJob(params) { 28 | return request({ 29 | url: '/admin/schedule/job', 30 | method: 'get', 31 | params 32 | }) 33 | } 34 | 35 | export function modifyJob(data) { 36 | return request({ 37 | url: '/admin/schedule/job', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | 43 | export function deleteJob(data) { 44 | return request({ 45 | url: '/admin/schedule/job', 46 | method: 'delete', 47 | data 48 | }) 49 | } 50 | 51 | export function fetchTask(params) { 52 | return request({ 53 | url: '/admin/schedule/task', 54 | method: 'get', 55 | params 56 | }) 57 | } 58 | 59 | export function modifyTask(data) { 60 | return request({ 61 | url: '/admin/schedule/task', 62 | method: 'post', 63 | data 64 | }) 65 | } 66 | 67 | export function deleteTask(data) { 68 | return request({ 69 | url: '/admin/schedule/task', 70 | method: 'delete', 71 | data 72 | }) 73 | } 74 | 75 | export function fetchSchedule(params) { 76 | return request({ 77 | url: '/admin/schedule/schedule', 78 | method: 'get', 79 | params 80 | }) 81 | } 82 | 83 | export function modifySchedule(data) { 84 | return request({ 85 | url: '/admin/schedule/schedule', 86 | method: 'post', 87 | data 88 | }) 89 | } 90 | 91 | export function deleteSchedule(data) { 92 | return request({ 93 | url: '/admin/schedule/schedule', 94 | method: 'delete', 95 | data 96 | }) 97 | } 98 | 99 | export function fetchNode(params) { 100 | return request({ 101 | url: '/admin/schedule/nodes', 102 | method: 'get', 103 | params 104 | }) 105 | } 106 | 107 | export function modifyNode(data) { 108 | return request({ 109 | url: '/admin/schedule/nodes', 110 | method: 'post', 111 | data 112 | }) 113 | } 114 | 115 | export function deleteNode(data) { 116 | return request({ 117 | url: '/admin/schedule/nodes', 118 | method: 'delete', 119 | data 120 | }) 121 | } 122 | 123 | -------------------------------------------------------------------------------- /admin/src/api/settings.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchSettings(params) { 4 | return request({ 5 | url: '/admin/settings', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function updateSettings(data) { 12 | return request({ 13 | url: '/admin/settings', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/admin/table/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /admin/src/api/task.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/admin/tasks', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchResult(query) { 12 | return request({ 13 | url: '/admin/task/result', 14 | method: 'get', 15 | params: query 16 | }) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /admin/src/api/ticket.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function updateTicket(data) { 4 | return request({ 5 | url: '/admin/ticket', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /admin/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/admin/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/admin/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/admin/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /admin/src/api/web.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchWebList(query) { 4 | return request({ 5 | url: '/admin/web', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function updateWebTicket(data) { 12 | return request({ 13 | url: '/admin/ticket', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/admin/src/assets/404_images/404.png -------------------------------------------------------------------------------- /admin/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/admin/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/admin/src/assets/logo.png -------------------------------------------------------------------------------- /admin/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ item.meta.title }} 6 | {{ item.meta.title }} 7 | 8 | 9 | 10 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /admin/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /admin/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /admin/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /admin/src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /admin/src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /admin/src/directive/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 4 | const dragDom = el.querySelector('.el-dialog') 5 | dialogHeaderEl.style.cssText += ';cursor:move;' 6 | dragDom.style.cssText += ';top:0px;' 7 | 8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 9 | const getStyle = (function() { 10 | if (window.document.currentStyle) { 11 | return (dom, attr) => dom.currentStyle[attr] 12 | } else { 13 | return (dom, attr) => getComputedStyle(dom, false)[attr] 14 | } 15 | })() 16 | 17 | dialogHeaderEl.onmousedown = (e) => { 18 | // 鼠标按下,计算当前元素距离可视区的距离 19 | const disX = e.clientX - dialogHeaderEl.offsetLeft 20 | const disY = e.clientY - dialogHeaderEl.offsetTop 21 | 22 | const dragDomWidth = dragDom.offsetWidth 23 | const dragDomHeight = dragDom.offsetHeight 24 | 25 | const screenWidth = document.body.clientWidth 26 | const screenHeight = document.body.clientHeight 27 | 28 | const minDragDomLeft = dragDom.offsetLeft 29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 30 | 31 | const minDragDomTop = dragDom.offsetTop 32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 33 | 34 | // 获取到的值带px 正则匹配替换 35 | let styL = getStyle(dragDom, 'left') 36 | let styT = getStyle(dragDom, 'top') 37 | 38 | if (styL.includes('%')) { 39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 41 | } else { 42 | styL = +styL.replace(/\px/g, '') 43 | styT = +styT.replace(/\px/g, '') 44 | } 45 | 46 | document.onmousemove = function(e) { 47 | // 通过事件委托,计算移动的距离 48 | let left = e.clientX - disX 49 | let top = e.clientY - disY 50 | 51 | // 边界处理 52 | if (-(left) > minDragDomLeft) { 53 | left = -minDragDomLeft 54 | } else if (left > maxDragDomLeft) { 55 | left = maxDragDomLeft 56 | } 57 | 58 | if (-(top) > minDragDomTop) { 59 | top = -minDragDomTop 60 | } else if (top > maxDragDomTop) { 61 | top = maxDragDomTop 62 | } 63 | 64 | // 移动当前元素 65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 66 | 67 | // emit onDrag event 68 | vnode.child.$emit('dragDialog') 69 | } 70 | 71 | document.onmouseup = function(e) { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /admin/src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /admin/src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * ... 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /admin/src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 14 | -------------------------------------------------------------------------------- /admin/src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /admin/src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const { value } = binding 6 | const roles = store.getters && store.getters.roles 7 | 8 | if (value && value instanceof Array && value.length > 0) { 9 | const permissionRoles = value 10 | 11 | const hasPermission = roles.some(role => { 12 | return permissionRoles.includes(role) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } else { 19 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /admin/src/directive/sticky.js: -------------------------------------------------------------------------------- 1 | const vueSticky = {} 2 | let listenAction 3 | vueSticky.install = Vue => { 4 | Vue.directive('sticky', { 5 | inserted(el, binding) { 6 | const params = binding.value || {} 7 | const stickyTop = params.stickyTop || 0 8 | const zIndex = params.zIndex || 1000 9 | const elStyle = el.style 10 | 11 | elStyle.position = '-webkit-sticky' 12 | elStyle.position = 'sticky' 13 | // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary) 14 | // if (~elStyle.position.indexOf('sticky')) { 15 | // elStyle.top = `${stickyTop}px`; 16 | // elStyle.zIndex = zIndex; 17 | // return 18 | // } 19 | const elHeight = el.getBoundingClientRect().height 20 | const elWidth = el.getBoundingClientRect().width 21 | elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}` 22 | 23 | const parentElm = el.parentNode || document.documentElement 24 | const placeholder = document.createElement('div') 25 | placeholder.style.display = 'none' 26 | placeholder.style.width = `${elWidth}px` 27 | placeholder.style.height = `${elHeight}px` 28 | parentElm.insertBefore(placeholder, el) 29 | 30 | let active = false 31 | 32 | const getScroll = (target, top) => { 33 | const prop = top ? 'pageYOffset' : 'pageXOffset' 34 | const method = top ? 'scrollTop' : 'scrollLeft' 35 | let ret = target[prop] 36 | if (typeof ret !== 'number') { 37 | ret = window.document.documentElement[method] 38 | } 39 | return ret 40 | } 41 | 42 | const sticky = () => { 43 | if (active) { 44 | return 45 | } 46 | if (!elStyle.height) { 47 | elStyle.height = `${el.offsetHeight}px` 48 | } 49 | 50 | elStyle.position = 'fixed' 51 | elStyle.width = `${elWidth}px` 52 | placeholder.style.display = 'inline-block' 53 | active = true 54 | } 55 | 56 | const reset = () => { 57 | if (!active) { 58 | return 59 | } 60 | 61 | elStyle.position = '' 62 | placeholder.style.display = 'none' 63 | active = false 64 | } 65 | 66 | const check = () => { 67 | const scrollTop = getScroll(window, true) 68 | const offsetTop = el.getBoundingClientRect().top 69 | if (offsetTop < stickyTop) { 70 | sticky() 71 | } else { 72 | if (scrollTop < elHeight + stickyTop) { 73 | reset() 74 | } 75 | } 76 | } 77 | listenAction = () => { 78 | check() 79 | } 80 | 81 | window.addEventListener('scroll', listenAction) 82 | }, 83 | 84 | unbind() { 85 | window.removeEventListener('scroll', listenAction) 86 | } 87 | }) 88 | } 89 | 90 | export default vueSticky 91 | 92 | -------------------------------------------------------------------------------- /admin/src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /admin/src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /admin/src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /admin/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /admin/src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/assets.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/ip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/mdashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/risk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/task.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/vuln.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /admin/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /admin/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Home 17 | 18 | 19 | 20 | Log Out 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 55 | 56 | 134 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /admin/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 58 | -------------------------------------------------------------------------------- /admin/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /admin/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /admin/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | /** 19 | * If you don't want to use mock-server 20 | * you want to use MockJs for mock api 21 | * you can execute: mockXHR() 22 | * 23 | * Currently MockJs will be used in the production environment, 24 | * please remove it before going online ! ! ! 25 | */ 26 | if (process.env.NODE_ENV === 'production') { 27 | const { mockXHR } = require('../mock') 28 | mockXHR() 29 | } 30 | 31 | // set ElementUI lang to EN 32 | Vue.use(ElementUI, { locale }) 33 | // 如果想要中文版 element-ui,按如下方式声明 34 | // Vue.use(ElementUI) 35 | 36 | Vue.config.productionTip = false 37 | 38 | new Vue({ 39 | el: '#app', 40 | router, 41 | store, 42 | render: h => h(App) 43 | }) 44 | -------------------------------------------------------------------------------- /admin/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | await store.dispatch('user/resetToken') 41 | Message.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /admin/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'PMon Admin', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /admin/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | import user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /admin/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /admin/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | if (state.hasOwnProperty(key)) { 14 | state[key] = value 15 | } 16 | } 17 | } 18 | 19 | const actions = { 20 | changeSetting({ commit }, data) { 21 | commit('CHANGE_SETTING', data) 22 | } 23 | } 24 | 25 | export default { 26 | namespaced: true, 27 | state, 28 | mutations, 29 | actions 30 | } 31 | 32 | -------------------------------------------------------------------------------- /admin/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/user' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | token: getToken(), 8 | name: '', 9 | avatar: '' 10 | } 11 | } 12 | 13 | const state = getDefaultState() 14 | 15 | const mutations = { 16 | RESET_STATE: (state) => { 17 | Object.assign(state, getDefaultState()) 18 | }, 19 | SET_TOKEN: (state, token) => { 20 | state.token = token 21 | }, 22 | SET_NAME: (state, name) => { 23 | state.name = name 24 | }, 25 | SET_AVATAR: (state, avatar) => { 26 | state.avatar = avatar 27 | } 28 | } 29 | 30 | const actions = { 31 | // user login 32 | login({ commit }, userInfo) { 33 | const { username, password } = userInfo 34 | return new Promise((resolve, reject) => { 35 | login({ username: username.trim(), password: password }).then(response => { 36 | const { data } = response 37 | commit('SET_TOKEN', data.token) 38 | setToken(data.token) 39 | resolve() 40 | }).catch(error => { 41 | reject(error) 42 | }) 43 | }) 44 | }, 45 | 46 | // get user info 47 | getInfo({ commit, state }) { 48 | return new Promise((resolve, reject) => { 49 | getInfo(state.token).then(response => { 50 | const { data } = response 51 | 52 | if (!data) { 53 | reject('Verification failed, please Login again.') 54 | } 55 | 56 | const { name, avatar } = data 57 | 58 | commit('SET_NAME', name) 59 | commit('SET_AVATAR', avatar) 60 | resolve(data) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // user logout 68 | logout({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | removeToken() // must remove token first 72 | resetRouter() 73 | commit('RESET_STATE') 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // remove token 82 | resetToken({ commit }) { 83 | return new Promise(resolve => { 84 | removeToken() // must remove token first 85 | commit('RESET_STATE') 86 | resolve() 87 | }) 88 | } 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | mutations, 95 | actions 96 | } 97 | 98 | -------------------------------------------------------------------------------- /admin/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /admin/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /admin/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /admin/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /admin/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /admin/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'PMon Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 38 | const value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | return value.toString().padStart(2, '0') 42 | }) 43 | return time_str 44 | } 45 | 46 | /** 47 | * @param {number} time 48 | * @param {string} option 49 | * @returns {string} 50 | */ 51 | export function formatTime(time, option) { 52 | if (('' + time).length === 10) { 53 | time = parseInt(time) * 1000 54 | } else { 55 | time = +time 56 | } 57 | const d = new Date(time) 58 | const now = Date.now() 59 | 60 | const diff = (now - d) / 1000 61 | 62 | if (diff < 30) { 63 | return '刚刚' 64 | } else if (diff < 3600) { 65 | // less 1 hour 66 | return Math.ceil(diff / 60) + '分钟前' 67 | } else if (diff < 3600 * 24) { 68 | return Math.ceil(diff / 3600) + '小时前' 69 | } else if (diff < 3600 * 24 * 2) { 70 | return '1天前' 71 | } 72 | if (option) { 73 | return parseTime(time, option) 74 | } else { 75 | return ( 76 | d.getMonth() + 77 | 1 + 78 | '月' + 79 | d.getDate() + 80 | '日' + 81 | d.getHours() + 82 | '时' + 83 | d.getMinutes() + 84 | '分' 85 | ) 86 | } 87 | } 88 | 89 | /** 90 | * @param {string} url 91 | * @returns {Object} 92 | */ 93 | export function param2Obj(url) { 94 | const search = url.split('?')[1] 95 | if (!search) { 96 | return {} 97 | } 98 | return JSON.parse( 99 | '{"' + 100 | decodeURIComponent(search) 101 | .replace(/"/g, '\\"') 102 | .replace(/&/g, '","') 103 | .replace(/=/g, '":"') 104 | .replace(/\+/g, ' ') + 105 | '"}' 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 50000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000) { 50 | Message({ 51 | message: res.data.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /admin/src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /admin/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/admin/components/ReportStatusPie.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 108 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/admin/components/UnreportedPie.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 107 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/admin/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeDestroy() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | // https://github.com/PanJiaChen/vue-element-admin/issues/2116 25 | activated() { 26 | this.$_initResizeEvent() 27 | this.$_initSidebarResizeEvent() 28 | }, 29 | deactivated() { 30 | this.$_destroyResizeEvent() 31 | this.$_destroySidebarResizeEvent() 32 | }, 33 | methods: { 34 | // use $_ for mixins properties 35 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 36 | $_initResizeEvent() { 37 | window.addEventListener('resize', this.$_resizeHandler) 38 | }, 39 | $_destroyResizeEvent() { 40 | window.removeEventListener('resize', this.$_resizeHandler) 41 | }, 42 | $_sidebarResizeHandler(e) { 43 | if (e.propertyName === 'width') { 44 | this.$_resizeHandler() 45 | } 46 | }, 47 | $_initSidebarResizeEvent() { 48 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 49 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | $_destroySidebarResizeEvent() { 52 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/admin/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 92 | 93 | 112 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/editor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Your roles: 6 | {{ item }} 7 | 8 | 9 | 10 | {{ name }} 11 | Editor's Dashboard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 42 | 43 | 75 | -------------------------------------------------------------------------------- /admin/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /admin/src/views/task/schedule.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 51 | -------------------------------------------------------------------------------- /admin/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /admin/tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /admin/tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /admin/tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /admin/tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /admin/tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('ten digits timestamp', () => { 9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('new Date', () => { 12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('format', () => { 15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 18 | }) 19 | it('get the day of the week', () => { 20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 24 | }) 25 | it('empty argument', () => { 26 | expect(parseTime()).toBeNull() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /admin/tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /pic/AnSecNote.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/pic/AnSecNote.jpg -------------------------------------------------------------------------------- /pic/Wechat_PMon.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/pic/Wechat_PMon.JPG -------------------------------------------------------------------------------- /server/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | #url = "https://pypi.org/simple" 4 | url = "https://mirrors.aliyun.com/pypi/simple" 5 | verify_ssl = true 6 | 7 | [dev-packages] 8 | 9 | [packages] 10 | python-dotenv = {index = "https://pypi.douban.com",version = "*"} 11 | requests = "*" 12 | ipaddress = "*" 13 | flask = "*" 14 | flask-restful = "*" 15 | celery = "*" 16 | redis = "*" 17 | pymongo = "*" 18 | flask-pymongo = "*" 19 | flask-apscheduler = "*" 20 | gunicorn = "==19.7.1" 21 | 22 | [requires] 23 | python_version = "3" -------------------------------------------------------------------------------- /server/doc/how_to_make_APScheduler_single.md: -------------------------------------------------------------------------------- 1 | # 解决多进程中APScheduler重复运行的问题 2 | 3 | https://blog.csdn.net/misayaaaaa/article/details/102684649 4 | 5 | 6 | 问题 7 | 在一个python web应用中需要定时执行一些任务,所以用了APScheduler这个库。又因为是用flask这个web框架,所以用了flask-apscheduler这个插件(本质上与直接用APScheduler一样,这里不作区分)。 8 | 9 | 在开发中直接测试运行是没有问题的,但是用gunicorn部署以后发生了重复运行的问题: 10 | 11 | 每个任务在时间到的时刻会同时执行好几遍。 12 | 13 | 注意了一下重复的数量,恰恰是gunicorn里配置的worker进程数量,显然是每个worker进程都启动了一份scheduler造成。 14 | 15 | 解决 16 | 可以想到的方案有几个: 17 | 18 | 用--preload启动gunicorn,确保scheduler只在loader的时候创建一次 19 | 另外创建一个单独的定时任务项目,单独以一个进程运行 20 | 用全局锁确保scheduler只运行一次 21 | 经过实践,只有第三个方案比较好。 22 | 23 | preload的问题: 24 | 25 | 虽然这样可以使用scheduler创建代码只执行一次,但是问题也在于它只执行一次,重新部署以后如果用kill -HUP重启gunicorn,它并不会重启,甚至整个项目都不会更新。这是preload的副作用,除非重写部署脚本,完全重启应用。 26 | 27 | 单独进程的问题: 28 | 29 | 也是因为部署麻烦,需要多一套部署方案,虽然用Docker会比较方便,但仍然不喜欢,而且同时维护两个项目也多出很多不必要的事情。 30 | 31 | 全局锁是一个较好的方案,但问题在于找一个合适的锁。 32 | 33 | python自带的多进程多线程锁方案都需要一个共享变量来维护,但是因为worker进程是被gunicorn的主进程启动的,并不方便自己维护,所以需要一个系统级的锁。 34 | 35 | 在Stackoverflow上看到有人是用了一个socket端口来做锁实现这个方案,但是我也不喜欢这样浪费一个宝贵的端口资源。不过这倒给了我一个启发: 36 | 37 | 可以用文件锁! 38 | 39 | 于是有了这个解决方案: 40 | ```python 41 | import atexit 42 | import fcntl 43 | from flask_apscheduler import APScheduler 44 | 45 | def init(app): 46 | f = open("scheduler.lock", "wb") 47 | try: 48 | fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) 49 | scheduler = APScheduler() 50 | scheduler.init_app(app) 51 | scheduler.start() 52 | except: 53 | pass 54 | def unlock(): 55 | fcntl.flock(f, fcntl.LOCK_UN) 56 | f.close() 57 | atexit.register(unlock) 58 | ``` 59 | 60 | 原理 61 | init函数为flask项目初始化所调用,这里为scheduler模块的初始化部分。 62 | 63 | 首先打开(或创建)一个scheduler.lock文件,并加上非阻塞互斥锁。成功后创建scheduler并启动。 64 | 65 | 如果加文件锁失败,说明scheduler已经创建,就略过创建scheduler的部分。 66 | 67 | 最后注册一个退出事件,如果这个flask项目退出,则解锁并关闭scheduler.lock文件的锁。 68 | 69 | 70 | 71 | 分布式场景下的问题解决: 72 | https://www.kawabangga.com/posts/2903 73 | 74 | 75 | -------------------------------------------------------------------------------- /server/pmon/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/server/pmon/backend/__init__.py -------------------------------------------------------------------------------- /server/pmon/backend/task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 任务结果处理 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from flask import request, jsonify, current_app 12 | from flask_restful import Resource 13 | 14 | import json 15 | from pmon.mongo import mongo 16 | from .port import PortParser 17 | from .web import WebParser 18 | 19 | 20 | class TaskBackend(Resource): 21 | """ 22 | 接收任务结果 23 | """ 24 | 25 | def post(self): 26 | """ 27 | :return: 28 | """ 29 | 30 | _report = json.loads(request.data) 31 | 32 | try: 33 | _task = mongo.db.task.find_one({'task-id': _report['task_id']}) 34 | 35 | TaskParse(_report, _task['param']).run() 36 | code = 200 37 | data = {'message': 'success'} 38 | except Exception as e: 39 | _task_detail = dict() 40 | code = 400 41 | data = {'message': "Update task_id:[%s] failed:[%s]" % (_report['task_id'], str(e))} 42 | current_app.logger.error("Update task_id:[%s] failed:[%s]" % (_report['task_id'], str(e))) 43 | 44 | return jsonify({'code': code, 'data': data}) 45 | 46 | 47 | class TaskParse(object): 48 | 49 | def __init__(self, report, param): 50 | 51 | if param['tag'] in ['PORT_CHEK', 'PORT_DISC', 'PORT_INFO', 'HOST_DISC', 'PORT_RISK']: 52 | self.parse = PortParser(param['tag']) 53 | elif param['tag'] in ['WEB_CHECK']: 54 | self.parse = WebParser(param['tag']) 55 | 56 | self.report = report 57 | self.param = param 58 | 59 | def run(self): 60 | try: 61 | self.parse(self.report, self.param) 62 | except Exception as e: 63 | raise Exception("Task parse error: %s" % str(e)) -------------------------------------------------------------------------------- /server/pmon/backend/web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 风险web任务结果处理 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from pmon.mongo import mongo 12 | 13 | 14 | class WebParser(object): 15 | 16 | def __init__(self, tag): 17 | __mapper = { 18 | 'WEB_CHECK': self.webcheck, 19 | } 20 | self.func = __mapper[tag] 21 | 22 | def __call__(self, report, param): 23 | self.func(report, param) 24 | 25 | def webcheck(self, report, param): 26 | 27 | collection = mongo.db.webcheck 28 | 29 | try: 30 | for res in report['result']: 31 | collection.update_one({'url': res['url']}, { 32 | '$set': res, 33 | '$setOnInsert': { 34 | 'create_time': report['timestamp'], 35 | 'assets': { 36 | 'host': 'NA', 37 | 'owner': 'NA', 38 | 'sys_code': 'NA', 39 | 'business': 'NA', 40 | }, 41 | 'ticket_info': { 42 | 'status': 'UNREPORTED', 43 | 'ticket_id': 'NA', 44 | 'owner': 'NA', 45 | 'remark': 'NA', 46 | 'create_time': 0, 47 | 'source': 'NA' 48 | } 49 | }}, upsert=True) 50 | except Exception as e: 51 | raise Exception("webcheck: [%s] report: %s param: %s" % (str(e), str(report), str(param))) 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /server/pmon/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/server/pmon/jobs/__init__.py -------------------------------------------------------------------------------- /server/pmon/mongo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | mongo 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | 12 | from flask_pymongo import PyMongo 13 | 14 | # 数据库 15 | mongo = PyMongo() 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/pmon/nodes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 节点相关 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | 12 | from pmon.mongo import mongo 13 | 14 | 15 | class Nodes(object): 16 | 17 | def __init__(self): 18 | self.collection = mongo.db.node 19 | self.nodes = [i for i in self.collection.find({'status': 'enable'}, {'_id': 0})] 20 | 21 | def get_node(self, name): 22 | 23 | _node = dict() 24 | for n in self.nodes: 25 | if n['name'] == name: 26 | _node = n 27 | if not _node: 28 | raise Exception("Can not find node name:[%s] in [%s]." % (name, self.collection)) 29 | 30 | return _node 31 | 32 | def select_best_node(self): 33 | """ 34 | 选择最优节点 35 | 36 | TODO: 后期完成 37 | """ 38 | return dict() 39 | -------------------------------------------------------------------------------- /server/pmon/resource/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 插件配置 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | 12 | from flask_restful import Api 13 | 14 | from .login import Login, User 15 | from .task import TaskIssue, Tasks 16 | from ..backend.task import TaskBackend 17 | from .port import Ports 18 | from .ticket import Ticket 19 | from .web import Web 20 | from .ipassets import IPAssets 21 | from .ipset import IPSset 22 | from .dashboard import Dashboard, PanelCount, HisLine, RiskBar, ReportPie, UnreportedPie 23 | from .base import BaseResource 24 | from .settings import Settings 25 | from .schedule import Trigger, Job, Task, Schedule 26 | from .node import Node 27 | 28 | base_url = '/admin' 29 | 30 | api = Api() 31 | api.add_resource(Login, base_url + '/user/login') 32 | api.add_resource(User, base_url + '/user/info') 33 | api.add_resource(Node, base_url + '/node') 34 | 35 | api.add_resource(Dashboard, base_url + '/dashboard') 36 | api.add_resource(PanelCount, base_url + '/panel_count') 37 | api.add_resource(HisLine, base_url + '/his_line') 38 | api.add_resource(RiskBar, base_url + '/risk_bar') 39 | api.add_resource(ReportPie, base_url + '/report_pie') 40 | api.add_resource(UnreportedPie, base_url + '/unreported_pie') 41 | 42 | api.add_resource(TaskIssue, base_url + '/task/issue') 43 | api.add_resource(Tasks, base_url + '/tasks') 44 | 45 | api.add_resource(TaskBackend, base_url + '/taskend') 46 | 47 | api.add_resource(Ports, base_url + '/ports') 48 | api.add_resource(Ticket, base_url + '/ticket') 49 | api.add_resource(Web, base_url + '/web') 50 | 51 | api.add_resource(IPAssets, base_url + '/ipassets') 52 | api.add_resource(IPSset, base_url + '/ipset') 53 | api.add_resource(Settings, base_url + '/settings') 54 | api.add_resource(BaseResource, '/base') 55 | 56 | api.add_resource(Trigger, base_url + '/schedule/trigger') 57 | api.add_resource(Job, base_url + '/schedule/job') 58 | api.add_resource(Task, base_url + '/schedule/task') 59 | api.add_resource(Schedule, base_url + '/schedule/schedule') 60 | 61 | -------------------------------------------------------------------------------- /server/pmon/resource/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | Resource基类 6 | """ 7 | 8 | from __future__ import print_function 9 | from __future__ import absolute_import 10 | from flask import jsonify 11 | from flask_restful import Resource, reqparse 12 | 13 | 14 | class BaseResource(Resource): 15 | 16 | def __init__(self): 17 | self.collection = None 18 | self.limit = 20 19 | self.skip = 0 20 | self.action = None 21 | self.parser_default_args() 22 | 23 | def parser_default_args(self): 24 | # 默认的参数解析 [page=1&limit=20&sort=%2Btask_id] 25 | parser = reqparse.RequestParser() 26 | parser.add_argument('page', type=int, location='args') 27 | parser.add_argument('limit', type=int, location='args') 28 | parser.add_argument('sort', type=str, location='args') 29 | parser.add_argument('action', type=str, location='args') 30 | 31 | args = parser.parse_args() 32 | 33 | if args['limit']: 34 | self.limit = args['limit'] 35 | if args['page']: 36 | self.skip = args['limit'] * (args['page'] - 1) 37 | self.action = args['action'] 38 | 39 | # TODO: parse sort 40 | 41 | def options(self, agg_body, default_data): 42 | """ 43 | 获取页面中过滤项中的可选择的选择项列表 44 | :return: 45 | """ 46 | 47 | try: 48 | try: 49 | dataset = list(self.collection.aggregate(agg_body))[0] 50 | except IndexError: 51 | # 如果为空,则返回默认的空值; 52 | dataset = default_data 53 | code = 20000 54 | data = dataset 55 | except Exception as e: 56 | code = 40000 57 | data = {'message': 'failed: [%s]' % str(e)} 58 | 59 | return self.create_response(code, data) 60 | 61 | @staticmethod 62 | def create_response(code, data): 63 | return jsonify({'code': code, 'data': data}) 64 | 65 | -------------------------------------------------------------------------------- /server/pmon/resource/ipset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 存活IP 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from flask_restful import reqparse, inputs 12 | from pmon.mongo import mongo 13 | 14 | from .base import BaseResource 15 | 16 | parser = reqparse.RequestParser() 17 | parser.add_argument('ip', type=inputs.regex(r'[\d.\\]{7,18}'), location=['args', 'json']) 18 | parser.add_argument('owner', type=str, location=['args', 'json']) 19 | parser.add_argument('host', type=str, location=['args', 'json']) 20 | parser.add_argument('sys_code', type=str, location=['args', 'json']) 21 | parser.add_argument('business', type=str, location=['args', 'json']) 22 | parser.add_argument('status', type=str, location=['args', 'json']) 23 | parser.add_argument('range', type=str, location=['args', 'json']) 24 | parser.add_argument('discover_range[]', type=int, action='append', location=['args']) 25 | 26 | 27 | class IPSset(BaseResource): 28 | 29 | def get(self): 30 | """" 31 | :return: 32 | """ 33 | 34 | self.collection = mongo.db.ipset 35 | 36 | if self.action == 'options': 37 | return self.options([{ 38 | '$group': { 39 | '_id': 'options', 40 | 'ip': {'$addToSet': '$source'}, 41 | 'range': {'$addToSet': '$range'}, 42 | 'owner': {'$addToSet': '$assets.owner'}, 43 | 'host': {'$addToSet': '$assets.host'}, 44 | 'sys_code': {'$addToSet': '$assets.sys_code'}, 45 | 'business': {'$addToSet': '$assets.business'}, 46 | 'status': {'$addToSet': '$status'} 47 | }}], {'ip': [], 'range': [], 'owner': [], 'host': [], 'sys_code': [], 'business': [], 'status': []}) 48 | 49 | args = parser.parse_args() 50 | 51 | query_body = dict() 52 | for _key, _value in args.items(): 53 | if _key == 'discover_range[]' and _value: 54 | query_body['discover_time'] = {'$gte': int(_value[0] / 1000), '$lte': int(_value[1] / 1000)} 55 | elif _key in ['owner', 'host', 'sys_code', 'business'] and _value: 56 | query_body['assets.' + _key] = _value 57 | elif _value: 58 | query_body[_key] = _value 59 | else: 60 | continue 61 | 62 | try: 63 | dataset = list(self.collection.find(query_body, {'_id': 0}).sort([('discover_time', -1)]).skip(self.skip).limit(self.limit)) 64 | datalen = len(list(self.collection.find(query_body, {'_id': 0}))) 65 | code = 20000 66 | data = { 67 | 'total': datalen, 68 | 'items': dataset 69 | } 70 | except Exception as e: 71 | code = 40000 72 | data = {'message': 'failed: [%s]' % str(e)} 73 | 74 | return self.create_response(code, data) 75 | 76 | def post(self): 77 | """ 78 | 新建、更新 IP资产 79 | :return: 80 | """ 81 | # args = json.loads(request.data) 82 | self.collection = mongo.db.ipset 83 | args = parser.parse_args() 84 | try: 85 | self.collection.update_one({'ip': args['ip']}, {'$set': args}, upsert=True) 86 | code = 20000 87 | data = {'message': 'success'} 88 | except Exception as e: 89 | code = 40000 90 | data = {'message': str(e)} 91 | 92 | return self.create_response(code, data) 93 | -------------------------------------------------------------------------------- /server/pmon/resource/login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 登录管理 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | 12 | import json 13 | from flask import request 14 | from flask_restful import Resource, reqparse 15 | 16 | 17 | tokens = { 18 | 'admin': {'token': 'admin-token'}, 19 | 'editor': {'token': 'editor-token'} 20 | } 21 | 22 | 23 | class Login(Resource): 24 | 25 | def post(self): 26 | request_body = json.loads(request.data) 27 | 28 | token = tokens.get(request_body['username']) 29 | if token: 30 | return {'code': 20000, 'data': token} 31 | 32 | 33 | users = { 34 | 'admin-token': { 35 | 'roles': ['admin'], 36 | 'introduction': 'I am a super administrator', 37 | 'avatar': 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 38 | 'name': 'Super Admin' 39 | }, 40 | 'editor-token': { 41 | 'roles': ['editor'], 42 | 'introduction': 'I am an editor', 43 | 'avatar': 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 44 | 'name': 'Normal Editor' 45 | } 46 | } 47 | 48 | 49 | class User(Resource): 50 | 51 | def get(self): 52 | parser = reqparse.RequestParser() 53 | parser.add_argument('token', type=str, location='args') 54 | args = parser.parse_args() 55 | 56 | user = users.get(args['token']) 57 | if user: 58 | return {'code': 20000, 'data': user} -------------------------------------------------------------------------------- /server/pmon/resource/node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 节点管理 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from flask_restful import reqparse 12 | from .base import BaseResource 13 | from pmon.mongo import mongo 14 | from pmon.resource.schedule import delete_collection, modify_collection 15 | parser = reqparse.RequestParser() 16 | 17 | parser.add_argument('uuid', type=str, location=['args', 'json']) 18 | parser.add_argument('name', type=str, location=['args', 'json']) 19 | parser.add_argument('remark', type=str, location=['args', 'json']) 20 | parser.add_argument('api', type=str, location=['args', 'json']) 21 | parser.add_argument('scope', type=str, action='append', location=['args', 'json']) 22 | parser.add_argument('status', type=str, location=['args', 'json']) 23 | 24 | 25 | class Node(BaseResource): 26 | 27 | def get(self): 28 | """ 29 | :return: 30 | """ 31 | self.collection = mongo.db.node 32 | 33 | try: 34 | __data = list(self.collection.find({}, {'_id': 0}).sort([('_id', 1)])) 35 | code = 20000 36 | data = { 37 | 'total': len(__data), 38 | 'items': __data 39 | } 40 | except Exception as e: 41 | code = 40000 42 | data = {'message': str(e)} 43 | 44 | return self.create_response(code, data) 45 | 46 | def post(self): 47 | """ 48 | 新建、更新 节点 49 | :return: 50 | """ 51 | collection = mongo.db.node 52 | 53 | args = parser.parse_args() 54 | 55 | try: 56 | _doc = dict() 57 | for _key, _value in args.items(): 58 | if _value: 59 | _doc[_key] = _value 60 | _doc['type'] = 'node' 61 | modify_collection(collection, _doc.get('uuid'), _doc) 62 | code = 20000 63 | data = {'message': 'success'} 64 | except Exception as e: 65 | code = 40000 66 | data = {'message': str(e)} 67 | return self.create_response(code, data) 68 | 69 | def delete(self): 70 | """ 71 | 删除节点 72 | :return: 73 | """ 74 | collection = mongo.db.node 75 | args = parser.parse_args() 76 | code, data = delete_collection(collection, args) 77 | return self.create_response(code, data) 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /server/pmon/resource/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 系统配置管理 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | 12 | from flask_restful import reqparse 13 | from pmon.mongo import mongo 14 | from .base import BaseResource 15 | 16 | 17 | parser = reqparse.RequestParser() 18 | parser.add_argument('name', type=str, location=['args', 'json']) 19 | parser.add_argument('value', type=str, action='append', location=['arg', 'json']) 20 | 21 | 22 | class Settings(BaseResource): 23 | 24 | def get(self): 25 | """ 26 | :return: 27 | """ 28 | 29 | self.collection = mongo.db.settings 30 | 31 | try: 32 | __data = list(self.collection.find({}, {'_id': 0})) 33 | code = 20000 34 | data = { 35 | 'total': len(__data), 36 | 'items': __data 37 | } 38 | except Exception as e: 39 | code = 40000 40 | data = {'message': str(e)} 41 | 42 | return self.create_response(code, data) 43 | 44 | def post(self): 45 | """ 46 | 新建、更新 节点 47 | :return: 48 | """ 49 | # args = json.loads(request.data) 50 | 51 | self.collection = mongo.db.settings 52 | args = parser.parse_args() 53 | 54 | print(args) 55 | 56 | try: 57 | self.collection.update_one({'name': args['name']}, {'$set': args}) 58 | code = 20000 59 | data = {'message': 'success'} 60 | except Exception as e: 61 | code = 40000 62 | data = {'message': str(e)} 63 | 64 | return self.create_response(code, data) 65 | 66 | def delete(self): 67 | """ 68 | 删除节点 69 | :return: 70 | """ 71 | # args = json.loads(request.data) 72 | 73 | self.collection = mongo.db.node 74 | args = parser.parse_args() 75 | 76 | try: 77 | self.collection.delete_one({'name': args['name']}) 78 | code = 20000 79 | data = {'message': 'success'} 80 | except Exception as e: 81 | code = 40000 82 | data = {'message': str(e)} 83 | 84 | return self.create_response(code, data) 85 | 86 | @classmethod 87 | def get_info(cls, name, field=None): 88 | 89 | _data = mongo.db.node.find_one({'name': name}, {'_id': 0}) 90 | if field: 91 | return _data.get(field, None) 92 | else: 93 | return _data 94 | 95 | 96 | -------------------------------------------------------------------------------- /server/pmon/resource/ticket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 工单报备 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from flask_restful import reqparse, inputs 12 | from pmon.mongo import mongo 13 | 14 | from .base import BaseResource 15 | 16 | parser = reqparse.RequestParser() 17 | parser.add_argument('ip', type=inputs.regex(r'[\d.\\]{7,18}'), location=['args', 'json']) 18 | parser.add_argument('port', type=str, location=['args', 'json']) 19 | parser.add_argument('url', type=str, location=['args', 'json']) 20 | 21 | # ticket 22 | parser.add_argument('ticket_id', type=str, location=['args', 'json']) 23 | parser.add_argument('source', type=str, location=['args', 'json']) 24 | parser.add_argument('remark', type=str, location=['args', 'json']) 25 | parser.add_argument('owner', type=str, location=['args', 'json']) 26 | parser.add_argument('status', type=str, location=['args', 'json']) 27 | parser.add_argument('create_time', type=int, location=['args', 'json']) 28 | 29 | 30 | class Ticket(BaseResource): 31 | 32 | def post(self): 33 | 34 | args = parser.parse_args() 35 | 36 | _doc = dict() 37 | for _key, _value in args.items(): 38 | if _value: 39 | _doc[_key] = _value 40 | 41 | if 'ip' and 'port' in _doc.keys(): 42 | query = {'ip': _doc['ip'], 'port': _doc['port']} 43 | self.collection = mongo.db.port 44 | elif 'url' in _doc.keys(): 45 | query = {'url': _doc['url']} 46 | self.collection = mongo.db.webcheck 47 | else: 48 | return self.create_response(40000, {'message': 'Can not parser ip:port or url.'}) 49 | 50 | self.collection.update_one(query, { 51 | '$set': { 52 | 'ticket_info.owner': _doc['owner'], 53 | 'ticket_info.ticket_id': _doc['ticket_id'], 54 | 'ticket_info.status': _doc['status'], 55 | 'ticket_info.source': _doc['source'], 56 | 'ticket_info.remark': _doc['remark'], 57 | 'ticket_info.create_time': _doc['create_time'] 58 | }}) 59 | 60 | return self.create_response(20000, {'message': 'Update success: %s' % str(query)}) 61 | -------------------------------------------------------------------------------- /server/pmon/resource/web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 风险web 6 | 7 | """ 8 | 9 | from __future__ import print_function 10 | from __future__ import absolute_import 11 | from flask_restful import reqparse 12 | from pmon.mongo import mongo 13 | 14 | from .base import BaseResource 15 | 16 | parser = reqparse.RequestParser() 17 | parser.add_argument('url', type=str, location=['args', 'json']) 18 | parser.add_argument('code', type=int, location=['args', 'json']) 19 | parser.add_argument('tag', type=str, location=['args', 'json']) 20 | parser.add_argument('status', type=str, location=['args', 'json']) 21 | parser.add_argument('create_range[]', type=int, action='append', location=['args']) 22 | 23 | 24 | class Web(BaseResource): 25 | 26 | def get(self): 27 | """" 28 | :return: 29 | """ 30 | 31 | self.collection = mongo.db.webcheck 32 | 33 | if self.action == 'options': 34 | return self.options([{ 35 | '$group': { 36 | '_id': 'options', 37 | 'code': {'$addToSet': '$code'}, 38 | 'tag': {'$addToSet': '$tag'}, 39 | 'status': {'$addToSet': '$status'}, 40 | }}], {'code': [], 'tag': [], 'status': []}) 41 | 42 | args = parser.parse_args() 43 | 44 | query_body = dict() 45 | 46 | for _key, _value in args.items(): 47 | if _key == 'create_range[]' and _value: 48 | query_body['create_time'] = {'$gte': int(_value[0] / 1000), '$lte': int(_value[1] / 1000)} 49 | elif _value: 50 | query_body[_key] = _value 51 | else: 52 | continue 53 | 54 | try: 55 | dataset = list( 56 | self.collection.find( 57 | query_body, {'_id': 0}).sort([('timestamp', -1)]).skip(self.skip).limit(self.limit)) 58 | datalen = len(list(self.collection.find(query_body, {'_id': 0}))) 59 | 60 | code = 20000 61 | data = { 62 | 'total': datalen, 63 | 'items': dataset 64 | } 65 | except Exception as e: 66 | code = 40000 67 | data = {'message': 'failed: [%s]' % str(e)} 68 | return self.create_response(code, data) 69 | -------------------------------------------------------------------------------- /server/pmon/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import os 5 | basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 6 | 7 | 8 | class BaseConfig(object): 9 | 10 | # SQLALCHEMY_TRACK_MODIFICATIONS = False 11 | # SQLALCHEMY_DATABASE_URI = 'sqlite:///%s/%s' % (basedir, 'db.sqlite3') 12 | 13 | SCHEDULER_API_ENABLED = True 14 | 15 | 16 | class DevelopmentConfig(BaseConfig): 17 | 18 | MONGO_URI = "mongodb://127.0.0.1:27017/PMon" 19 | 20 | 21 | class ProductionConfig(BaseConfig): 22 | MONGO_URI = "mongodb://127.0.0.1:27017/PMon" 23 | 24 | 25 | class TestingConfig(BaseConfig): 26 | TESTING = True 27 | # SQLALCHEMY_DATABASE_URI = 'sqlite:///' 28 | WTF_CSRF_ENABLED = False 29 | 30 | 31 | config = { 32 | 'development': DevelopmentConfig, 33 | 'production': ProductionConfig, 34 | 'testing': TestingConfig 35 | } 36 | -------------------------------------------------------------------------------- /server/scheduler.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/server/scheduler.lock -------------------------------------------------------------------------------- /server/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from dotenv import load_dotenv 6 | from pmon import create_app 7 | 8 | dotenv_path = os.path.join(os.path.dirname(__file__), '.flaskenv') 9 | if os.path.exists(dotenv_path): 10 | load_dotenv(dotenv_path) 11 | 12 | app = create_app(os.getenv('FLASK_ENV')) 13 | -------------------------------------------------------------------------------- /work/.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | #lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | .flaskenv 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypyf 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | bin/masscan 132 | 133 | *.rdb 134 | Pipfile.lock 135 | -------------------------------------------------------------------------------- /work/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.douban.com/simple" 4 | #url = "https://mirrors.aliyun.com/pypi/simple" 5 | verify_ssl = true 6 | 7 | [dev-packages] 8 | 9 | [packages] 10 | python-dotenv = {index = "https://pypi.douban.com",version = "*"} 11 | requests = "*" 12 | python-libnmap = "*" 13 | ipaddress = "*" 14 | celery = "*" 15 | gunicorn = "==19.7.1" 16 | lxml = "*" 17 | redis = "*" 18 | flower = "*" 19 | 20 | [requires] 21 | python_version = "3" 22 | -------------------------------------------------------------------------------- /work/doc/logger_config.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```python 4 | 5 | # 日志配置 6 | # logger = logging.getLogger(__name__) 7 | # formatter = TaskFormatter('%(asctime)s - %(task_id)s - %(task_name)s - %(name)s - %(levelname)s - %(message)s') 8 | # file_handler = RotatingFileHandler(os.path.join(basedir, './PortAg/log/celery.log'), 9 | # maxBytes=10 * 1024 * 1024, backupCount=10) 10 | # file_handler.setFormatter(formatter) 11 | # file_handler.setLevel(logging.INFO) 12 | # logger.addHandler(file_handler) 13 | 14 | # logger = get_task_logger(__name__) 15 | ``` -------------------------------------------------------------------------------- /work/doc/single_process_demo.md: -------------------------------------------------------------------------------- 1 | ## Python执行可执行文件的方法demo 2 | 3 | ```python 4 | 5 | #!/usr/bin/env python 6 | # -*- coding: utf-8 -*- 7 | 8 | import os 9 | import shlex 10 | import subprocess 11 | from threading import Thread 12 | from xml.dom import pulldom 13 | import warnings 14 | import platform 15 | try: 16 | import pwd 17 | except ImportError: 18 | pass 19 | 20 | 21 | class MasscanProcess(Thread): 22 | 23 | def __init__(self): 24 | Thread.__init__(self) 25 | 26 | self.cmd = '/usr/bin/sudo /Users/chiweiwei/Development/PortAg/PortAg/bin/masscan -oJ - -p22,8080,80 -n -Pn 10.0.0.0/24' 27 | 28 | def run(self): 29 | proc = subprocess.Popen(args=shlex.split(self.cmd), 30 | stdout=subprocess.PIPE, 31 | stderr=subprocess.PIPE, 32 | universal_newlines=True, 33 | bufsize=0) 34 | 35 | print(proc.poll()) 36 | print("---------------") 37 | while proc.poll() is None: 38 | for stre in iter(proc.stderr.readline, ''): 39 | print(stre) 40 | 41 | for streamline in iter(proc.stdout.readline, ''): 42 | print(streamline) 43 | 44 | print(proc.stderr.read()) 45 | print("+++++++++++++") 46 | print(proc.poll()) 47 | 48 | 49 | if __name__ == '__main__': 50 | # m = MasscanProcess() 51 | # m.start() 52 | 53 | import re 54 | m = re.match(r'Starting\smasscan\s([\d\.]{5}) \(.+?\)\sat\s([\d\-\s\:]{19}) GMT\n', 'Starting masscan 1.0.6 (http://bit.ly/14GZzcT) at 2020-01-04 08:53:31 GMT\n') 55 | print(m.group(2)) 56 | 57 | 58 | ``` -------------------------------------------------------------------------------- /work/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/work/log/.gitkeep -------------------------------------------------------------------------------- /work/work/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/work/work/__init__.py -------------------------------------------------------------------------------- /work/work/celery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 6 | """ 7 | 8 | from __future__ import print_function 9 | from __future__ import absolute_import 10 | 11 | import os 12 | import logging 13 | from celery import Celery, chain 14 | from celery.signals import after_setup_logger 15 | from .port import NmapTask, MasscanTask 16 | from .web import WebCheckTask 17 | 18 | basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 19 | brokers = 'redis://127.0.0.1:6379/1' 20 | backend = 'redis://127.0.0.1:6379/2' 21 | 22 | app = Celery('work', broker=brokers, backend=backend) 23 | 24 | nmap_task = NmapTask() 25 | masscan_task = MasscanTask() 26 | webcheck_task = WebCheckTask() 27 | 28 | 29 | @app.task(bind=True) 30 | def nmap(self, *args, **kwargs): 31 | return nmap_task(self, *args) 32 | 33 | 34 | @app.task(bind=True) 35 | def masscan(self, *args, **kwargs): 36 | return masscan_task(self, *args) 37 | 38 | 39 | @app.task(bind=True) 40 | def webcheck(self, *args, **kwargs): 41 | return webcheck_task(self, *args) 42 | 43 | 44 | @after_setup_logger.connect 45 | def setup_loggers(*args, **kwargs): 46 | 47 | logger = logging.getLogger("work") 48 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 49 | 50 | # StreamHandler 51 | # sh = logging.StreamHandler() 52 | # sh.setFormatter(formatter) 53 | # logger.addHandler(sh) 54 | 55 | # FileHandler 56 | fh = logging.FileHandler('./log/worker.log') 57 | fh.setFormatter(formatter) 58 | logger.addHandler(fh) -------------------------------------------------------------------------------- /work/work/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiww/PMon/e710862312764502b0299f6ce241717a090b683d/work/work/lib/__init__.py -------------------------------------------------------------------------------- /work/work/task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | proc(address, options) => stdout 6 | parser(stdout) => report 7 | reformat(report) => output 8 | 9 | """ 10 | 11 | from __future__ import print_function 12 | from __future__ import absolute_import 13 | import time 14 | import requests 15 | from celery.utils.log import get_task_logger 16 | 17 | logger = get_task_logger("work") 18 | 19 | 20 | class Task(object): 21 | 22 | @staticmethod 23 | def handle_output(task_obj, task_tag, output, result_backend): 24 | 25 | data = dict() 26 | data['timestamp'] = int(time.time()) 27 | data['result'] = output 28 | data['task_id'] = task_obj.request.id 29 | data['tag'] = task_tag 30 | 31 | try: 32 | response = requests.post(url=result_backend, json=data) 33 | 34 | # 将结果推送到目标web api 35 | if response.status_code != 200: 36 | logger.error("Error! Response code [%s]; [%s]" % (str(response.status_code), data['task_id'])) 37 | else: 38 | logger.info("Backend Request success! [%s]" % data['task_id']) 39 | except Exception as e: 40 | logger.error("Error! [%s] Post to [%s] [%s]!" % (data['task_id'], result_backend, str(e))) 41 | 42 | return output 43 | 44 | @staticmethod 45 | def event(task_obj): 46 | 47 | def _event(scan_obj): 48 | cur_task = scan_obj.current_task 49 | if cur_task: 50 | _task = scan_obj.tasks[cur_task.name] 51 | task_obj.update_state(state='PROGRESS', meta={'percent': _task.progress}) 52 | 53 | return _event 54 | 55 | -------------------------------------------------------------------------------- /work/work/web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | 高危Web应用检测 6 | """ 7 | 8 | from __future__ import print_function 9 | from __future__ import absolute_import 10 | from work.lib.webcheck import WebCheckProcess 11 | from celery.utils.log import get_task_logger 12 | from .task import Task 13 | 14 | logger = get_task_logger("work") 15 | 16 | 17 | class WebCheckTask(Task): 18 | 19 | def __init__(self): 20 | self.process = WebCheckProcess 21 | self.output = [] 22 | super(Task, self) 23 | 24 | def __call__(self, task_obj, task_tag, address, options, result_backend): 25 | 26 | self.options = options 27 | proc = self.process(targets=str(address), options=options) 28 | proc.run() 29 | self.stdout = proc.stdout 30 | return self.handle_output(task_obj, task_tag, self.stdout, result_backend) 31 | --------------------------------------------------------------------------------