├── front
├── static
│ └── .gitkeep
├── .dockerignore
├── .eslintignore
├── build
│ ├── logo.png
│ ├── vue-loader.conf.js
│ ├── build.js
│ ├── check-versions.js
│ ├── webpack.base.conf.js
│ ├── utils.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── prod.env.js
│ ├── test.env.js
│ ├── dev.env.js
│ └── index.js
├── src
│ ├── assets
│ │ └── logo.png
│ ├── App.vue
│ ├── main.js
│ └── components
│ │ └── Echart.vue
├── .editorconfig
├── .gitignore
├── .postcssrc.js
├── Dockerfile
├── .babelrc
├── README.md
├── index.html
├── .eslintrc.js
└── package.json
├── .gitignore
├── api
├── requirements.txt
├── Dockerfile
└── app.py
├── parser
├── requirements.txt
├── Dockerfile
├── ws
│ └── ws.py
├── parser.py
├── board_parser.py
└── game.py
├── .env.example
├── nginx
└── http.conf
├── LICENSE
├── docker-compose.yml
├── README.md
└── docs
└── README.ru.md
/front/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | data/
--------------------------------------------------------------------------------
/front/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/api/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.1.2
2 | pymongo==3.12.1
--------------------------------------------------------------------------------
/front/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 | /test/unit/coverage/
6 |
--------------------------------------------------------------------------------
/front/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Red-Cadets/beard/HEAD/front/build/logo.png
--------------------------------------------------------------------------------
/front/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/front/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Red-Cadets/beard/HEAD/front/src/assets/logo.png
--------------------------------------------------------------------------------
/parser/requirements.txt:
--------------------------------------------------------------------------------
1 | selenium==3.7.0
2 | requestium==0.1.9
3 | bs4==0.0.1
4 | requests>=2.20.0
5 | coloredlogs
6 | webdriver-manager
7 | websockets
8 | pymongo
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SCOREBOARD=http://6.0.0.1/board
2 | TEAM='Red Cadets'
3 | TYPE=hackerdom
4 | BOT_URL=http://bot/key
5 | ROUND_TIME=120
6 | EXTEND_ROUND=5
7 | MONGO_USER=parser
8 | MONGO_PASS=parser
--------------------------------------------------------------------------------
/front/config/test.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const devEnv = require('./dev.env')
4 |
5 | module.exports = merge(devEnv, {
6 | NODE_ENV: '"testing"'
7 | })
8 |
--------------------------------------------------------------------------------
/front/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8-slim
2 |
3 | WORKDIR /app
4 |
5 | COPY requirements.txt .
6 | RUN pip3 install -r requirements.txt
7 |
8 | COPY app.py .
9 |
10 | CMD [ "python3", "app.py"]
11 |
12 |
--------------------------------------------------------------------------------
/front/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/front/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | /test/unit/coverage/
8 | /test/e2e/reports/
9 | selenium-debug.log
10 |
11 | # Editor directories and files
12 | .idea
13 | .vscode
14 | *.suo
15 | *.ntvs*
16 | *.njsproj
17 | *.sln
18 |
--------------------------------------------------------------------------------
/front/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/front/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | Language: English | Русский 10 |
11 | 12 | Beard - a comfortable way to track progress of your team during A/D competitions 13 | 14 | 15 | 16 |
17 |
18 | # ✨ Features
19 |
20 | - Parsing of supported scoreboards (hackerdom/forcad)
21 | - Score graph of all teams with automatic scaling for your team
22 | - Primitive prediction of the score graph
23 | - Flag loss graph for each service
24 | - Graph of receiving flags for each service (similar to the effectiveness of exploits)
25 | - Telegram alerts about flag loss, service status, place changing (use [courier](https://github.com/Red-Cadets/courier))
26 |
27 | ## 🛠 Supported scoreboards
28 |
29 | | **A/D framework** | Link | Status | Description
30 | | ------------------ | ---- | ------ | -----------
31 | | ForcAD | https://github.com/pomo-mondreganto/ForcAD | ✅ |
32 | | HackerDom checksystem | https://github.com/HackerDom/checksystem | ✅ | parsing old-style view at /board
33 |
34 | ## 🙋 Table of Contents
35 | * 📖 [Fast Installation Guide](https://github.com/Red-Cadets/beard#-fast-installation-guide)
36 | * 🐋 [Docker Usage](https://github.com/Red-Cadets/beard#whale-docker)
37 | * 🖼️ [Gallery](https://github.com/Red-Cadets/beard#-gallery)
38 | * 🎪 [Community](https://github.com/Red-Cadets/beard#-community)
39 | * 📝 [TODO](https://github.com/Red-Cadets/beard#-todo)
40 |
41 |
42 | # 📖 Fast Installation Guide
43 |
44 | ## :whale: Docker
45 |
46 | Clone repository
47 | ```bash
48 | git clone https://github.com/Red-Cadets/beard.git
49 | ```
50 | Go to folder:
51 | ```bash
52 | cd beard
53 | ```
54 | Change .env with your settings:
55 | - `SCOREBOARD` - Scoreboard location. Example: `http://6.0.0.1/board`
56 | - `TEAM` - Team name or team IP to display information about. Example: `Red Cadets` or `10.10.1.15`
57 | - `TYPE` - Scoreboard type. Example: `forcad` or `hackerdom`
58 | - `BOT_URL` - Telegram bot api address (webhook) for notification. For easy bot integration, use [courier](https://github.com/Red-Cadets/courier). Message format:
59 | ```json
60 | {
61 | "message": "Notification here",
62 | "type": "markdown",
63 | "id": "parser",
64 | "to": "tg chat id here"
65 | }
66 | ```
67 |
68 | - `ROUND_TIME` - Round time in seconds. For example: `120`
69 | - `EXTEND_ROUND` - The number of rounds to predict future graph. The prediction is based on the points of the last 5 rounds. For example: `10`
70 | - `MONGO_USER` - DB username. Например: `parser`
71 | - `MONGO_PASS` - DB password. Например: `parser`
72 | Run docker-compose:
73 | ```bash
74 | docker-compose up -d
75 | ```
76 | and go to URL
77 | ```bash
78 | http://127.0.0.1:65005/
79 | ```
80 |
81 | ## 🖼️ Gallery
82 |
83 | ||
84 | |:-------------------------:|
85 | ||
86 | |Graph of scores of all teams on the scoreboard|
87 | |
88 | |Flag loss graph|
89 | ||
90 | |Graph of receiving flags|
91 | ||
92 | |Telegram alerts|
93 | # 🎪 Community
94 |
95 | If you have any feature suggestions or bugs, leave a Github issue.
96 | Open to pull requests and other forms of collaboration!
97 |
98 | We communicate over Telegram. [Click here](https://t.me/redcadets_chat) to join our Telegram community!
99 |
100 | ## 📝 TODO
101 |
102 | > Open to ideas!
103 |
104 | # ❤️ Thanks to
105 |
106 | Hackerdom parser is based on https://github.com/Vindori/hackerdom-board-parser
--------------------------------------------------------------------------------
/docs/README.ru.md:
--------------------------------------------------------------------------------
1 | # Beard - Attack/Defense CTF scoreboard parser
2 |
3 | 9 | Язык: English | Русский 10 |
11 | 12 | Beard - инструмент для удобного отслеживания прогресса вашей команды во время A/D CTF соревнований. 13 | 14 |
15 |
16 | # ✨ Возможности
17 |
18 | - Парсинг поддерживаемых видов турнирных таблиц (hackerdom/forcad)
19 | - График очков всех команд с автоматическим масштабированием для вашей команды
20 | - Примитивное предсказание графика очков
21 | - График потерь флагов для каждого сервиса
22 | - График получения флагов для каждого сервиса (аналогично эффективности эксплойтов)
23 | - Оповещения в телеграм о потере флагов, статусе сервисов, изменении места команды (используйте [курьера](https://github.com/Red-Cadets/courier))
24 |
25 | ## 🛠 Поддерживаемые турнирные таблицы
26 |
27 | | **A/D framework** | Ссылка | Статус | Описание
28 | | ------------------ | ---- | ------ | -----------
29 | | ForcAD | https://github.com/pomo-mondreganto/ForcAD | ✅ |
30 | | HackerDom checksystem | https://github.com/HackerDom/checksystem | ✅ | парсинг старой версии на /board
31 |
32 | ## 🙋 Содержание
33 | * 📖 [Инструкция по быстрой установке](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-инструкция-по-быстрой-установке)
34 | * 🐋 [Docker](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#whale-docker)
35 | * 🖼️ [Галерея скриншотов](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-галерея)
36 | * 🎪 [Сообщество](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-сообщество)
37 | * 📝 [Планы на будущее](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-планы-на-будущее)
38 |
39 |
40 | # 📖 Инструкция по быстрой установке]
41 |
42 | ## :whale: Docker
43 |
44 | Скачайте репозиторий
45 | ```bash
46 | git clone https://github.com/Red-Cadets/beard.git
47 | ```
48 | Перейдите в директорию:
49 | ```bash
50 | cd beard
51 | ```
52 | Измените .env с вашими настройками:
53 | - `HOST` - IP адрес или домен, на котором будет развёрнуто приложение. Данный параметр необходим для настройки CORS. Пример: `http://8.8.8.8` или `http://example.com`
54 | - `SCOREBOARD` - Адрес скорборда. Пример: `http://6.0.0.1`
55 | - `TEAM` - Название команды либо IP команды, информацию о которой необходимо отображать. Пример: `Red Cadets` или `10.10.1.15`
56 | - `TYPE` - Тип скорборда. Пример: `forcad` или `hackerdom`
57 | - `BOT_URL` - Адрес бота (вебхук) для уведомления о событиях. Формат сообщения: ```{"message": "Здесь уведомление", "type": "markdown", "id": "parser"}```. Для простой интеграции бота используйте [курьера](https://github.com/Red-Cadets/courier). Формат сообщения:
58 | ```json
59 | {
60 | "message": "Сообщение уведомления",
61 | "type": "markdown",
62 | "id": "parser",
63 | "to": "id телеграм чата"
64 | }
65 | ```
66 | - `ROUND_TIME` - Время раунда в секундах. Например: `120`
67 | - `EXTEND_ROUND` - Количество раундов, на которое необходимо предсказать график очков всех команд. Предсказание происходит на основе очков крайних 5-ти раундов. Например: `10`
68 | - `MONGO_USER` - Пользователь БД. Например: `parser`
69 | - `MONGO_PASS` - Пароль пользователя БД. Например: `parser`
70 |
71 | Запустите docker-compose:
72 | ```bash
73 | docker-compose up -d
74 | ```
75 | и перейдите по ссылке
76 | ```bash
77 | http://127.0.0.1:65005/
78 | ```
79 |
80 | ## 🖼️ Галерея
81 |
82 | ||
83 | |:-------------------------:|
84 | ||
85 | |График очков всех команд на турнирной таблице|
86 | |
87 | |График потери флагов|
88 | ||
89 | |График получения флагов|
90 | ||
91 | |Оповещения в телеграм|
92 |
93 |
94 |
95 | # 🎪 Сообщество
96 |
97 | Вы можете оставить информацию о багах или улучшениях в виде ишьюсов.
98 | Мы также открыты к вашим пулл-реквестам и любым видам взаимодействия!
99 |
100 | Мы взаимодействуем через Telegram. [Нажмите здесь](https://t.me/redcadets_chat), чтобы вступить в наше сообщество!
101 |
102 | ## 📝 Планы на будущее
103 |
104 | > Открыты к новым идеям!
105 |
106 | # ❤️ Благодарности
107 |
108 | Парсер турнирной таблицы Hackerdom основан на https://github.com/Vindori/hackerdom-board-parser
109 |
--------------------------------------------------------------------------------
/front/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = process.env.NODE_ENV === 'testing'
15 | ? require('../config/test.env')
16 | : require('../config/prod.env')
17 |
18 | const webpackConfig = merge(baseWebpackConfig, {
19 | module: {
20 | rules: utils.styleLoaders({
21 | sourceMap: config.build.productionSourceMap,
22 | extract: true,
23 | usePostCSS: true
24 | })
25 | },
26 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
31 | },
32 | plugins: [
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | new UglifyJsPlugin({
38 | uglifyOptions: {
39 | compress: {
40 | warnings: false
41 | }
42 | },
43 | sourceMap: config.build.productionSourceMap,
44 | parallel: true
45 | }),
46 | // extract css into its own file
47 | new ExtractTextPlugin({
48 | filename: utils.assetsPath('css/[name].[contenthash].css'),
49 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
53 | allChunks: true,
54 | }),
55 | // Compress extracted CSS. We are using this plugin so that possible
56 | // duplicated CSS from different components can be deduped.
57 | new OptimizeCSSPlugin({
58 | cssProcessorOptions: config.build.productionSourceMap
59 | ? { safe: true, map: { inline: false } }
60 | : { safe: true }
61 | }),
62 | // generate dist index.html with correct asset hash for caching.
63 | // you can customize output by editing /index.html
64 | // see https://github.com/ampedandwired/html-webpack-plugin
65 | new HtmlWebpackPlugin({
66 | filename: process.env.NODE_ENV === 'testing'
67 | ? 'index.html'
68 | : config.build.index,
69 | template: 'index.html',
70 | inject: true,
71 | minify: {
72 | removeComments: true,
73 | collapseWhitespace: true,
74 | removeAttributeQuotes: true
75 | // more options:
76 | // https://github.com/kangax/html-minifier#options-quick-reference
77 | },
78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
79 | chunksSortMode: 'dependency'
80 | }),
81 | // keep module.id stable when vendor modules does not change
82 | new webpack.HashedModuleIdsPlugin(),
83 | // enable scope hoisting
84 | new webpack.optimize.ModuleConcatenationPlugin(),
85 | // split vendor js into its own file
86 | new webpack.optimize.CommonsChunkPlugin({
87 | name: 'vendor',
88 | minChunks (module) {
89 | // any required modules inside node_modules are extracted to vendor
90 | return (
91 | module.resource &&
92 | /\.js$/.test(module.resource) &&
93 | module.resource.indexOf(
94 | path.join(__dirname, '../node_modules')
95 | ) === 0
96 | )
97 | }
98 | }),
99 | // extract webpack runtime and module manifest to its own file in order to
100 | // prevent vendor hash from being updated whenever app bundle is updated
101 | new webpack.optimize.CommonsChunkPlugin({
102 | name: 'manifest',
103 | minChunks: Infinity
104 | }),
105 | // This instance extracts shared chunks from code splitted chunks and bundles them
106 | // in a separate chunk, similar to the vendor chunk
107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
108 | new webpack.optimize.CommonsChunkPlugin({
109 | name: 'app',
110 | async: 'vendor-async',
111 | children: true,
112 | minChunks: 3
113 | }),
114 |
115 | // copy custom static assets
116 | new CopyWebpackPlugin([
117 | {
118 | from: path.resolve(__dirname, '../static'),
119 | to: config.build.assetsSubDirectory,
120 | ignore: ['.*']
121 | }
122 | ])
123 | ]
124 | })
125 |
126 | if (config.build.productionGzip) {
127 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
128 |
129 | webpackConfig.plugins.push(
130 | new CompressionWebpackPlugin({
131 | asset: '[path].gz[query]',
132 | algorithm: 'gzip',
133 | test: new RegExp(
134 | '\\.(' +
135 | config.build.productionGzipExtensions.join('|') +
136 | ')$'
137 | ),
138 | threshold: 10240,
139 | minRatio: 0.8
140 | })
141 | )
142 | }
143 |
144 | if (config.build.bundleAnalyzerReport) {
145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
147 | }
148 |
149 | module.exports = webpackConfig
150 |
--------------------------------------------------------------------------------
/parser/board_parser.py:
--------------------------------------------------------------------------------
1 | import re
2 | import requests
3 | import coloredlogs
4 | import logging
5 |
6 | from bs4 import BeautifulSoup
7 | from requests.exceptions import ConnectionError
8 |
9 | coloredlogs.install()
10 |
11 |
12 | def prettify(text):
13 | return text.strip().replace('\n', '').replace(' ', '')
14 |
15 |
16 | def remove_trash(text):
17 | trash = re.findall('\([+|-][0-9]+\)', text)
18 | for t in trash:
19 | text = text.replace(t, '')
20 | return text
21 |
22 |
23 | def return_status(status_code):
24 | if status_code == "status-110" or status_code == "status_" or 'rgb(255, 255, 0)' in status_code:
25 | return "CHECK FAILED"
26 | elif status_code == 'status-101' or status_code == "status_up" or 'rgb(125, 252, 116)' in status_code:
27 | return "UP"
28 | elif status_code == 'status-102' or status_code == "status_corrupt" or 'rgb(81, 145, 255)' in status_code:
29 | return "CORRUPT"
30 | elif status_code == 'status-103' or status_code == "status_mumble" or 'rgb(255, 145, 20)' in status_code:
31 | return "MUMBLE"
32 | elif status_code == 'status-104' or status_code == "status_down" or 'rgb(255, 91, 91);' in status_code:
33 | return "DOWN"
34 | else:
35 | return "ERROR, UNKNOWN STATUS CODE"
36 |
37 |
38 | def get_services(driver, soup):
39 | if driver:
40 | services = []
41 | # * Ждать, пока страница прогрузится
42 | while not services:
43 | services = driver.find_elements_by_class_name("service-name")
44 | return [service.strip() for service in services[0].text.split('\n')]
45 | elif soup:
46 | return [service.text for service in soup.findAll('th', 'service_name')]
47 | else:
48 | logging.critical("Something went wrong [get-services]")
49 | exit(1)
50 |
51 |
52 | def init_patch(driver, soup):
53 | patch = {}
54 | services = get_services(driver, soup)
55 | for service in services:
56 | patch[service] = True
57 | return patch
58 |
59 |
60 | def get_current_round(driver, soup):
61 | if soup:
62 | current_round = re.findall(
63 | '[0-9]+', soup.find('div', attrs={'id': 'round'}).text.strip())[0]
64 | return int(current_round)
65 | elif driver:
66 | current_round = 0
67 | # * Ждать пока страница прогрузится
68 | while int(current_round) == 0:
69 | current_round = driver.re(r'Round: (\d+)')[0]
70 | return int(current_round)
71 | else:
72 | logging.critical("Something went wrong [get-current-round]")
73 | exit(1)
74 |
75 |
76 | def get_teams_info(driver, soup):
77 | if driver:
78 | teams_info = []
79 | # * Ждать, пока страница прогрузится
80 | while not teams_info:
81 | teams_info = driver.find_elements_by_class_name("row")[1:]
82 | teams = [{
83 | 'name': team.text.split('\n')[1].strip(),
84 | 'place': int(team.text.split('\n')[0].strip()),
85 | 'score': float(team.text.split('\n')[3].strip()),
86 | 'ip': team.text.split('\n')[2].strip(),
87 | 'services': get_services_info_forcad(team, driver)
88 | } for team in teams_info]
89 | return teams
90 | elif soup:
91 | services = get_services(driver, soup)
92 | teams_html = soup.findAll('tr', attrs={'class': 'team'})[1:]
93 | teams = \
94 | [
95 | {
96 | 'name': team.find('div', 'team_name').text.strip(),
97 | 'place': int(remove_trash(team.find('td', 'place').text.strip())),
98 | 'score': float(team.find('td', 'score').text.strip()),
99 | 'ip': team.find('div', 'team_server').text.strip(),
100 | 'services': get_services_info_hackerdom(team, services)
101 | }
102 | for team in teams_html
103 | ]
104 | return teams
105 | else:
106 | logging.critical("Something went wrong [get-teams-info]")
107 | exit(1)
108 |
109 |
110 | def get_services_info_forcad(team, driver):
111 | services = team.text.split('\n')[4:]
112 | statuses = get_status_info(driver, team)
113 | services_name = get_services(driver, None)
114 | services_info = [{
115 | "name": services_name[i % len(services)],
116 | "title": None,
117 | "sla": float(services[i * 3].split(':')[1].strip()[:-1]),
118 | "flag_points": float(services[i * 3 + 1].split(':')[1].strip()),
119 | "flags": {'got': int(services[i * 3 + 2].split('/')[0][1:]), 'lost':int(services[i * 3 + 2].split('/')[1][1:])},
120 | "status": statuses[services_name[i % len(services)]]
121 | } for i, _ in enumerate(services[::3])]
122 | return services_info
123 |
124 |
125 | def get_services_info_hackerdom(soup, services):
126 | services_html = soup.findAll('td', 'team_service')
127 | services_status = [service['class'][1] for service in services_html]
128 | services_title = [service['title'].strip() if service['title']
129 | else None for service in services_html]
130 | services_sla = [prettify(sla.find('div', 'param_value').text)
131 | for sla in soup.findAll('div', 'sla')]
132 | services_fp = [prettify(fp.find('div', 'param_value').text)
133 | for fp in soup.findAll('div', 'fp')]
134 | services_flags = [prettify(flags.find('div', 'param_value').text).split(
135 | '/') for flags in soup.findAll('div', 'flags')]
136 | services_flags = [[abs(int(i)) for i in flags] if len(flags) == 2 else [
137 | int(flags[0]), 0] for flags in services_flags]
138 | services_flags = [{'got': flags[0], 'lost': flags[1]}
139 | for flags in services_flags]
140 | services_info = \
141 | [
142 | {
143 | 'name': services[i % len(services)],
144 | 'status': service_info[0],
145 | 'sla': float(service_info[1][:-1]),
146 | 'flag_points': float(service_info[2]),
147 | 'flags': service_info[3],
148 | 'title': service_info[4]
149 | }
150 | for i, service_info in enumerate(zip(services_status, services_sla, services_fp, services_flags, services_title))
151 | ]
152 | return services_info
153 |
154 |
155 | def get_status_info(driver, team):
156 | services_name = get_services(driver, None)
157 | services = team.find_elements_by_class_name("service-cell")
158 | return {services_name[number % len(services)]: return_status(code.get_attribute("style")) for number, code in enumerate(services)}
159 |
160 |
161 | def get_soup_by_address(address):
162 | if not address.startswith('http'):
163 | address = 'http://' + address
164 | try:
165 | html = requests.get(address)
166 | return BeautifulSoup(html.text, 'html.parser')
167 |
168 | except ConnectionError:
169 | logging.critical("Connection Error: " + address)
170 | exit(1)
171 |
172 | except Exception as e:
173 | logging.critical("Something went wrong: ".format(e))
174 | exit(1)
175 |
--------------------------------------------------------------------------------
/parser/game.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | import coloredlogs
4 | import logging
5 |
6 | import board_parser
7 |
8 | coloredlogs.install()
9 |
10 | URL = os.getenv('BOT_URL', 'https://bot.example.com/key')
11 |
12 |
13 | class AD(object):
14 | def __init__(self, ip, driver, scoreboard, teamname):
15 | global info, delta, soup
16 | delta = []
17 | self.ip = ip
18 | self.teamname = teamname
19 | self.scoreboard = scoreboard
20 |
21 | if not driver:
22 | soup = board_parser.get_soup_by_address(self.scoreboard)
23 | else:
24 | soup = None
25 |
26 | self.patch = board_parser.init_patch(driver, soup)
27 |
28 | self.round = board_parser.get_current_round(driver, soup)
29 | info = board_parser.get_teams_info(driver, soup)
30 |
31 | def get_info_by_ip(self, ip):
32 | for team in info:
33 | if team['ip'] == ip:
34 | return team
35 | logging.critical("Нет команды с IP {ip}".format(ip=ip))
36 |
37 | def get_info_by_name(self, name):
38 | for team in info:
39 | if team['name'] == name:
40 | return team
41 | logging.critical("Нет команды с названием {}".format(name))
42 |
43 | def dump(self):
44 | return info
45 |
46 | def get_delta_by_ip(self, ip):
47 | for team in delta:
48 | if team['ip'] == ip:
49 | return team
50 | logging.critical("Нет команды с IP {ip}".format(ip=ip))
51 |
52 | def get_delta_by_name(self, name):
53 | for team in delta:
54 | if team['name'] == name:
55 | return team
56 | logging.critical("Нет команды с названием {name}".format(name=name))
57 |
58 | def refresh(self, driver):
59 | global info
60 | if driver:
61 | driver.get(self.scoreboard)
62 | current_round = board_parser.get_current_round(driver, None)
63 | if self.round != current_round:
64 | new_info = board_parser.get_teams_info(driver, None)
65 | self.round = current_round
66 | self.__recalculate_delta(new_info)
67 | info = new_info
68 | return True
69 | else:
70 | return False
71 | else:
72 | new_soup = board_parser.get_soup_by_address(self.scoreboard)
73 | new_info = board_parser.get_teams_info(driver, new_soup)
74 | current_round = board_parser.get_current_round(driver, new_soup)
75 | if self.round != current_round:
76 | self.round = current_round
77 | self.__recalculate_delta(new_info)
78 | info = new_info.copy()
79 | return True
80 | else:
81 | return False
82 |
83 | def __recalculate_delta(self, new_info):
84 | global delta
85 | delta = []
86 | for team_new in new_info:
87 | if self.ip:
88 | team_old = self.get_info_by_ip(team_new['ip'])
89 | else:
90 | team_old = self.get_info_by_name(team_new['name'])
91 | delta_services = {}
92 | for service_new, service_old in zip(team_new['services'], team_old['services']):
93 | name = service_new['name']
94 | team_got_new_flags = service_new['flags']['got'] - \
95 | service_old['flags']['got']
96 | team_lost_new_flags = service_new['flags']['lost'] - \
97 | service_old['flags']['lost']
98 | delta_services[name] = {
99 | 'status': service_new['status'],
100 | 'title': service_new['title'],
101 | 'flags': {
102 | 'got': team_got_new_flags,
103 | 'lost': team_lost_new_flags
104 | }}
105 |
106 | if team_new['ip'] == self.ip or team_new['name'] == self.teamname:
107 | # * Уведомление о положении команды в рейтинге
108 | if team_old['place'] > team_new['place']:
109 | telegram_alert(
110 | 'PLACE', status='up', place_old=team_old['place'], place_new=team_new['place'])
111 | elif team_old['place'] < team_new['place']:
112 | telegram_alert(
113 | 'PLACE', status='down', place_old=team_old['place'], place_new=team_new['place'])
114 |
115 | # * Уведомление о статусе сервисов
116 | for service_new, service_old in zip(team_new['services'], team_old['services']):
117 | name = service_new['name']
118 | title = service_new['title']
119 |
120 | new_status = service_new['status']
121 | old_status = service_old['status']
122 |
123 | if soup:
124 | new_status = board_parser.return_status(new_status)
125 | old_status = board_parser.return_status(old_status)
126 |
127 | # * Если сервис не взлетел или не изменил своего состояния
128 | if new_status == old_status and new_status != 'UP':
129 | telegram_alert(
130 | 'STATUS',
131 | status='not change',
132 | now=new_status,
133 | service=name,
134 | title=title
135 | )
136 |
137 | # * Если сервис взлетел или изменил состояние на UP
138 | if old_status != 'UP' and new_status == 'UP':
139 | telegram_alert(
140 | 'STATUS',
141 | status='up',
142 | now=new_status,
143 | service=name
144 | )
145 |
146 | # * Если сервис не взлетел или изменил состояние не на UP
147 | if old_status != new_status and new_status != 'UP':
148 | telegram_alert(
149 | 'STATUS',
150 | status='down',
151 | now=new_status,
152 | service=name,
153 | title=title
154 | )
155 |
156 | # * Уведомление о первой крови
157 | if int(delta_services[name]['flags']['lost']) != 0 and self.patch[name] == True:
158 | self.patch[name] = False
159 | telegram_alert('FB', service=name)
160 |
161 | # * Уведомление о прекращении потери флагов
162 | elif int(delta_services[name]['flags']['lost']) == 0 and self.patch[name] == False and new_status == 'UP':
163 | self.patch[name] = True
164 | telegram_alert('PATCH', service=name)
165 |
166 | delta.append({
167 | 'round': self.round,
168 | 'name': team_new['name'],
169 | 'ip': team_new['ip'],
170 | 'place': team_new['place'],
171 | 'score': round(team_new['score'] - team_old['score'], 2),
172 | 'services': delta_services
173 | })
174 |
175 |
176 | def telegram_alert(alert_type, **args):
177 | if alert_type == 'PLACE':
178 | requests.post(URL, json={
179 | "message": "{} с *{}* на *{}* место".format('⬇ Наша команда спустилась' if args['status'] == 'down' else '⬆ Наша команда поднялась', args['place_old'], args['place_new']),
180 | "type": "markdown",
181 | "id": "parser"
182 | })
183 | if alert_type == 'STATUS':
184 | if args['now'] == 'UP':
185 | simb = '*🟢 {} 🟢*\n'
186 | elif args['now'] == 'DOWN':
187 | simb = '*🔴 {} 🔴*\n'
188 | elif args['now'] == 'CORRUPT':
189 | simb = '*🔵 {} 🔵*\n'
190 | elif args['now'] == 'MUMBLE':
191 | simb = '*🟠 {} 🟠*\n'
192 | elif args['now'] == 'CHECK FAILED':
193 | simb = '*🟡 {} 🟡*\n'
194 |
195 | if args['status'] == 'down':
196 | otvet = "Сервису поплохело"
197 | if args['title']:
198 | otvet += "\n{}".format(args['title'])
199 | elif args['status'] == 'up':
200 | otvet = "Сервис снова жив"
201 | elif args['status'] == 'not change':
202 | otvet = "Сервису ВСЁ ЕЩЁ плохо"
203 | if args['title']:
204 | otvet += "\n *Check Error:* {}".format(args['title'])
205 |
206 | requests.post(URL, json={
207 | "message": "{} {}".format(simb.format(args['service']), otvet),
208 | "type": "markdown",
209 | "id": "parser"
210 | })
211 | if alert_type == 'FB':
212 | requests.post(URL, json={
213 | "message": "🩸 Мы теряем флаги на сервисе *{}*".format(args['service']),
214 | "type": "markdown",
215 | "id": "parser"
216 | })
217 | if alert_type == 'PATCH':
218 | requests.post(URL, json={
219 | "message": "💎 Мы запатчили сервис *{}*".format(args['service']),
220 | "type": "markdown",
221 | "id": "parser"
222 | })
223 |
--------------------------------------------------------------------------------
/front/src/components/Echart.vue:
--------------------------------------------------------------------------------
1 |
2 |