├── src
├── core
│ ├── tox.ini
│ ├── LICENSE.md
│ ├── MANIFEST.in
│ ├── gunicorn.conf.py
│ ├── app.py
│ ├── migrations
│ │ ├── README
│ │ ├── script.py.mako
│ │ └── alembic.ini
│ ├── .gitignore
│ ├── core
│ │ ├── __main__.py
│ │ ├── auth
│ │ │ ├── __init__.py
│ │ │ ├── openid_authenticator.py
│ │ │ ├── test_authenticator.py
│ │ │ ├── database_authenticator.py
│ │ │ └── base_authenticator.py
│ │ ├── managers
│ │ │ ├── __init__.py
│ │ │ ├── asset_manager.py
│ │ │ ├── input_validators.py
│ │ │ ├── sse.py
│ │ │ ├── db_manager.py
│ │ │ ├── api_manager.py
│ │ │ └── data_manager.py
│ │ ├── api
│ │ │ ├── isalive.py
│ │ │ ├── sse.py
│ │ │ ├── __init__.py
│ │ │ ├── user.py
│ │ │ ├── auth.py
│ │ │ └── publishers.py
│ │ ├── model
│ │ │ ├── __init__.py
│ │ │ ├── token_blacklist.py
│ │ │ ├── address.py
│ │ │ ├── permission.py
│ │ │ └── base_model.py
│ │ ├── static
│ │ │ └── presenter_templates
│ │ │ │ ├── pdf_template.html
│ │ │ │ ├── cert_at_daily_report.html
│ │ │ │ ├── text_template.txt
│ │ │ │ └── misp_template.json
│ │ └── __init__.py
│ ├── run.py
│ ├── tests
│ │ ├── .env
│ │ ├── test_settings.py
│ │ ├── functional
│ │ │ ├── word_list_test_data.csv
│ │ │ ├── word_list_test_data_2.csv
│ │ │ ├── helpers.py
│ │ │ └── osint_sources_test_data_v2.json
│ │ └── test_api.py
│ ├── README.md
│ ├── client_secrets.json
│ └── pyproject.toml
├── worker
│ ├── tox.ini
│ ├── LICENSE.md
│ ├── worker
│ │ ├── misc
│ │ │ ├── __init__.py
│ │ │ ├── misc_tasks.py
│ │ │ └── wordlist_update.py
│ │ ├── __main__.py
│ │ ├── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_bots.py
│ │ │ └── polyfuzz_load_testing.py
│ │ ├── presenters
│ │ │ ├── json_presenter.py
│ │ │ ├── html_presenter.py
│ │ │ ├── text_presenter.py
│ │ │ ├── __init__.py
│ │ │ ├── pdf_presenter.py
│ │ │ └── base_presenter.py
│ │ ├── collectors
│ │ │ └── __init__.py
│ │ ├── publishers
│ │ │ ├── base_publisher.py
│ │ │ ├── __init__.py
│ │ │ ├── misp_publisher.py
│ │ │ ├── twitter_publisher.py
│ │ │ ├── wordpress_publisher.py
│ │ │ └── publisher_tasks.py
│ │ ├── __init__.py
│ │ ├── bots
│ │ │ ├── __init__.py
│ │ │ ├── tagging_bot.py
│ │ │ ├── base_bot.py
│ │ │ ├── story_bot.py
│ │ │ ├── grouping_bot.py
│ │ │ ├── bot_tasks.py
│ │ │ ├── ioc_bot.py
│ │ │ ├── wordlist_updater_bot.py
│ │ │ └── analyst_bot.py
│ │ ├── tasks.py
│ │ ├── config.py
│ │ └── scheduler.py
│ ├── .gitignore
│ ├── README.md
│ ├── start_dev_worker.py
│ └── pyproject.toml
├── gui
│ ├── public
│ │ ├── config.json
│ │ └── favicon.ico
│ ├── .browserslistrc
│ ├── babel.config.js
│ ├── .eslintignore
│ ├── .prettierrc.json
│ ├── src
│ │ ├── assets
│ │ │ ├── cvssicons.png
│ │ │ ├── watermark.png
│ │ │ ├── bg-filter-panel.png
│ │ │ ├── menu_btn.svg
│ │ │ ├── common.css
│ │ │ ├── themes.js
│ │ │ └── centralize.css
│ │ ├── api
│ │ │ ├── auth.js
│ │ │ ├── user.js
│ │ │ ├── dashboard.js
│ │ │ ├── publish.js
│ │ │ ├── assets.js
│ │ │ └── analyze.js
│ │ ├── styles
│ │ │ ├── settings.scss
│ │ │ └── awake.scss
│ │ ├── i18n
│ │ │ ├── messages.js
│ │ │ ├── i18n.js
│ │ │ ├── datetimeformat.js
│ │ │ └── sk
│ │ │ │ └── messages.js
│ │ ├── views
│ │ │ ├── admin
│ │ │ │ └── OpenAPI.vue
│ │ │ ├── nav
│ │ │ │ └── UserNav.vue
│ │ │ └── users
│ │ │ │ ├── ClusterView.vue
│ │ │ │ ├── NewsItemView.vue
│ │ │ │ ├── settings
│ │ │ │ └── UserView.vue
│ │ │ │ ├── AssetGroupView.vue
│ │ │ │ ├── ProductView.vue
│ │ │ │ ├── StoryView.vue
│ │ │ │ ├── AssetView.vue
│ │ │ │ └── ReportView.vue
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── DeprecationWarning.vue
│ │ │ │ ├── DashBoardCard.vue
│ │ │ │ ├── DateInput.vue
│ │ │ │ ├── CodeEditor.vue
│ │ │ │ ├── TrendingCard.vue
│ │ │ │ ├── Notification.vue
│ │ │ │ ├── TagTable.vue
│ │ │ │ ├── IconNavigation.vue
│ │ │ │ └── FormParameters.vue
│ │ │ ├── layouts
│ │ │ │ ├── ViewLayout.vue
│ │ │ │ └── DialogLayout.vue
│ │ │ ├── assess
│ │ │ │ ├── filter
│ │ │ │ │ ├── dateChips.vue
│ │ │ │ │ ├── filterSelectList.vue
│ │ │ │ │ └── filterSortList.vue
│ │ │ │ └── card
│ │ │ │ │ ├── SummarizedContent.vue
│ │ │ │ │ └── votes.vue
│ │ │ ├── analyze
│ │ │ │ ├── AnalyzeFilter.vue
│ │ │ │ └── AttributeCVSS.vue
│ │ │ ├── config
│ │ │ │ └── ImportExport.vue
│ │ │ ├── popups
│ │ │ │ ├── PopupDeleteItem.vue
│ │ │ │ └── PopupShareItems.vue
│ │ │ └── UserMenu.vue
│ │ ├── services
│ │ │ ├── config.js
│ │ │ └── api_service.js
│ │ ├── stores
│ │ │ ├── AssetsStore.js
│ │ │ ├── PublishStore.js
│ │ │ ├── DashboardStore.js
│ │ │ ├── MainStore.js
│ │ │ └── AnalyzeStore.js
│ │ ├── utils
│ │ │ ├── sse.js
│ │ │ └── ListFilters.js
│ │ ├── main.js
│ │ ├── App.vue
│ │ └── plugins
│ │ │ └── vuetify.js
│ ├── .env
│ ├── .gitignore
│ ├── .editorconfig
│ ├── jsconfig.json
│ ├── extras
│ │ ├── patch_config_json.js
│ │ ├── remove_version.js
│ │ ├── default.conf.template
│ │ ├── update_version.js
│ │ └── 30-update_config_from_env.sh
│ ├── index.html
│ ├── .eslintrc.json
│ ├── vite.config.js
│ ├── README.md
│ └── package.json
└── .gitignore
├── tox.ini
├── .dockerignore
├── pyproject.toml
├── resources
└── images
│ ├── logo.png
│ └── screenshot.png
├── doc
└── 2023_IKTSichKonf_AWAKE_v3.pdf
├── .devcontainer
├── entrypoint.sh
├── nginx.dev.conf
├── start-dev.sh
├── devcontainer.json
└── Containerfile
├── README.md
├── docker
├── env.sample
├── git_info.sh
├── Dockerfile.gui
├── Dockerfile.worker
├── Dockerfile.core
└── gunicorn.conf.py
├── .pre-commit-config.yaml
├── .gitignore
└── .github
└── workflows
├── linting.yaml
├── docker_build_gui.yaml
└── docker_build_worker.yaml
/src/core/tox.ini:
--------------------------------------------------------------------------------
1 | ../../tox.ini
--------------------------------------------------------------------------------
/src/worker/tox.ini:
--------------------------------------------------------------------------------
1 | ../../tox.ini
--------------------------------------------------------------------------------
/src/core/LICENSE.md:
--------------------------------------------------------------------------------
1 | ../../LICENSE.md
--------------------------------------------------------------------------------
/src/core/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft core/static
--------------------------------------------------------------------------------
/src/worker/LICENSE.md:
--------------------------------------------------------------------------------
1 | ../../LICENSE.md
--------------------------------------------------------------------------------
/src/worker/worker/misc/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 142
3 |
--------------------------------------------------------------------------------
/src/core/gunicorn.conf.py:
--------------------------------------------------------------------------------
1 | ../../docker/gunicorn.conf.py
--------------------------------------------------------------------------------
/src/core/app.py:
--------------------------------------------------------------------------------
1 | from core.__init__ import create_app # noqa
2 |
--------------------------------------------------------------------------------
/src/core/migrations/README:
--------------------------------------------------------------------------------
1 | Single-database configuration for Flask.
2 |
--------------------------------------------------------------------------------
/src/gui/public/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "TARANIS_CORE_API": "/api"
3 | }
4 |
--------------------------------------------------------------------------------
/src/worker/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | .envrc
3 | __pycache__/
4 | *.pyc
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | venv
3 | dist
4 | **/venv
5 | **/node_modules
6 |
--------------------------------------------------------------------------------
/src/gui/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 | not ie 11
5 |
--------------------------------------------------------------------------------
/src/core/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | worker_init.flag
3 | .envrc
4 | __pycache__/
5 | *.pyc
6 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 142
3 | target-version = ["py38","py310"]
4 |
--------------------------------------------------------------------------------
/src/gui/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/resources/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/resources/images/logo.png
--------------------------------------------------------------------------------
/src/core/core/__main__.py:
--------------------------------------------------------------------------------
1 | from core.__init__ import create_app
2 |
3 | app = create_app()
4 | app.run()
5 |
--------------------------------------------------------------------------------
/src/gui/.eslintignore:
--------------------------------------------------------------------------------
1 | src/assets/cvss31_mixin.js
2 | src/assets/keyboard_mixin.js
3 | src/i18n/en/messages.js
--------------------------------------------------------------------------------
/src/gui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none"
5 | }
--------------------------------------------------------------------------------
/src/gui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/src/gui/public/favicon.ico
--------------------------------------------------------------------------------
/src/core/core/auth/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["database_authenticator", "openid_authenticator", "test_authenticator"]
2 |
--------------------------------------------------------------------------------
/src/worker/worker/__main__.py:
--------------------------------------------------------------------------------
1 | from worker import CeleryWorker
2 |
3 | cw = CeleryWorker()
4 | celery = cw.app
5 |
--------------------------------------------------------------------------------
/resources/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/resources/images/screenshot.png
--------------------------------------------------------------------------------
/src/gui/src/assets/cvssicons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/src/gui/src/assets/cvssicons.png
--------------------------------------------------------------------------------
/src/gui/src/assets/watermark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/src/gui/src/assets/watermark.png
--------------------------------------------------------------------------------
/doc/2023_IKTSichKonf_AWAKE_v3.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/doc/2023_IKTSichKonf_AWAKE_v3.pdf
--------------------------------------------------------------------------------
/src/gui/.env:
--------------------------------------------------------------------------------
1 | #VITE_TARANIS_CORE_API="http://localhost:5000/api/v1"
2 | #VITE_TARANIS_LOGIN_URL =
3 | #VITE_TARANIS_LOGOUT_URL =
4 |
--------------------------------------------------------------------------------
/src/gui/src/assets/bg-filter-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ait-cs-IaaS/Taranis-NG/HEAD/src/gui/src/assets/bg-filter-panel.png
--------------------------------------------------------------------------------
/src/gui/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Sentry Auth Token
3 | .env.sentry-build-plugin
4 | /public/config.json.local
5 | /public/config.local.json
6 | .npm
7 | *.tgz
8 |
--------------------------------------------------------------------------------
/.devcontainer/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sudo service postgresql start
4 | sudo service rabbitmq-server start
5 | sudo service nginx start
6 |
7 | $@
8 |
--------------------------------------------------------------------------------
/src/core/run.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | from core import create_app
4 |
5 | app = create_app()
6 |
7 | if __name__ == "__main__":
8 | app.run()
9 |
--------------------------------------------------------------------------------
/src/gui/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/src/worker/worker/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture(scope="session")
5 | def celery_config():
6 | return {"broker_url": "memory://"}
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Taranis AI
2 |
3 | > [!WARNING]
4 | > This repository is abandoned. Work for Taranis AI has been moved to: [https://github.com/taranis-ai/taranis-ai](https://github.com/taranis-ai/taranis-ai)
5 |
6 |
--------------------------------------------------------------------------------
/src/core/tests/.env:
--------------------------------------------------------------------------------
1 | API_KEY=test_key
2 | SECRET_KEY=test_key
3 | FLASK_DEBUG=1
4 | DEBUG=true
5 | SQLALCHEMY_DATABASE_URI="sqlite:///:memory:"
6 | QUEUE_BROKER_URL="memory://localhost"
7 | PRE_SEED_PASSWORD_USER="test"
8 |
--------------------------------------------------------------------------------
/src/core/core/managers/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "api_manager",
3 | "asset_manager",
4 | "auth_manager",
5 | "db_manager",
6 | "log_manager",
7 | "sse_manager",
8 | "queue_manager",
9 | ]
10 |
--------------------------------------------------------------------------------
/src/worker/worker/tests/test_bots.py:
--------------------------------------------------------------------------------
1 | def test_initalize_bots():
2 | import worker.bots as bots
3 |
4 | bots.AnalystBot(),
5 | bots.GroupingBot(),
6 | bots.NLPBot(),
7 | bots.TaggingBot(),
8 | bots.WordlistUpdaterBot(),
9 |
--------------------------------------------------------------------------------
/src/gui/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"],
6 | }
7 | },
8 | "exclude": ["node_modules", "dist"],
9 | "extensions": [
10 | ".vue"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/gui/src/api/auth.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function authenticate(userData) {
4 | return apiService.post('/auth/login', userData)
5 | }
6 |
7 | export function refresh() {
8 | return apiService.get('/auth/refresh')
9 | }
10 |
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | .devcontainer/
3 | .gitconfig
4 | .gnupg/
5 | .local/
6 | .ssh/
7 | .vscode-server-insiders/
8 | .bash_history
9 | .psql_history
10 | .python_history
11 | .env.sentry-build-plugin
12 | .npm
13 | venv/
14 | worker_init.flag
15 | .envrc
16 | __pycache__/
17 | *.pyc
18 |
--------------------------------------------------------------------------------
/src/core/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | def test_api_key():
2 | from core.config import Config
3 |
4 | api_key = Config.API_KEY
5 | assert api_key == "test_key"
6 |
7 |
8 | def test_flask_secret_key(app):
9 | secret_key = app.config.get("SECRET_KEY", None)
10 | assert secret_key == "test_key"
11 |
--------------------------------------------------------------------------------
/src/gui/src/styles/settings.scss:
--------------------------------------------------------------------------------
1 | @use 'vuetify/_settings' with (
2 | $typography: (
3 | 'caption': (
4 | 'letter-spacing': normal
5 | )
6 | ),
7 | $button-text-transform: none,
8 | $button-text-letter-spacing: normal,
9 | $button-font-size: 1rem,
10 | $icon-margin-end: 100px
11 | );
12 |
--------------------------------------------------------------------------------
/src/gui/src/i18n/messages.js:
--------------------------------------------------------------------------------
1 | import { messages_en } from '@/i18n/en/messages'
2 | import { messages_sk } from '@/i18n/sk/messages'
3 | import { messages_de } from '@/i18n/de/messages'
4 |
5 | export const messages = {
6 | de: messages_de,
7 | en: messages_en,
8 | 'en-GB': messages_en,
9 | sk: messages_sk
10 | }
11 |
--------------------------------------------------------------------------------
/docker/env.sample:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME=taranis
2 | TARANIS_TAG=awake
3 | DOCKER_IMAGE_NAMESPACE=ghcr.io/ait-cs-iaas
4 |
5 | TARANIS_PORT=8080
6 |
7 | # Default passwords. CHANGE THESE FOR PRODUCTION!
8 | POSTGRES_PASSWORD=supersecret
9 | JWT_SECRET_KEY=supersecret
10 | API_KEY=supersecret
11 | RABBITMQ_PASSWORD=supersecret
12 |
--------------------------------------------------------------------------------
/src/core/core/api/isalive.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Resource, Api
2 |
3 | from core.managers.auth_manager import no_auth
4 |
5 |
6 | class IsAlive(Resource):
7 | @no_auth
8 | def get(self):
9 | return {"isalive": True}
10 |
11 |
12 | def initialize(api: Api):
13 | api.add_resource(IsAlive, "/isalive", "/")
14 |
--------------------------------------------------------------------------------
/src/worker/README.md:
--------------------------------------------------------------------------------
1 | # Taranis Worker
2 |
3 | This worker is a celery worker
4 |
5 |
6 | ## Install
7 |
8 | ```bash
9 | pip install -e .
10 | ```
11 |
12 | ## Usage
13 |
14 | As a worker
15 |
16 | ```bash
17 | celery -A worker worker
18 | ```
19 |
20 | as a scheduler
21 |
22 | ```bash
23 | celery -A worker beat
24 | ```
25 |
--------------------------------------------------------------------------------
/.devcontainer/nginx.dev.conf:
--------------------------------------------------------------------------------
1 |
2 | server {
3 | listen 9000;
4 | server_name _;
5 |
6 | location / {
7 | proxy_pass http://localhost:8081;
8 | }
9 |
10 | location /api {
11 | proxy_pass http://localhost:5000/api/v1;
12 | }
13 |
14 | location /api/v1 {
15 | proxy_pass http://localhost:5000/api/v1;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/core/core/api/sse.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Resource, Api
2 | from flask_jwt_extended import jwt_required
3 |
4 | from core.managers.sse_manager import sse_manager
5 |
6 |
7 | class SSE(Resource):
8 | @jwt_required()
9 | def get(self):
10 | return sse_manager.sse.listeners
11 |
12 |
13 | def initialize(api: Api):
14 | api.add_resource(SSE, "/sse")
15 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/json_presenter.py:
--------------------------------------------------------------------------------
1 | from .base_presenter import BasePresenter
2 |
3 |
4 | class JSONPresenter(BasePresenter):
5 | type = "JSON_PRESENTER"
6 | name = "JSON Presenter"
7 | description = "Presenter for generating JSON files"
8 |
9 | def generate(self, product, template) -> dict[str, bytes | str]:
10 | return super().generate(product, template)
11 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/html_presenter.py:
--------------------------------------------------------------------------------
1 | from .base_presenter import BasePresenter
2 |
3 |
4 | class HTMLPresenter(BasePresenter):
5 | type = "HTML_PRESENTER"
6 | name = "HTML Presenter"
7 | description = "Presenter for generating html documents"
8 |
9 | def generate(self, product, template) -> dict[str, bytes | str]:
10 | return super().generate(product, template)
11 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/text_presenter.py:
--------------------------------------------------------------------------------
1 | from .base_presenter import BasePresenter
2 |
3 |
4 | class TextPresenter(BasePresenter):
5 | type = "TEXT_PRESENTER"
6 | name = "TEXT Presenter"
7 | description = "Presenter for generating text documents"
8 |
9 | def generate(self, product, template) -> dict[str, bytes | str]:
10 | return super().generate(product, template)
11 |
--------------------------------------------------------------------------------
/src/worker/worker/collectors/__init__.py:
--------------------------------------------------------------------------------
1 | from worker.collectors.email_collector import EmailCollector
2 | from worker.collectors.rss_collector import RSSCollector
3 | from worker.collectors.simple_web_collector import SimpleWebCollector
4 |
5 | # from worker.collectors.web_collector import WebCollector
6 |
7 | __all__ = [
8 | "EmailCollector",
9 | "RSSCollector",
10 | "SimpleWebCollector",
11 | ]
12 |
--------------------------------------------------------------------------------
/src/gui/src/i18n/i18n.js:
--------------------------------------------------------------------------------
1 | import { createI18n } from 'vue-i18n'
2 | import { messages } from '@/i18n/messages'
3 | import { datetimeFormats } from '@/i18n/datetimeformat'
4 |
5 | export const i18n = createI18n({
6 | legacy: false,
7 | locale:
8 | typeof import.meta.env.VITE_TARANIS_LOCALE === 'undefined'
9 | ? 'en'
10 | : import.meta.env.VITE_TARANIS_LOCALE,
11 | fallbackLocale: 'en',
12 | messages,
13 | datetimeFormats
14 | })
15 |
--------------------------------------------------------------------------------
/docker/git_info.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | git_branch=$(git rev-parse --abbrev-ref HEAD)
4 | git_tag=$(git describe --tags --exact-match 2>/dev/null)
5 | if [ $? -eq 0 ]; then
6 | git_reference_name="TAG"
7 | git_reference_value="$git_tag"
8 | else
9 | git_reference_name="HEAD"
10 | git_reference_value="$(git rev-parse --short HEAD)"
11 | fi
12 |
13 | printf '{"branch":"%s","%s":"%s"}\n' "$git_branch" "$git_reference_name" "$git_reference_value"
14 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/base_publisher.py:
--------------------------------------------------------------------------------
1 | from worker.log import logger
2 |
3 |
4 | class BasePublisher:
5 | type = "BASE_PUBLISHER"
6 | name = "Base Publisher"
7 | description = "Base abstract type for all publishers"
8 |
9 | parameters = []
10 |
11 | def publish(self, publisher_input):
12 | pass
13 |
14 | def print_exception(self, error):
15 | logger.log_debug_trace(f"Publishing Failed: {self.type} - {error}")
16 |
--------------------------------------------------------------------------------
/src/gui/extras/patch_config_json.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | const configPath = path.join(__dirname, '../dist/config.json')
5 | const configJson = require(configPath)
6 | configJson.BUILD_DATE = new Date().toISOString()
7 |
8 | const gitInfo = process.env.GIT_INFO
9 | if (gitInfo && gitInfo.trim() !== '') {
10 | configJson.GIT_INFO = JSON.parse(gitInfo)
11 | }
12 |
13 | fs.writeFileSync(configPath, JSON.stringify(configJson, null, 2))
14 |
--------------------------------------------------------------------------------
/src/gui/src/api/user.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function getProfile() {
4 | return apiService.get('/users/profile')
5 | }
6 |
7 | export function updateProfile(data) {
8 | return apiService.put('/users/profile', data)
9 | }
10 |
11 | export function getAllUserProductTypes() {
12 | return apiService.get('/users/my-product-types')
13 | }
14 |
15 | export function getAllUserPublishersPresets() {
16 | return apiService.get('/users/my-publisher-presets')
17 | }
18 |
--------------------------------------------------------------------------------
/src/core/core/model/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = [
2 | "user",
3 | "role",
4 | "permission",
5 | "news_item",
6 | "osint_source",
7 | "parameter",
8 | "parameter_value",
9 | "report_item",
10 | "attribute",
11 | "report_item_type",
12 | "address",
13 | "organization",
14 | "acl_entry",
15 | "product_type",
16 | "product",
17 | "publisher_preset",
18 | "word_list",
19 | "asset",
20 | "bot",
21 | "token_blacklist",
22 | "worker",
23 | ]
24 |
--------------------------------------------------------------------------------
/src/core/core/api/__init__.py:
--------------------------------------------------------------------------------
1 | from core.api import (
2 | analyze,
3 | assess,
4 | assets,
5 | auth,
6 | bots,
7 | config,
8 | dashboard,
9 | isalive,
10 | publish,
11 | publishers,
12 | user,
13 | worker,
14 | )
15 |
16 | __all__ = [
17 | "analyze",
18 | "assess",
19 | "assets",
20 | "auth",
21 | "bots",
22 | "config",
23 | "dashboard",
24 | "isalive",
25 | "publish",
26 | "publishers",
27 | "user",
28 | "worker",
29 | ]
30 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/__init__.py:
--------------------------------------------------------------------------------
1 | from worker.publishers.email_publisher import EMAILPublisher
2 | from worker.publishers.twitter_publisher import TWITTERPublisher
3 | from worker.publishers.wordpress_publisher import WORDPRESSPublisher
4 | from worker.publishers.base_publisher import BasePublisher
5 | from worker.publishers.ftp_publisher import FTPPublisher
6 |
7 | __all__ = [
8 | "EMAILPublisher",
9 | "TWITTERPublisher",
10 | "WORDPRESSPublisher",
11 | "BasePublisher",
12 | "FTPPublisher",
13 | ]
14 |
--------------------------------------------------------------------------------
/.devcontainer/start-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | tmux new-session -d -s dev
5 |
6 | # Start the Flask server in the first window (0th window is automatically created with new-session)
7 | tmux send-keys -t dev:0 'cd core; flask run' C-m
8 |
9 | # Create a new window for the npm dev server and run it
10 | tmux new-window -t dev:1
11 | tmux send-keys -t dev:1 'cd gui; npm run dev' C-m
12 |
13 | # Create an empty third window
14 | tmux new-window -t dev:2
15 |
16 | # Finally, attach to the tmux session
17 | tmux attach -t dev
18 |
--------------------------------------------------------------------------------
/src/gui/src/views/admin/OpenAPI.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
25 |
--------------------------------------------------------------------------------
/src/gui/extras/remove_version.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | // Path to the package.json file
5 | const packagePath = path.join(__dirname, '..', 'package.json')
6 |
7 | // Read the package.json file
8 | const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
9 |
10 | // Delete the version field
11 | delete packageData.version
12 |
13 | // Write the updated package.json data back to the file
14 | fs.writeFileSync(
15 | packagePath,
16 | JSON.stringify(packageData, null, 2) + '\n',
17 | 'utf8'
18 | )
19 |
--------------------------------------------------------------------------------
/src/worker/worker/misc/misc_tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 |
3 | from worker.core_api import CoreApi
4 | from worker.misc.wordlist_update import update_wordlist
5 |
6 |
7 | @shared_task(time_limit=10, name="cleanup_token_blacklist")
8 | def cleanup_token_blacklist():
9 | core_api = CoreApi()
10 | core_api.cleanup_token_blacklist()
11 | return "Token blacklist cleaned up"
12 |
13 |
14 | @shared_task(time_limit=30, name="gather_word_list")
15 | def gather_word_list(word_list_id: int):
16 | return update_wordlist(word_list_id)
17 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/__init__.py:
--------------------------------------------------------------------------------
1 | from worker.presenters.html_presenter import HTMLPresenter
2 | from worker.presenters.json_presenter import JSONPresenter
3 | from worker.presenters.pdf_presenter import PDFPresenter
4 | from worker.presenters.base_presenter import BasePresenter
5 | from worker.presenters.text_presenter import TextPresenter
6 |
7 | # from worker.collectors.web_collector import WebCollector
8 |
9 | __all__ = [
10 | "HTMLPresenter",
11 | "JSONPresenter",
12 | "PDFPresenter",
13 | "BasePresenter",
14 | "TextPresenter",
15 | ]
16 |
--------------------------------------------------------------------------------
/src/core/README.md:
--------------------------------------------------------------------------------
1 | # TaranisNG Core
2 |
3 | The Tarins-NG Core could be called the "backend" of TaranisNG.
4 |
5 | It offers API Endpoints to the Frontend, is the sole persistence layer (via SQLAlchemy) and schedules tasks via Celery.
6 |
7 | Furthermore it offers SSE to the Frontend and acts as celery scheduler backend.
8 |
9 |
10 | ## Requirements
11 |
12 | * Python version 3.11 or greater.
13 | * SQLite or PostgreSQL
14 | * [Optional] RabbitMQ
15 |
16 |
17 | ## Setup
18 |
19 | `pip install -e .[dev]`
20 |
21 | ## Run
22 |
23 | `flask run`
24 |
25 | or
26 |
27 | `gunicorn`
28 |
--------------------------------------------------------------------------------
/src/gui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Taranis AI
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/DeprecationWarning.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This item component is deprecated and will be removed in the future.
5 |
6 |
7 |
8 |
9 |
26 |
--------------------------------------------------------------------------------
/src/gui/src/views/nav/UserNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
--------------------------------------------------------------------------------
/src/worker/worker/__init__.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 |
3 | from worker.config import Config
4 | from worker.core_api import CoreApi
5 |
6 | from worker.tasks import setup_tasks
7 |
8 |
9 | class CeleryWorker:
10 | def __init__(self):
11 | celery_config = Config.CELERY
12 |
13 | self.app = Celery(__name__)
14 | self.app.config_from_object(celery_config)
15 | self.app.set_default()
16 | self.core_api = CoreApi()
17 | setup_tasks(self.app)
18 |
19 |
20 | if __name__ == "worker":
21 | cw = CeleryWorker()
22 | celery = cw.app
23 |
--------------------------------------------------------------------------------
/src/gui/src/api/dashboard.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function getDashboardData() {
4 | return apiService.get('/dashboard')
5 | }
6 |
7 | export function getTrendingClusters() {
8 | return apiService.get('/dashboard/trending-clusters')
9 | }
10 |
11 | export function getCluster(tag_type, filter_data) {
12 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
13 | return apiService.get(`/dashboard/cluster/${tag_type}?${filter}`)
14 | }
15 |
16 | export function getCoreBuildInfo() {
17 | return apiService.get('/dashboard/build-info')
18 | }
19 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 |
2 | repos:
3 | - repo: https://github.com/psf/black
4 | rev: 23.3.0
5 | hooks:
6 | - id: black
7 | files: ^(src/core|src/worker)
8 |
9 | - repo: local
10 | hooks:
11 | - id: eslint
12 | name: lint
13 | language: node
14 | entry: npm --prefix src/gui run lint_and_format
15 | files: ^src/gui
16 | types_or: [javascript, jsx, ts, tsx, vue]
17 | pass_filenames: false
18 |
19 | - repo: https://github.com/pre-commit/pre-commit-hooks
20 | rev: v4.4.0
21 | hooks:
22 | - id: trailing-whitespace
23 |
--------------------------------------------------------------------------------
/src/core/tests/functional/word_list_test_data.csv:
--------------------------------------------------------------------------------
1 | value;category;description
2 | 2fa;Security;Two-factor Authentication
3 | adversary;Security;Attacker/hacker
4 | adware;Security;Ads and pop-up windows
5 | ai;MISC;Artificial Intelligence
6 | apt;Security;Advanced Persistent Threat
7 | atp;Security;Advanced Threat Protection
8 | attack;Security;
9 | authentication;MISC;
10 | av;Security;Anti-virus
11 | backdoor;Security;
12 | ransomware;Security;
13 | rdp;MISC;Remote Desktop Protocol
14 | virus;Security;
15 | vpn;MISC;Virtual Private Network
16 | vulnerability;Security;
17 | zero-day;Security;
18 | zombie;Uncategorized;
--------------------------------------------------------------------------------
/src/core/tests/functional/word_list_test_data_2.csv:
--------------------------------------------------------------------------------
1 | value,category,description
2 | 2fa,Security,Two-factor Authentication
3 | adversary,Security,Attacker/hacker
4 | adware,Security,Ads and pop-up windows
5 | ai,MISC,Artificial Intelligence
6 | apt,Security,Advanced Persistent Threat
7 | atp,Security,Advanced Threat Protection
8 | attack,Security,
9 | authentication,MISC,
10 | av,Security,Anti-virus
11 | backdoor,Security,
12 | ransomware,Security,
13 | rdp,MISC,Remote Desktop Protocol
14 | virus,Security,
15 | vpn,MISC,Virtual Private Network
16 | vulnerability,Security,
17 | zero-day,Security,
18 | zombie,Uncategorized,
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # built and cached files
2 | venv/
3 | .envrc
4 | __pycache__/
5 | *.pyc
6 | node_modules/
7 | dist/
8 | build/
9 | *.egg-info/
10 | .installed.cfg
11 | *.egg
12 | .pytest_cache/
13 | celerybeat-schedule
14 | .hypothesis
15 | git_info.json
16 | taranis_data/
17 |
18 | # temporary files of editors
19 | .DS_Store
20 | .*.sw?
21 | .~
22 | *.*~
23 | *.bak
24 |
25 | # sensitive data not to be commited
26 | .env.local
27 | docker/.env
28 | .env.*.local
29 | src/.env
30 | *.pem
31 | *.key
32 | *.log
33 |
34 | # settings of editors
35 | .idea/
36 | .vscode/
37 | *.suo
38 | *.ntvs*
39 | *.njsproj
40 | *.sln
41 | *.sw?
42 |
--------------------------------------------------------------------------------
/src/core/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/__init__.py:
--------------------------------------------------------------------------------
1 | from worker.bots.analyst_bot import AnalystBot
2 | from worker.bots.grouping_bot import GroupingBot
3 | from worker.bots.tagging_bot import TaggingBot
4 | from worker.bots.wordlist_updater_bot import WordlistUpdaterBot
5 | from worker.bots.wordlist_bot import WordlistBot
6 | from worker.bots.nlp_bot import NLPBot
7 | from worker.bots.story_bot import StoryBot
8 | from worker.bots.ioc_bot import IOCBot
9 | from worker.bots.summary_bot import SummaryBot
10 |
11 | __all__ = ["AnalystBot", "GroupingBot", "NLPBot", "TaggingBot", "WordlistBot", "WordlistUpdaterBot", "StoryBot", "IOCBot", "SummaryBot"]
12 |
--------------------------------------------------------------------------------
/src/gui/src/components/layouts/ViewLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/ClusterView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
--------------------------------------------------------------------------------
/src/gui/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "extends": [
6 | "plugin:vue/vue3-recommended",
7 | "eslint:recommended",
8 | "@vue/prettier",
9 | "@vue/eslint-config-prettier",
10 | "@vue/typescript/recommended"
11 | ],
12 | "parserOptions": {
13 | "ecmaVersion": 2020
14 | },
15 | "rules": {
16 | "camelcase": "off",
17 | "quotes": [
18 | 2,
19 | "single",
20 | {
21 | "avoidEscape": true
22 | }
23 | ],
24 | "space-before-function-paren": 0,
25 | "vue/valid-v-slot": ["error", {
26 | "allowModifiers": true
27 | }]
28 | },
29 | "root": true
30 | }
--------------------------------------------------------------------------------
/src/gui/src/services/config.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export async function getLocalConfig() {
4 | try {
5 | const configJson =
6 | typeof import.meta.env.VITE_TARANIS_CONFIG_JSON === 'undefined'
7 | ? '/config.json'
8 | : import.meta.env.VITE_TARANIS_CONFIG_JSON
9 | const response = await axios.get(configJson, { baseURL: '' })
10 | return response.data
11 | } catch (error) {
12 | if (error.response && error.response.status === 404) {
13 | console.error('Config file not found')
14 | } else {
15 | console.error('Error parsing config file:', error.message)
16 | }
17 | return null
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/core/client_secrets.json:
--------------------------------------------------------------------------------
1 | {
2 | "web": {
3 | "issuer": "http://127.0.0.1:8081/auth/realms/taranisng",
4 | "auth_uri": "http://127.0.0.1:8081/auth/realms/taranisng/protocol/openid-connect/auth",
5 | "client_id": "taranisng",
6 | "client_secret": "30444bfc-f068-4300-b80c-4f5e125ae295",
7 | "redirect_uris": [
8 | "http://127.0.0.1:5000/*"
9 | ],
10 | "userinfo_uri": "http://127.0.0.1:8081/auth/realms/taranisng/protocol/openid-connect/userinfo",
11 | "token_uri": "http://127.0.0.1:8081/auth/realms/taranisng/protocol/openid-connect/token",
12 | "token_introspection_uri": "http://127.0.0.1:8081/auth/realms/taranisng/protocol/openid-connect/token/introspect"
13 | }
14 | }
--------------------------------------------------------------------------------
/src/gui/src/i18n/datetimeformat.js:
--------------------------------------------------------------------------------
1 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
2 |
3 | const defaultShort = {
4 | year: 'numeric',
5 | month: '2-digit',
6 | day: '2-digit'
7 | }
8 |
9 | const defaultLong = {
10 | year: 'numeric',
11 | month: 'short',
12 | day: '2-digit',
13 | hour: '2-digit',
14 | minute: '2-digit',
15 | hour12: false
16 | }
17 |
18 | export const datetimeFormats = {
19 | en: {
20 | short: {
21 | year: 'numeric',
22 | month: 'long',
23 | day: '2-digit'
24 | },
25 | long: defaultLong
26 | },
27 | de: {
28 | short: defaultShort,
29 | long: defaultLong
30 | },
31 | sk: {
32 | short: defaultShort,
33 | long: defaultLong
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/worker/worker/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 |
3 | from worker.config import Config
4 | from worker.presenters.presenter_tasks import PresenterTask
5 | from worker.collectors.collector_tasks import CollectorTask
6 | from worker.bots.bot_tasks import BotTask
7 | from worker.publishers.publisher_tasks import PublisherTask
8 | import worker.misc.misc_tasks # noqa: F401
9 |
10 |
11 | def setup_tasks(app: Celery):
12 | if "Bots" in Config.WORKER_TYPES:
13 | app.register_task(BotTask())
14 | if "Collectors" in Config.WORKER_TYPES:
15 | app.register_task(CollectorTask())
16 | if "Presenters" in Config.WORKER_TYPES:
17 | app.register_task(PresenterTask())
18 | if "Publishers" in Config.WORKER_TYPES:
19 | app.register_task(PublisherTask())
20 |
--------------------------------------------------------------------------------
/src/core/core/managers/asset_manager.py:
--------------------------------------------------------------------------------
1 | from core.managers.db_manager import db
2 | from core.model.asset import Asset
3 | from core.model.report_item import ReportItem
4 |
5 |
6 | def remove_vulnerability(report_item_id):
7 | Asset.remove_vulnerability(report_item_id)
8 | db.session.commit()
9 |
10 |
11 | def report_item_changed(report_item: "ReportItem"):
12 | if not report_item:
13 | return
14 | if not report_item.completed:
15 | return
16 | cpes = [cpe.value for cpe in report_item.report_item_cpes]
17 | assets = Asset.get_by_cpe(cpes)
18 |
19 | notification_groups = set()
20 |
21 | for asset in assets:
22 | asset.add_vulnerability(report_item)
23 | notification_groups.add(asset.asset_group)
24 |
25 | db.session.commit()
26 |
--------------------------------------------------------------------------------
/docker/Dockerfile.gui:
--------------------------------------------------------------------------------
1 | FROM node:lts as build-stage
2 |
3 | RUN npm install -g npm@latest
4 |
5 | WORKDIR /app
6 | COPY ./src/gui/package*.json ./
7 | RUN npm ci
8 | COPY ./src/gui/ /app/
9 | ARG git_info
10 | ENV GIT_INFO=$git_info
11 |
12 | ENV NODE_OPTIONS=--openssl-legacy-provider
13 | RUN npm run build
14 |
15 | RUN gzip -k -9 /app/dist/assets/*
16 |
17 | FROM nginx:mainline
18 |
19 | RUN mkdir -p /etc/nginx/templates
20 | ENV TARANIS_CORE_UPSTREAM=core
21 |
22 | COPY ./src/gui/extras/30-update_config_from_env.sh /docker-entrypoint.d/
23 | COPY ./src/gui/extras/default.conf.template /etc/nginx/templates/default.conf.template
24 |
25 | RUN envsubst '${TARANIS_CORE_UPSTREAM}' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf
26 |
27 | COPY --from=build-stage /app/dist /usr/share/nginx/html
28 |
--------------------------------------------------------------------------------
/.github/workflows/linting.yaml:
--------------------------------------------------------------------------------
1 | name: linting
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 |
7 | jobs:
8 | black:
9 | name: run black linter
10 | runs-on: ubuntu-22.04
11 | strategy:
12 | matrix:
13 | python-version: ['3.11']
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: lint with black
17 | uses: rickstaa/action-black@v1
18 | with:
19 | black_args: "src --check"
20 |
21 | eslint:
22 | name: eslint
23 | runs-on: ubuntu-22.04
24 | steps:
25 | - uses: actions/checkout@v3
26 | - uses: actions/setup-node@v3
27 | with:
28 | node-version: 18
29 | - name: install dependencies
30 | run: npm --prefix src/gui ci
31 | - name: lint with eslint
32 | run: npm --prefix src/gui run lint_and_format
33 |
--------------------------------------------------------------------------------
/src/core/core/managers/input_validators.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from flask import request, abort
3 |
4 |
5 | def validate_id(id_name):
6 | def decorator(f):
7 | @wraps(f)
8 | def decorated_function(*args, **kwargs):
9 | id_value = kwargs.get(id_name, request.args.get(id_name, None))
10 | if id_value is None:
11 | abort(400, description=f"No {id_name} provided")
12 | try:
13 | id_value = int(id_value)
14 | except ValueError:
15 | abort(400, description=f"{id_name} must be an integer")
16 | if id_value < 0 or id_value > 2**31 - 1:
17 | abort(400, description=f"{id_name} must be a positive int32")
18 | return f(*args, **kwargs)
19 |
20 | return decorated_function
21 |
22 | return decorator
23 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/pdf_presenter.py:
--------------------------------------------------------------------------------
1 | from weasyprint import HTML
2 |
3 | from .base_presenter import BasePresenter
4 |
5 |
6 | class PDFPresenter(BasePresenter):
7 | type = "PDF_PRESENTER"
8 | name = "PDF Presenter"
9 | description = "Presenter for generating PDF documents"
10 |
11 | def generate(self, product, template) -> dict[str, str | bytes]:
12 | try:
13 | output_text = super().generate(product, template)
14 |
15 | html = HTML(string=output_text["data"])
16 |
17 | if data := html.write_pdf(target=None):
18 | return {"mime_type": "application/pdf", "data": data}
19 |
20 | return {"error": "Could not generate PDF"}
21 |
22 | except Exception as error:
23 | BasePresenter.print_exception(self, error)
24 | return {"error": str(error)}
25 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/NewsItemView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
--------------------------------------------------------------------------------
/src/gui/src/stores/AssetsStore.js:
--------------------------------------------------------------------------------
1 | import { getAllAssetGroups, getAllAssets } from '@/api/assets'
2 | import { defineStore } from 'pinia'
3 | import { useFilterStore } from './FilterStore'
4 |
5 | export const useAssetsStore = defineStore('assets', {
6 | state: () => ({
7 | asset_groups: [],
8 | assets: []
9 | }),
10 | actions: {
11 | loadAssetGroups(data) {
12 | return getAllAssetGroups(data).then((response) => {
13 | this.asset_groups = response.data
14 | })
15 | },
16 | loadAssets(data) {
17 | return getAllAssets(data).then((response) => {
18 | this.assets = response.data
19 | })
20 | },
21 | updateFilteredAssets() {
22 | const filter = useFilterStore()
23 | return getAllAssets(filter.assetFilter).then((response) => {
24 | this.assets = response.data
25 | })
26 | }
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/DashBoardCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ linkText }}
8 |
9 | {{ linkText }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
32 |
--------------------------------------------------------------------------------
/src/core/core/model/token_blacklist.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from core.managers.db_manager import db
4 | from core.model.base_model import BaseModel
5 |
6 |
7 | class TokenBlacklist(BaseModel):
8 | id = db.Column(db.Integer, primary_key=True)
9 | token = db.Column(db.String(), nullable=False)
10 | created = db.Column(db.DateTime, default=datetime.now)
11 |
12 | def __init__(self, token):
13 | self.id = None
14 | self.token = token
15 |
16 | @classmethod
17 | def add(cls, token):
18 | db.session.add(TokenBlacklist(token))
19 | db.session.commit()
20 |
21 | @classmethod
22 | def invalid(cls, token):
23 | return db.session.query(db.exists().where(TokenBlacklist.token == token)).scalar()
24 |
25 | @classmethod
26 | def delete_older(cls, check_time):
27 | db.session.query(TokenBlacklist).filter(TokenBlacklist.created < check_time).delete()
28 | db.session.commit()
29 |
--------------------------------------------------------------------------------
/src/worker/worker/presenters/base_presenter.py:
--------------------------------------------------------------------------------
1 | from worker.log import logger
2 | import jinja2
3 | import datetime
4 |
5 |
6 | class BasePresenter:
7 | type = "BASE_PRESENTER"
8 | name = "Base Presenter"
9 | description = "Base abstract type for all presenters"
10 |
11 | def print_exception(self, error):
12 | logger.log_debug_trace("[{0}] {1}".format(self.name, error))
13 |
14 | def generate(self, product, template) -> dict[str, bytes | str]:
15 | try:
16 | env = jinja2.Environment()
17 | tmpl = env.from_string(template)
18 | product["current_date"] = datetime.datetime.now().strftime("%Y-%m-%d")
19 |
20 | output_text = tmpl.render(data=product).encode("utf-8")
21 |
22 | return {"mime_type": product["mime_type"], "data": output_text}
23 | except Exception as error:
24 | BasePresenter.print_exception(self, error)
25 | return {"error": str(error)}
26 |
--------------------------------------------------------------------------------
/src/gui/src/components/layouts/DialogLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | mdi-close-circle
9 |
10 | {{ title }}
11 |
12 |
13 |
14 |
15 | mdi-content-save
16 | {{ $t('osint_source.add') }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/DateInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 | style="width: 290px;" >
20 |
21 |
22 |
27 |
28 |
29 |
30 |
45 |
--------------------------------------------------------------------------------
/src/core/core/auth/openid_authenticator.py:
--------------------------------------------------------------------------------
1 | from authlib.integrations.flask_client import OAuth
2 |
3 | from core.auth.base_authenticator import BaseAuthenticator
4 |
5 |
6 | oauth = OAuth()
7 |
8 |
9 | class OpenIDAuthenticator(BaseAuthenticator):
10 | @staticmethod
11 | def initialize(app):
12 | oauth.init_app(app)
13 | oauth.register(
14 | name="openid",
15 | client_id=app.config.get("OPENID_CLIENT_ID"),
16 | client_secret=app.config.get("OPENID_CLIENT_SECRET"),
17 | server_metadata_url=app.config.get("OPENID_METADATA_URL"),
18 | )
19 |
20 | def authenticate(self, credentials):
21 | oauth.openid.authorize_access_token()
22 | user = oauth.openid.userinfo()
23 | if user.preferred_username:
24 | print(user.preferred_username)
25 | return BaseAuthenticator.generate_jwt(user)
26 |
27 | return BaseAuthenticator.generate_error()
28 |
29 | @staticmethod
30 | def logout(token):
31 | BaseAuthenticator.logout(token)
32 |
--------------------------------------------------------------------------------
/src/gui/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import vuetify from 'vite-plugin-vuetify'
4 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
5 | import path from 'path'
6 |
7 | export default defineConfig({
8 | server: {
9 | host: '0.0.0.0',
10 | port: 8081
11 | },
12 | resolve: {
13 | alias: {
14 | '@': path.resolve(__dirname, './src')
15 | }
16 | },
17 | plugins: [
18 | vue(),
19 | VueI18nPlugin(),
20 | vuetify({
21 | autoImport: true,
22 | styles: { configFile: './src/styles/settings.scss' }
23 | })
24 | ],
25 | build: {
26 | sourcemap: true,
27 | rollupOptions: {
28 | // https://rollupjs.org/guide/en/#outputmanualchunks
29 | output: {
30 | manualChunks: {
31 | vue: ['vue', 'vue-router'],
32 | vuetify: ['vuetify', 'vuetify/components', 'vuetify/directives'],
33 | materialdesignicons: ['@mdi/font/css/materialdesignicons.css']
34 | }
35 | }
36 | }
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/src/core/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic,flask_migrate
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [logger_flask_migrate]
38 | level = INFO
39 | handlers =
40 | qualname = flask_migrate
41 |
42 | [handler_console]
43 | class = StreamHandler
44 | args = (sys.stderr,)
45 | level = NOTSET
46 | formatter = generic
47 |
48 | [formatter_generic]
49 | format = %(levelname)-5.5s [%(name)s] %(message)s
50 | datefmt = %H:%M:%S
51 |
--------------------------------------------------------------------------------
/src/core/tests/test_api.py:
--------------------------------------------------------------------------------
1 | def test_is_alive(client):
2 | response = client.get("/api/isalive")
3 | assert b'"isalive": true' in response.data
4 |
5 |
6 | def test_is_alive_fail(client):
7 | response = client.get("/api/isalive")
8 | assert b'"isalive": false' not in response.data
9 |
10 |
11 | def test_auth_login_fail(client):
12 | response = client.get("/api/auth/login")
13 | assert response.status_code == 401
14 |
15 |
16 | def test_auth_logout(client):
17 | response = client.get("/api/auth/logout")
18 | assert response.status_code == 200
19 |
20 |
21 | def test_auth_login(client):
22 | body = {"username": "user", "password": "test"}
23 | response = client.post("/api/auth/login", json=body)
24 | assert response.status_code == 200
25 |
26 |
27 | def test_access_token(access_token):
28 | assert access_token is not None
29 |
30 |
31 | def test_user_profile(client, auth_header):
32 | response = client.get("/api/users/profile", headers=auth_header)
33 | assert response.json
34 | assert response.data
35 | assert response.status_code == 200
36 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Taranis Dev Container",
3 | "build": { "dockerfile": "Containerfile" },
4 | "workspaceMount": "source=${localWorkspaceFolder}/src,target=/app,type=bind,consistency=cached",
5 | "workspaceFolder": "/app",
6 | "runArgs": [
7 | "--userns=keep-id:uid=1000,gid=1000"
8 | ],
9 | "overrideCommand": true,
10 | "init": true,
11 | "customizations": {
12 | "vscode": {
13 | "extensions": [
14 | "ms-python.python",
15 | "ms-python.black-formatter",
16 | "eamodio.gitlens",
17 | "ms-python.vscode-pylance",
18 | "sourcery.sourcery",
19 | "rangav.vscode-thunder-client",
20 | "vue.volar",
21 | "esbenp.prettier-vscode"
22 | ],
23 | "settings": {
24 | "terminal.integrated.shell.linux": "/bin/bash"
25 | }
26 | }
27 | },
28 | "forwardPorts": [5000, 5432, 5672, 15672, 8081, 9000],
29 | "postCreateCommand": "/usr/local/bin/entrypoint.sh && pip install -e core[dev] && /usr/local/bin/start-dev.sh"
30 | }
31 |
--------------------------------------------------------------------------------
/src/worker/worker/tests/polyfuzz_load_testing.py:
--------------------------------------------------------------------------------
1 | import random
2 | from typing import Dict, List
3 | import string
4 | from worker.bots.nlp_bot import NLPBot
5 |
6 | nlpbot: NLPBot
7 |
8 |
9 | def generate_all_keywords(n: int = 60000) -> Dict[str, Dict[str, List[str]]]:
10 | all_keywords = {}
11 | for _ in range(n):
12 | word_length = random.randint(4, 7) # Generate a random length between 4 and 7
13 | word = "".join(random.choices(string.ascii_uppercase, k=word_length)) # Generate a random string
14 |
15 | sub_forms_length = random.randint(0, 2) # Generate a random length between 0 and 2 for sub_forms
16 | sub_forms = [f"{word}Sub{str(j)}" for j in range(sub_forms_length)]
17 |
18 | all_keywords[word] = {"name:": word, "tag_type": "MISC", "sub_forms": sub_forms}
19 |
20 | return all_keywords
21 |
22 |
23 | # Generate current_keywords as a random subset of all_keywords
24 | def generate_current_keywords(all_keywords: Dict[str, Dict[str, List[str]]], m: int = 25) -> Dict[str, Dict[str, List[str]]]:
25 | return dict(random.sample(list(all_keywords.items()), m))
26 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/settings/UserView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ username }}
8 |
9 | User Name
10 |
11 |
12 |
13 |
14 |
15 | {{ organizationName }}
16 |
17 | Organization
18 |
19 |
20 |
21 |
22 |
23 |
24 |
44 |
--------------------------------------------------------------------------------
/src/core/core/model/address.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from core.managers.db_manager import db
4 | from core.model.base_model import BaseModel
5 | from core.managers.log_manager import logger
6 |
7 |
8 | class Address(BaseModel):
9 | id = db.Column(db.Integer, primary_key=True)
10 | street = db.Column(db.String())
11 | city = db.Column(db.String())
12 | zip = db.Column(db.String())
13 | country = db.Column(db.String())
14 |
15 | def __init__(self, street=None, city=None, zip=None, country=None, id=None):
16 | self.id = id
17 | self.street = street
18 | self.city = city
19 | self.zip = zip
20 | self.country = country
21 |
22 | def update(self, new_item: dict[str, Any]) -> tuple[str, int]:
23 | for key, value in new_item.items():
24 | if hasattr(self, key) and key != "id":
25 | setattr(self, key, value)
26 |
27 | db.session.commit()
28 | return f"Successfully updated {self.id}", 200
29 |
30 | def update_from_address(self, new_address: "Address") -> tuple[str, int]:
31 | return self.update(new_address.to_dict())
32 |
--------------------------------------------------------------------------------
/src/gui/src/components/assess/filter/dateChips.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | day
10 | week
11 | month
12 |
13 |
14 |
15 |
44 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/misp_publisher.py:
--------------------------------------------------------------------------------
1 | import json
2 | from base64 import b64decode
3 | import urllib3
4 | from pymisp import ExpandedPyMISP, MISPEvent
5 |
6 | from .base_publisher import BasePublisher
7 |
8 |
9 | class MISPPublisher(BasePublisher):
10 | type = "MISP_PUBLISHER"
11 | name = "MISP Publisher"
12 | description = "Publisher for publishing in MISP"
13 |
14 | def publish(self, publisher_input):
15 | try:
16 | misp_url = publisher_input.parameter_values_map["MISP_URL"]
17 | misp_key = publisher_input.parameter_values_map["MISP_API_KEY"]
18 | misp_verifycert = False
19 |
20 | data = publisher_input.data[:]
21 | bytes_data = b64decode(data, validate=True)
22 |
23 | event_json = json.loads(bytes_data)
24 |
25 | urllib3.disable_warnings()
26 |
27 | misp = ExpandedPyMISP(misp_url, misp_key, misp_verifycert)
28 |
29 | event = MISPEvent()
30 | event.load(event_json)
31 | misp.add_event(event)
32 | except Exception as error:
33 | BasePublisher.print_exception(self, error)
34 |
--------------------------------------------------------------------------------
/src/gui/src/utils/sse.js:
--------------------------------------------------------------------------------
1 | export function connectSSE() {
2 | if (import.meta.env.VITE_TARANIS_CORE_SSE === undefined) {
3 | return
4 | }
5 | // this.$sse(
6 | // `${import.meta.env.VITE_TARANIS_CORE_SSE}?jwt=${this.$store.getters.getJWT}`,
7 | // { format: 'json' }
8 | // ).then((sse) => {
9 | // sse.subscribe('news-items-updated', (data) => {
10 | // this.$root.$emit('news-items-updated', data)
11 | // })
12 | // sse.subscribe('report-items-updated', (data) => {
13 | // this.$root.$emit('report-items-updated', data)
14 | // })
15 | // sse.subscribe('report-item-updated', (data) => {
16 | // this.$root.$emit('report-item-updated', data)
17 | // })
18 | // sse.subscribe('report-item-locked', (data) => {
19 | // this.$root.$emit('report-item-locked', data)
20 | // })
21 | // sse.subscribe('report-item-unlocked', (data) => {
22 | // this.$root.$emit('report-item-unlocked', data)
23 | // })
24 | // })
25 | }
26 |
27 | export function reconnectSSE() {
28 | if (this.sseConnection !== null) {
29 | this.sseConnection.close()
30 | this.sseConnection = null
31 | }
32 | this.connectSSE()
33 | }
34 |
--------------------------------------------------------------------------------
/src/core/core/static/presenter_templates/pdf_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PDF Template
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Sample Report - {{ data.current_date }}
14 |
15 |
16 |
Sample report
17 |
Report Items
18 | {% for report_item in data.report_items %}
19 |
{{ report_item.title }}
20 |
21 | {{ report_item.attributes }}
22 |
23 |
https://url/
24 |
25 |
26 |
Stories
27 | {% for story in report_item.news_item_aggregates %}
28 |
{{ story.title }}
29 |
30 | {{ story }}
31 |
32 |
https://url/
33 |
34 | {% endfor %}
35 | {% endfor %}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/core/core/static/presenter_templates/cert_at_daily_report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CERT Report
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Tageszusammenfassung - {{ data.current_date }}
14 |
15 |
16 |
End-of-Day report
17 |
Report Items
18 | {% for report_item in data.report_items %}
19 |
{{ report_item.title }}
20 |
21 | {{ report_item.attributes }}
22 |
23 |
https://url/
24 |
25 |
26 |
Stories
27 | {% for story in report_item.news_item_aggregates %}
28 |
{{ story.title }}
29 |
30 | {{ story }}
31 |
32 |
https://url/
33 |
34 | {% endfor %}
35 | {% endfor %}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/gui/src/components/analyze/AnalyzeFilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | mdi-magnify
16 |
17 |
18 |
19 | mdi-checkbox-multiple-marked
20 |
21 |
22 |
23 | mdi-dots-vertical
24 |
25 |
26 |
27 |
28 |
29 |
30 |
43 |
--------------------------------------------------------------------------------
/src/gui/extras/default.conf.template:
--------------------------------------------------------------------------------
1 | upstream core {
2 | server ${TARANIS_CORE_UPSTREAM};
3 | }
4 |
5 | server {
6 | listen 80;
7 | server_name _;
8 |
9 | root /usr/share/nginx/html;
10 | client_max_body_size 50m;
11 |
12 | location /assets {
13 | add_header Cache-Control "max-age=300, must-revalidate, s-maxage=300";
14 | }
15 |
16 | location / {
17 | expires -1;
18 | add_header Pragma "no-cache";
19 | add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
20 |
21 | try_files $uri /index.html =404;
22 | }
23 |
24 | proxy_set_header Host ${TARANIS_CORE_UPSTREAM};
25 | proxy_set_header X-Real-IP $remote_addr;
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27 | proxy_set_header X-Forwarded-Proto $scheme;
28 |
29 | location /api {
30 | proxy_pass http://core/api;
31 | }
32 |
33 | location /api/v1 {
34 | proxy_pass http://core/api;
35 | }
36 |
37 | location /sse {
38 | proxy_pass http://core/sse;
39 | }
40 |
41 | error_page 500 502 503 504 /50x.html;
42 | location = /50x.html {
43 | root /usr/share/nginx/html;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/core/core/auth/test_authenticator.py:
--------------------------------------------------------------------------------
1 | from core.managers.log_manager import logger
2 | from core.auth.base_authenticator import BaseAuthenticator
3 |
4 |
5 | users = {"user": "user", "admin": "admin"}
6 |
7 |
8 | class TestAuthenticator(BaseAuthenticator):
9 | def get_authenticator_name(self):
10 | return "TestAuthenticator"
11 |
12 | def __str__(self):
13 | return f"Authenticator: {self.get_authenticator_name()} Users: {users}"
14 |
15 | def get_required_credentials(self):
16 | return ["username", "password"]
17 |
18 | def authenticate(self, credentials: dict[str, str]) -> tuple[dict[str, str], int]:
19 | logger.log_debug(f"TEST AUTH with {credentials}")
20 | if credentials is None:
21 | return BaseAuthenticator.generate_error()
22 | if "username" not in credentials or "password" not in credentials:
23 | return BaseAuthenticator.generate_error()
24 | if users[credentials["username"]] == credentials["password"]:
25 | return BaseAuthenticator.generate_jwt(credentials["username"])
26 |
27 | logger.store_auth_error_activity(f"Authentication failed with credentials: {credentials}")
28 | return BaseAuthenticator.generate_error()
29 |
--------------------------------------------------------------------------------
/src/worker/start_dev_worker.py:
--------------------------------------------------------------------------------
1 | import time
2 | import subprocess
3 | from watchdog.observers import Observer
4 | from watchdog.events import FileSystemEventHandler
5 |
6 |
7 | class CeleryRestartHandler(FileSystemEventHandler):
8 | def __init__(self):
9 | self.celery_process = None
10 | self.restart_celery()
11 |
12 | def restart_celery(self):
13 | if self.celery_process:
14 | self.celery_process.terminate()
15 | self.celery_process.wait()
16 |
17 | self.celery_process = subprocess.Popen(["celery", "-A", "worker", "worker"])
18 |
19 | def on_modified(self, event):
20 | if event.is_directory:
21 | return
22 | self.restart_celery()
23 |
24 |
25 | if __name__ == "__main__":
26 | path = "." # Watch the current directory, modify if necessary
27 | event_handler = CeleryRestartHandler()
28 | observer = Observer()
29 | observer.schedule(event_handler, path, recursive=True)
30 | observer.start()
31 |
32 | try:
33 | while True:
34 | time.sleep(1)
35 | except KeyboardInterrupt:
36 | observer.stop()
37 | event_handler.celery_process.terminate()
38 | event_handler.celery_process.wait()
39 |
40 | observer.join()
41 |
--------------------------------------------------------------------------------
/docker/Dockerfile.worker:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim as builder
2 |
3 | WORKDIR /app/
4 |
5 | RUN apt-get update && apt-get upgrade -y && \
6 | apt-get install --no-install-recommends -y \
7 | build-essential \
8 | python3-dev \
9 | libglib2.0-0 \
10 | libpango-1.0-0 \
11 | libpangoft2-1.0-0 \
12 | git
13 |
14 | COPY ./src/worker/. /app/
15 |
16 | RUN python3 -m venv /app/.venv && \
17 | export PATH="/app/.venv/bin:$PATH" && \
18 | pip install --no-cache-dir --upgrade pip wheel && \
19 | pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu torch && \
20 | pip install --no-cache-dir -e /app/
21 |
22 | FROM python:3.11-slim
23 |
24 | WORKDIR /app
25 |
26 | RUN groupadd user && useradd --home-dir /app -g user user && chown -R user:user /app
27 | USER user
28 | COPY --from=builder --chown=user:user /app/.venv /app/.venv
29 | COPY --from=builder /usr/lib/x86_64-linux-gnu/* /usr/lib/x86_64-linux-gnu/
30 |
31 | COPY --chown=user:user ./src/worker/. /app/
32 | ENV PYTHONPATH=/app
33 | ENV PATH="/app/.venv/bin:$PATH"
34 |
35 |
36 | # bake spacy modell into Image
37 | RUN python -c 'from worker.bots.nlp_bot import NLPBot; n = NLPBot()'
38 |
39 | ENTRYPOINT [ "celery" ]
40 |
41 | CMD ["--app", "worker", "worker"]
42 |
--------------------------------------------------------------------------------
/src/core/core/managers/sse.py:
--------------------------------------------------------------------------------
1 | import queue
2 | import json
3 | from flask import g
4 |
5 |
6 | class SSE:
7 | def __init__(self):
8 | self.listeners = []
9 |
10 | def listen(self):
11 | q = queue.Queue(maxsize=20)
12 | self.listeners.append(q)
13 | return q
14 |
15 | def publish(self, data: str | dict, event=None):
16 | msg = self.format_sse(data, event)
17 | for i in reversed(range(len(self.listeners))):
18 | try:
19 | self.listeners[i].put_nowait(msg)
20 | except queue.Full:
21 | del self.listeners[i]
22 |
23 | def format_sse(self, data: str | dict, event=None) -> str:
24 | """Formats a string and an event name in order to follow the event stream convention.
25 | https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
26 | >>> format_sse(data=json.dumps({'abc': 123}), event='Jackson 5')
27 | 'event: Jackson 5\\ndata: {"abc": 123}\\n\\n'
28 | """
29 | if isinstance(data, dict):
30 | data = json.dumps(data)
31 | msg = f"data: {data}\n\n"
32 | if event is not None:
33 | msg = f"event: {event}\n{msg}"
34 | return msg
35 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/twitter_publisher.py:
--------------------------------------------------------------------------------
1 | from base64 import b64decode
2 | import tweepy
3 |
4 | from .base_publisher import BasePublisher
5 |
6 |
7 | class TWITTERPublisher(BasePublisher):
8 | type = "TWITTER_PUBLISHER"
9 | name = "Twitter Publisher"
10 | description = "Publisher for publishing to Twitter account"
11 |
12 | def publish(self, publisher_input):
13 | try:
14 | api_key = publisher_input.parameter_values_map["TWITTER_API_KEY"]
15 | api_key_secret = publisher_input.parameter_values_map["TWITTER_API_KEY_SECRET"]
16 | access_token = publisher_input.parameter_values_map["TWITTER_ACCESS_TOKEN"]
17 | access_token_secret = publisher_input.parameter_values_map["TWITTER_ACCESS_TOKEN_SECRET"]
18 |
19 | auth = tweepy.OAuthHandler(api_key, api_key_secret)
20 | auth.set_access_token(access_token, access_token_secret)
21 |
22 | api = tweepy.API(auth)
23 |
24 | data = publisher_input.data[:]
25 |
26 | bytes_data = b64decode(data, validate=True)
27 |
28 | if len(bytes_data) <= 240:
29 | api.update_status(bytes_data)
30 | except Exception as error:
31 | BasePublisher.print_exception(self, error)
32 |
--------------------------------------------------------------------------------
/src/gui/src/api/publish.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function getAllProducts(filter_data) {
4 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
5 | return apiService.get(`/publish/products?${filter}`)
6 | }
7 |
8 | export function createProduct(data) {
9 | return apiService.post('/publish/products', data)
10 | }
11 |
12 | export function updateProduct(data) {
13 | return apiService.put(`/publish/products/${data.id}`, data)
14 | }
15 |
16 | export function getProduct(product) {
17 | return apiService.get(`/publish/products/${product}`)
18 | }
19 |
20 | export function getRenderdProduct(product) {
21 | return apiService.get(`/publish/products/${product}/render`)
22 | }
23 |
24 | export function triggerRenderProduct(product) {
25 | return apiService.post(`/publish/products/${product}/render`)
26 | }
27 |
28 | export function deleteProduct(product) {
29 | return apiService.delete(`/publish/products/${product.id}`)
30 | }
31 |
32 | export function getAllProductTypes() {
33 | return apiService.get('/publish/product-types')
34 | }
35 |
36 | export function publishProduct(product_id, publisher_id) {
37 | return apiService.post(
38 | `/publish/products/${product_id}/publishers/${publisher_id}`,
39 | {}
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/gui/src/stores/PublishStore.js:
--------------------------------------------------------------------------------
1 | import { getAllProducts, getAllProductTypes } from '@/api/publish'
2 | import { getAllUserPublishersPresets } from '@/api/user'
3 | import { useFilterStore } from './FilterStore'
4 | import { defineStore } from 'pinia'
5 |
6 | export const usePublishStore = defineStore('publish', {
7 | state: () => ({
8 | products: { total_count: 0, items: [] },
9 | product_types: { total_count: 0, items: [] },
10 | products_publisher_presets: { total_count: 0, items: [] }
11 | }),
12 | actions: {
13 | async loadProducts(data) {
14 | const response = await getAllProducts(data)
15 | this.products = response.data
16 | },
17 | async loadProductTypes(data) {
18 | const response = await getAllProductTypes(data)
19 | this.product_types = response.data
20 | },
21 | async updateProducts() {
22 | const filter = useFilterStore()
23 | const response = await getAllProducts(filter.productFilter)
24 | this.products = response.data
25 | },
26 | async loadUserPublishersPresets(context, data) {
27 | const response = await getAllUserPublishersPresets(data)
28 | this.products_publisher_presets = response.data
29 | }
30 | },
31 | persist: {
32 | paths: ['product_types']
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/src/gui/src/assets/menu_btn.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/AssetGroupView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
52 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/CodeEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
57 |
58 |
64 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/TrendingCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ tagText }}
7 |
8 |
9 | Cluster size: {{ cluster.size }}
10 |
11 |
12 |
13 |
14 |
15 | {{ item.name }}
16 |
17 | mdi-chevron-right
18 | {{ item.size }}
19 |
20 |
21 |
22 |
23 |
51 |
--------------------------------------------------------------------------------
/src/core/core/managers/db_manager.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from flask_sqlalchemy import SQLAlchemy
4 | from flask_migrate import Migrate
5 | from core.managers.db_seed_manager import pre_seed
6 | from sqlalchemy.engine import reflection
7 | from core.managers.log_manager import logger
8 |
9 | from sqlalchemy.engine import Engine
10 | from sqlalchemy import event
11 | from sqlite3 import Connection as SQLite3Connection
12 |
13 | db = SQLAlchemy()
14 | migrate = Migrate()
15 |
16 |
17 | def is_db_empty():
18 | inspector = reflection.Inspector.from_engine(db.engine)
19 | tables = inspector.get_table_names()
20 | return len(tables) == 0
21 |
22 |
23 | def initialize(app, first_worker):
24 | db.init_app(app)
25 | migrate.init_app(app, db)
26 |
27 | if "db" in sys.argv: # called via flask db
28 | return
29 |
30 | if not first_worker:
31 | return
32 |
33 | if is_db_empty():
34 | logger.debug("Create new Database")
35 | db.create_all()
36 | pre_seed(db)
37 | else:
38 | logger.debug("Make sure to call: `flask db upgrade`")
39 |
40 |
41 | @event.listens_for(Engine, "connect")
42 | def set_sqlite_pragma(dbapi_connection, connection_record):
43 | if isinstance(dbapi_connection, SQLite3Connection):
44 | cursor = dbapi_connection.cursor()
45 | cursor.execute("PRAGMA foreign_keys=ON")
46 | cursor.close()
47 |
--------------------------------------------------------------------------------
/src/gui/src/stores/DashboardStore.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import {
3 | getDashboardData,
4 | getTrendingClusters,
5 | getCluster
6 | } from '@/api/dashboard'
7 | import { notifyFailure } from '@/utils/helpers'
8 |
9 | export const useDashboardStore = defineStore('dashboard', {
10 | state: () => ({
11 | dashboard_data: {
12 | total_news_items: 0,
13 | total_products: 0,
14 | report_items_completed: 0,
15 | report_items_in_progress: 0,
16 | total_database_items: 0,
17 | latest_collected: '',
18 | tag_cloud: {}
19 | },
20 | clusters: {}
21 | }),
22 | actions: {
23 | async loadDashboardData() {
24 | try {
25 | const response = await getDashboardData()
26 | this.dashboard_data = response.data
27 | } catch (error) {
28 | notifyFailure(error.message)
29 | }
30 | },
31 | async loadClusters() {
32 | try {
33 | const response = await getTrendingClusters()
34 | this.clusters = response.data
35 | } catch (error) {
36 | notifyFailure(error.message)
37 | }
38 | },
39 | async getCluster(tag_type, filter_data) {
40 | try {
41 | const response = await getCluster(tag_type, filter_data)
42 | return response.data
43 | } catch (error) {
44 | notifyFailure(error.message)
45 | }
46 | }
47 | }
48 | })
49 |
--------------------------------------------------------------------------------
/src/core/core/auth/database_authenticator.py:
--------------------------------------------------------------------------------
1 | from core.managers.log_manager import logger
2 | from core.auth.base_authenticator import BaseAuthenticator
3 | from werkzeug.security import check_password_hash
4 | from core.model.user import User
5 |
6 |
7 | class DatabaseAuthenticator(BaseAuthenticator):
8 | def get_authenticator_name(self):
9 | return "DatabaseAuthenticator"
10 |
11 | def get_database_uri(self):
12 | return "DatabaseAuthenticator"
13 |
14 | def __str__(self):
15 | return f"Authenticator: {self.get_authenticator_name()} DB: {self.get_database_uri()}"
16 |
17 | def get_required_credentials(self):
18 | return ["username", "password"]
19 |
20 | def authenticate(self, credentials: dict[str, str]) -> tuple[dict[str, str], int]:
21 | if credentials is None:
22 | return BaseAuthenticator.generate_error()
23 | if "username" not in credentials or "password" not in credentials:
24 | return BaseAuthenticator.generate_error()
25 |
26 | user = User.find_by_name(credentials["username"])
27 | if user and check_password_hash(user.password, credentials["password"]):
28 | return BaseAuthenticator.generate_jwt(credentials["username"])
29 |
30 | logger.store_auth_error_activity(f"Authentication failed with credentials: {credentials}")
31 | return BaseAuthenticator.generate_error()
32 |
--------------------------------------------------------------------------------
/docker/Dockerfile.core:
--------------------------------------------------------------------------------
1 | FROM python:3-slim as builder
2 |
3 | WORKDIR /app/
4 |
5 | # install common packages
6 | RUN apt-get update && apt-get upgrade -y && apt-get install --no-install-recommends -y \
7 | libpq-dev \
8 | curl \
9 | openssl \
10 | build-essential \
11 | python3-dev
12 |
13 | COPY ./src/core/. /app/
14 |
15 | RUN python3 -m venv /app/.venv && \
16 | export PATH="/app/.venv/bin:$PATH" && \
17 | pip install --no-cache-dir --upgrade pip wheel && \
18 | pip install --no-cache-dir -e /app/
19 |
20 | FROM python:3-slim
21 |
22 | WORKDIR /app/
23 |
24 | RUN groupadd user && useradd --home-dir /app -g user user && chown -R user:user /app
25 | RUN apt-get update && apt-get upgrade -y && apt-get install --no-install-recommends -y \
26 | libpq-dev \
27 | curl \
28 | openssl
29 | RUN install -d -o user -g user /app/data
30 |
31 | COPY --from=builder --chown=user:user /app/.venv /app/.venv
32 | COPY --chown=user:user ./src/core/. /app/
33 | COPY --chown=user:user ./docker/gunicorn.conf.py /app/
34 |
35 | USER user
36 |
37 | ENV PATH="/app/.venv/bin:$PATH"
38 | ENV PYTHONPATH=/app
39 | ARG git_info
40 | ENV GIT_INFO=${git_info:-'{}'}
41 | RUN echo BUILD_DATE=$(date --iso-8601=minutes) > .env
42 | ENV DATA_FOLDER=/app/data
43 |
44 | VOLUME ["/app/data"]
45 | EXPOSE 80
46 |
47 | HEALTHCHECK --interval=60s --timeout=3s --retries=5 CMD curl --fail http://localhost/api/isalive || exit 1
48 |
49 | CMD ["gunicorn"]
50 |
--------------------------------------------------------------------------------
/src/gui/src/components/analyze/AttributeCVSS.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/gui/extras/update_version.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('child_process')
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | // Fetch the latest tag that matches the regex
6 | const getLatestTag = () => {
7 | try {
8 | return execSync('git tag --list v[0-9].[0-9]*.[0-9]* | sort -V | tail -n 1')
9 | .toString()
10 | .trim()
11 | } catch (err) {
12 | return null
13 | }
14 | }
15 |
16 | // Check if current commit is the same as the latest tag
17 | const isHeadAtLatestTag = (tag) => {
18 | try {
19 | const tagCommit = execSync(`git rev-list -n 1 ${tag}`).toString().trim()
20 | const headCommit = execSync('git rev-parse HEAD').toString().trim()
21 | return tagCommit === headCommit
22 | } catch (err) {
23 | return false
24 | }
25 | }
26 |
27 | const main = () => {
28 | let latestTag = getLatestTag()
29 |
30 | if (!latestTag) {
31 | console.error('Failed to get the latest tag. Using Fallback version 0.0.0')
32 | latestTag = 'v0.0.0'
33 | }
34 |
35 | let version = latestTag.slice(1) // remove the "v" prefix
36 |
37 | if (!isHeadAtLatestTag(latestTag)) {
38 | version += '-dev'
39 | }
40 |
41 | const packagePath = path.join(__dirname, '..', 'package.json')
42 | const packageData = require(packagePath)
43 | packageData.version = version
44 |
45 | fs.writeFileSync(packagePath, JSON.stringify(packageData, null, 2))
46 | console.log(`Updated version to ${version}`)
47 | }
48 |
49 | main()
50 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/Notification.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ notificationContent }}
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
48 |
--------------------------------------------------------------------------------
/src/gui/src/assets/common.css:
--------------------------------------------------------------------------------
1 | /* common.css */
2 | .v-chip-group .v-slide-group__content {
3 | padding: 0 !important;
4 | }
5 |
6 | .view,
7 | .view .row {
8 | padding: 0;
9 | margin: 0;
10 | }
11 |
12 | .view .view-panel {
13 | position: sticky;
14 | top: 42px;
15 | z-index: 2;
16 | }
17 |
18 | .container {
19 | max-width: calc(100%) !important;
20 | }
21 |
22 | .card .status.in_progress,
23 | .card .status.new,
24 | .card .status.status-orange {
25 | border-left: 4px solid #ffd556;
26 | }
27 |
28 | .card .status,
29 | .card .status.completed,
30 | .card .status.read,
31 | .card .status.status-green {
32 | border-left: 4px solid #33DD40;
33 | }
34 |
35 | .card .status.alert,
36 | .card .status.important,
37 | .card .status.status-red {
38 | border-left: 4px solid red;
39 | }
40 |
41 | .v-expansion-panel > .v-expansion-panel-title,
42 | .v-expansion-panel--active > .v-expansion-panel-title {
43 | min-height: 32px;
44 | }
45 |
46 | .v-expansion-panels.groups,
47 | .v-expansion-panels.items .v-expansion-panel:hover,
48 | .v-expansion-panels.items .v-expansion-panel--active{
49 | border-left: 3px solid #4092dd;
50 | }
51 |
52 | .v-expansion-panels.items .v-expansion-panel {
53 | border-left: 3px solid #6abef2;
54 | }
55 |
56 | .v-card .compact {
57 | display: -webkit-box;
58 | overflow: hidden;
59 | -webkit-line-clamp: 5;
60 | -webkit-box-orient: vertical;
61 | }
62 |
63 | .taranis-vertical-view {
64 | position: relative;
65 | height: calc(100vh - 3em);
66 | overflow-y: auto
67 | }
68 |
--------------------------------------------------------------------------------
/src/gui/src/components/assess/card/SummarizedContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-text-short
6 |
7 |
8 |
9 | ...
10 |
11 |
12 |
13 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/wordpress_publisher.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from base64 import b64decode
3 | from datetime import datetime
4 | import requests
5 |
6 | from .base_publisher import BasePublisher
7 |
8 |
9 | class WORDPRESSPublisher(BasePublisher):
10 | type = "WORDPRESS_PUBLISHER"
11 | name = "Wordpress Publisher"
12 | description = "Publisher for publishing on Wordpress webpage"
13 |
14 | def publish(self, publisher_input):
15 | try:
16 | user = publisher_input.parameter_values_map["WP_USER"]
17 | python_app_secret = publisher_input.parameter_values_map["WP_PYTHON_APP_SECRET"]
18 | main_wp_url = publisher_input.parameter_values_map["WP_URL"]
19 |
20 | data_string = f"{user}:{python_app_secret}"
21 |
22 | token = base64.b64encode(data_string.encode())
23 |
24 | headers = {"Authorization": "Basic " + token.decode("utf-8")}
25 |
26 | data = publisher_input.data[:]
27 |
28 | bytes_data = b64decode(data, validate=True).decode("utf-8")
29 |
30 | now = datetime.now()
31 | title = f"Report from TaranisNG on {now.strftime('%d.%m.%Y')} at {now.strftime('%H:%M')}"
32 |
33 | post = {"title": title, "status": "publish", "content": bytes_data}
34 |
35 | requests.post(
36 | f"{main_wp_url}/index.php/wp-json/wp/v2/posts",
37 | headers=headers,
38 | json=post,
39 | )
40 | except Exception as error:
41 | BasePublisher.print_exception(self, error)
42 |
--------------------------------------------------------------------------------
/src/gui/src/styles/awake.scss:
--------------------------------------------------------------------------------
1 | $primary: #7468e8;
2 | $secondary: #34a5e8;
3 | $accent: #6daed9;
4 | $info: #2196f3;
5 | $error: #ba3b3b;
6 | $success: #4caf50;
7 | $warning: #ffc107;
8 | $cx-app-header: #c9c9c9;
9 | $cx-toolbar-filter: #ffffff;
10 | $cx-combo-gray: #f2f2f2;
11 | $cx-user-menu: #d9d9d9;
12 | $cx-drawer-bg: #ffffff;
13 | $cx-drawer-text: #000000;
14 | $cx-drawer-text-invert: #000;
15 | $cx-line: #fafafa;
16 | $cx-favorites: #ff9d48;
17 | $cx-filter: #9f02ff;
18 | $cx-wordlist: #ffc107;
19 |
20 | $light-grey-1: #c9c9c9;
21 | $light-grey-2: #c8c8c8;
22 | $dark-grey: #575757;
23 | $very-dark-grey: #313131;
24 |
25 | $awake-green-color: #77bb70;
26 | $awake-yellow-color: #e9c645;
27 | $awake-red-color: #d18e8e;
28 |
29 | .item-action-btn.btn-group button,
30 | button.item-action-btn,
31 | a.item-action-btn {
32 | flex-grow: inherit;
33 | height: 36px !important;
34 | min-width: 34px;
35 | padding: 0 12px 0 8px;
36 | margin: 4px 4px 4px 0px;
37 | letter-spacing: normal;
38 | border-radius: 0;
39 | font-size: 0.85rem;
40 | text-transform: lowercase;
41 | font-weight: normal;
42 |
43 | &.expandable {
44 | padding: 0px 10px !important;
45 | min-width: 40px !important;
46 | flex-grow: unset;
47 | & .v-icon {
48 | font-size: 1.15rem;
49 | }
50 | }
51 | }
52 |
53 | .news-item-title {
54 | // Clipping
55 | overflow: hidden;
56 | text-overflow: ellipsis;
57 | display: -webkit-box;
58 | -webkit-line-clamp: 1;
59 | line-clamp: 1;
60 | -webkit-box-orient: vertical;
61 | max-height: calc(1.5em * 2);
62 | line-height: 1.3;
63 | }
64 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/TagTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
63 |
--------------------------------------------------------------------------------
/src/gui/src/components/config/ImportExport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 | Import
11 |
12 |
19 | Export
20 |
21 |
28 |
29 |
30 |
31 |
70 |
71 |
76 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/ProductView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
59 |
--------------------------------------------------------------------------------
/.devcontainer/Containerfile:
--------------------------------------------------------------------------------
1 | FROM debian
2 |
3 | WORKDIR /app/
4 |
5 | RUN apt-get update && apt-get upgrade -y && apt-get install --no-install-recommends -y \
6 | git \
7 | curl \
8 | sudo \
9 | tmux \
10 | vim \
11 | openssl \
12 | python3 \
13 | libpq-dev \
14 | python3-dev \
15 | python3-pip \
16 | python3-venv \
17 | postgresql-all \
18 | build-essential \
19 | rabbitmq-server \
20 | python-is-python3 \
21 | nginx \
22 | nodejs \
23 | npm
24 |
25 | RUN groupadd user && useradd --shell /bin/bash --home-dir /app -g user user && chown -R user:user /app && \
26 | /usr/sbin/service postgresql start && \
27 | echo "user ALL=NOPASSWD: ALL" > /etc/sudoers.d/user && \
28 | chmod 0440 /etc/sudoers.d/user && \
29 | su postgres -c "psql -c \"CREATE USER taranis WITH PASSWORD 'supersecret';\"" && \
30 | su postgres -c "psql -c 'CREATE DATABASE taranis OWNER taranis;'" && \
31 | su postgres -c "psql -c 'GRANT ALL PRIVILEGES ON DATABASE taranis TO taranis;'" && \
32 | install -d -o user -g user /venv && \
33 | su user -c 'python3 -m venv /venv && /venv/bin/pip install --upgrade pip wheel setuptools Flask celery gunicorn'
34 |
35 |
36 | EXPOSE 5000
37 | EXPOSE 5432
38 | EXPOSE 5672
39 | EXPOSE 15672
40 | EXPOSE 8080
41 | EXPOSE 9000
42 |
43 | ENV PATH="/venv/bin:$PATH"
44 | ENV PYTHONPATH=/app
45 |
46 | VOLUME ["/app"]
47 |
48 | COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint.sh
49 | COPY --chmod=0755 start-dev.sh /usr/local/bin/start-dev.sh
50 | COPY nginx.dev.conf /etc/nginx/sites-enabled/default
51 |
52 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
53 |
--------------------------------------------------------------------------------
/src/gui/src/components/popups/PopupDeleteItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-close
5 |
6 |
7 |
8 |
9 |
10 | Item:
11 |
12 | "{{ newsItem.title }}"
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | This action deletes the item permanently.
21 |
22 |
23 |
24 | mdi-alert-octagon-outline
25 |
26 | This action cannot be undone.
27 |
28 |
29 |
30 | mdi-delete
31 | delete item
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
58 |
--------------------------------------------------------------------------------
/src/core/core/managers/api_manager.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Api
2 | from swagger_ui import api_doc
3 | from flask import jsonify
4 | from pathlib import Path
5 |
6 | import core.api as core_api
7 |
8 |
9 | def initialize(app):
10 | api = Api(app, version="1", title="Taranis API", doc="/api/swagger", prefix="/api")
11 |
12 | app.register_error_handler(400, handle_bad_request)
13 | app.register_error_handler(401, handle_unauthorized)
14 | app.register_error_handler(404, handle_not_found)
15 |
16 | openapi_yaml = Path(__file__).parent.parent / "static" / "openapi3_0.yaml"
17 | api_doc(app, config_path=openapi_yaml, url_prefix="/api/doc", editor=False)
18 |
19 | core_api.analyze.initialize(api)
20 | core_api.assess.initialize(api)
21 | core_api.assets.initialize(api)
22 | core_api.auth.initialize(api)
23 | core_api.bots.initialize(api)
24 | core_api.config.initialize(api)
25 | core_api.dashboard.initialize(api)
26 | core_api.isalive.initialize(api)
27 | core_api.publish.initialize(api)
28 | core_api.user.initialize(api)
29 | core_api.worker.initialize(api)
30 |
31 |
32 | def handle_bad_request(e):
33 | if hasattr(e, "description"):
34 | return jsonify(error=str(e.description)), 400
35 | return jsonify(error="Bad request"), 400
36 |
37 |
38 | def handle_unauthorized(e):
39 | if hasattr(e, "description"):
40 | return jsonify(error=str(e.description)), 401
41 | return jsonify(error="Unauthorized"), 401
42 |
43 |
44 | def handle_not_found(e):
45 | if hasattr(e, "item"):
46 | return jsonify(error=f"{e.item} not found"), 404
47 | return jsonify(error="Not found"), 404
48 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/tagging_bot.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from .base_bot import BaseBot
4 | from worker.log import logger
5 |
6 |
7 | class TaggingBot(BaseBot):
8 | type = "TAGGING_BOT"
9 | name = "Tagging Bot"
10 | description = "Bot for tagging news items based on regular expressions"
11 |
12 | def execute(self, parameters=None):
13 | if not parameters:
14 | return
15 | try:
16 | regexp = parameters.get("REGULAR_EXPRESSION", r"CVE-\d{4}-\d{4,7}")
17 |
18 | if not (data := self.get_stories(parameters)):
19 | return "Error getting news items"
20 |
21 | found_tags = {}
22 | for aggregate in data:
23 | findings = set()
24 | existing_tags = aggregate["tags"] or []
25 | for news_item in aggregate["news_items"]:
26 | content = news_item["news_item_data"]["content"]
27 | title = news_item["news_item_data"]["title"]
28 | review = news_item["news_item_data"]["review"]
29 |
30 | analyzed_content = set((title + review + content).split())
31 |
32 | for element in analyzed_content:
33 | if finding := re.search(f"({regexp})", element.strip(".,")):
34 | if finding[1] not in existing_tags:
35 | findings.add(finding[1])
36 | found_tags[aggregate["id"]] = findings
37 |
38 | self.core_api.update_tags(found_tags, self.type)
39 |
40 | except Exception:
41 | logger.log_debug_trace(f"Error running Bot: {self.type}")
42 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/StoryView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
61 |
--------------------------------------------------------------------------------
/src/gui/src/components/assess/card/votes.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ story.likes }}
12 |
18 |
19 |
27 | {{ story.dislikes }}
28 |
34 |
35 |
36 |
37 |
38 |
62 |
63 |
72 |
--------------------------------------------------------------------------------
/src/gui/src/main.js:
--------------------------------------------------------------------------------
1 | import '@mdi/font/css/materialdesignicons.css'
2 | import { createApp } from 'vue'
3 | import App from './App.vue'
4 | import { router } from './router'
5 | import { ApiService } from '@/services/api_service'
6 | import DatePicker from 'vue-datepicker-next'
7 | import { i18n } from '@/i18n/i18n'
8 | import { vuetify } from '@/plugins/vuetify'
9 | import { createPinia } from 'pinia'
10 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
11 | import 'vue-datepicker-next/index.css'
12 | import VueDOMPurifyHTML from 'vue-dompurify-html'
13 |
14 | import * as Sentry from '@sentry/vue'
15 |
16 | export const app = createApp(App)
17 | app.use(DatePicker)
18 | app.use(i18n)
19 |
20 | const pinia = createPinia()
21 | pinia.use(piniaPluginPersistedstate)
22 |
23 | app.use(router)
24 | app.use(pinia)
25 | app.use(vuetify)
26 | app.use(VueDOMPurifyHTML)
27 |
28 | import { useMainStore } from './stores/MainStore'
29 | const mainStore = useMainStore()
30 | mainStore.updateFromLocalConfig()
31 | const { coreAPIURL, sentryDSN } = mainStore
32 |
33 | console.debug('CoreAPI initialized ', coreAPIURL, sentryDSN)
34 | export const apiService = new ApiService(coreAPIURL)
35 | app.provide('$coreAPIURL', coreAPIURL)
36 |
37 | if (sentryDSN) {
38 | Sentry.init({
39 | app,
40 | dsn: sentryDSN,
41 | autoSessionTracking: true,
42 | integrations: [
43 | new Sentry.BrowserTracing({
44 | routingInstrumentation: Sentry.vueRouterInstrumentation(router)
45 | }),
46 | new Sentry.Replay()
47 | ],
48 | environment: import.meta.env.DEV ? 'development' : 'production',
49 | tracesSampleRate: 1.0
50 | })
51 | }
52 |
53 | app.mount('#app')
54 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/AssetView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
67 |
--------------------------------------------------------------------------------
/src/gui/src/views/users/ReportView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
60 |
--------------------------------------------------------------------------------
/src/worker/worker/misc/wordlist_update.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from worker.log import logger
4 | from worker.core_api import CoreApi
5 |
6 |
7 | def update_wordlist(word_list_id: int):
8 | core_api = CoreApi()
9 |
10 | if not word_list_id:
11 | logger.error("No word list id provided")
12 | return "No word list id provided"
13 |
14 | word_list = core_api.get_word_list(word_list_id)
15 |
16 | if not word_list:
17 | logger.error(f"Word list with id {word_list_id} not found")
18 | return f"Word list with id {word_list_id} not found"
19 |
20 | url = word_list["link"]
21 | logger.info(f"Updating word list {word_list['name']} from {url}")
22 | response = requests.get(url=url)
23 | if not response.ok:
24 | logger.error(f"Failed to download word list {word_list['name']} - from {url} : {response.status_code}")
25 | return
26 |
27 | content_type = response.headers.get("content-type", "")
28 | if content_type == "text/csv" or url.endswith(".csv"):
29 | content_type = "text/csv"
30 | content = response.text
31 | elif content_type == "application/json" or url.endswith(".json"):
32 | content_type = "application/json"
33 | content = response.json()
34 | else:
35 | logger.error("Could not determine content type.")
36 | return
37 |
38 | logger.debug(f"Updating word list {word_list['name']} with {len(content)} entries - {content_type}")
39 |
40 | response = core_api.update_word_list(word_list["id"], content, content_type)
41 |
42 | if not response:
43 | logger.error(f"Failed to update word list {word_list['name']}")
44 | return
45 |
46 | logger.info(response)
47 | return response
48 |
--------------------------------------------------------------------------------
/src/core/core/api/user.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Resource, Namespace, Api
2 | from flask import request
3 |
4 | from core.managers import auth_manager
5 | from core.managers.auth_manager import auth_required
6 | from core.model import product_type, publisher_preset
7 | from core.model.user import User
8 |
9 |
10 | class UserProfile(Resource):
11 | def get(self):
12 | if user := auth_manager.get_user_from_jwt():
13 | return user.get_profile_json()
14 | return {"message": "User not found"}, 404
15 |
16 | def put(self):
17 | # sourcery skip: assign-if-exp, reintroduce-else, swap-if-else-branches, use-named-expression
18 | user = auth_manager.get_user_from_jwt()
19 | if not user:
20 | return {"message": "User not found"}, 404
21 | json_data = request.json
22 | if not json_data:
23 | return {"message": "No input data provided"}, 400
24 | return User.update_profile(user, request.json)
25 |
26 |
27 | class UserProductTypes(Resource):
28 | @auth_required("PUBLISH_ACCESS")
29 | def get(self):
30 | return product_type.ProductType.get_all_json(None, auth_manager.get_user_from_jwt(), False)
31 |
32 |
33 | class UserPublisherPresets(Resource):
34 | @auth_required("PUBLISH_ACCESS")
35 | def get(self):
36 | return publisher_preset.PublisherPreset.get_all_json(None)
37 |
38 |
39 | def initialize(api: Api):
40 | namespace = Namespace("users", description="User API")
41 |
42 | namespace.add_resource(UserProfile, "/profile")
43 | namespace.add_resource(UserProductTypes, "/my-product-types")
44 | namespace.add_resource(UserPublisherPresets, "/my-publisher-presets")
45 | api.add_namespace(namespace, path="/users")
46 |
--------------------------------------------------------------------------------
/src/gui/src/components/UserMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ user.name }}
9 | {{
10 | user.organization_name
11 | }}
12 |
13 |
14 |
15 |
16 | {{ $t('user_menu.settings') }}
17 |
18 |
19 |
20 | {{ $t('user_menu.logout') }}
21 |
22 |
23 |
24 |
25 |
26 |
60 |
--------------------------------------------------------------------------------
/src/gui/README.md:
--------------------------------------------------------------------------------
1 | # Taranis GUI
2 |
3 | The GUI is written in [Vue.js](https://vuejs.org/) with [Vuetify](https://vuetifyjs.com/en/).
4 |
5 | Currently, the best way to build and deploy is via Docker. For more information, see [docker/README.md](../../docker/README.md) and the [toplevel README.md file](../../README.md).
6 |
7 | If you wish to develop and build the GUI separately, read on.
8 |
9 | ### Project setup
10 |
11 | Install the dependencies
12 |
13 | ```
14 | npm install
15 | ```
16 |
17 | ### Developing
18 |
19 | You can run the GUI on the local machine, and edit it with your favorite IDE or text editor. The application will react to your changes in real time. Depending on whether you expose your API directly on `http://localhost:5000` (see `docker-compose.yml`), via Traefik on `https://localhost:4443`, or on a public IP and host name, you may need to change the following environment variables.
20 |
21 | ```
22 | # env variables
23 | env variables can be adapted in .env file
24 |
25 |
26 | to run the applicaiton locally add `VITE_TARANIS_CONFIG_JSON = "/config.local.json"` to .env file and add a `/config.local.json` file in the public folder. This can look like:
27 |
28 | ```
29 |
30 | {
31 | "TARANIS_CORE_API": "http://localhost:5000/api"
32 | }
33 |
34 | ```
35 | npm run dev
36 | ```
37 |
38 | ### Building the static version
39 |
40 | When you are ready to generate the final static version of the GUI, run
41 |
42 | ```
43 | npm run build
44 | ```
45 |
46 | The static html/js/css files will be stored under the `dist/` subdirectory.
47 |
48 | ### Testing and linting
49 |
50 | ```
51 | npm run test
52 | npm run lint
53 | ```
54 |
55 | ### Customize the configuration
56 |
57 | See [Configuration Reference](https://cli.vuejs.org/config/).
58 |
--------------------------------------------------------------------------------
/src/gui/extras/30-update_config_from_env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CONFIG_FILE="/usr/share/nginx/html/config.json"
4 |
5 | perl_script() {
6 | perl - "$CONFIG_FILE" <<'END_OF_PERL'
7 | use strict;
8 | use warnings;
9 |
10 | my $CONFIG_FILE = shift @ARGV;
11 |
12 | sub add_to_json {
13 | my ($key, $value, $file) = @_;
14 |
15 | # Read the file into a scalar
16 | open my $fh, '<', $file or die "Cannot open $file: $!";
17 | local $/;
18 | my $content = <$fh>;
19 | close $fh;
20 |
21 | # Check if key exists and either modify or append
22 | if ($content =~ /"$key"/) {
23 | $content =~ s/("$key"\s*:\s*)".*?"/$1"$value"/;
24 | } else {
25 | # Check if the content ends with "}" after optional spaces or newlines
26 | if ($content =~ m/}\s*$/) {
27 | $content =~ s/}\s*$/,\n "$key":"$value"\n}/;
28 | } else {
29 | $content .= "{\n \"$key\":\"$value\"\n}\n";
30 | }
31 | }
32 |
33 | # Write the modified content back to the file
34 | open my $ofh, '>', $file or die "Cannot write to $file: $!";
35 | print $ofh $content;
36 | close $ofh;
37 | }
38 |
39 | # Ensure the file has an initial JSON object if it doesn't exist or is empty
40 | unless (-e $CONFIG_FILE) {
41 | open my $fh, '>', $CONFIG_FILE or die "Cannot create $CONFIG_FILE: $!";
42 | print $fh '{}';
43 | close $fh;
44 | }
45 |
46 | # Iterate over all environment variables that start with TARANIS_NG
47 | foreach my $key (sort keys %ENV) {
48 | if ($key =~ /^TARANIS_NG/) {
49 | my $value = $ENV{$key};
50 | if (defined $value && $value ne '') {
51 | add_to_json($key, $value, $CONFIG_FILE);
52 | }
53 | }
54 | }
55 | END_OF_PERL
56 | }
57 |
58 | perl_script
59 |
--------------------------------------------------------------------------------
/src/gui/src/components/assess/filter/filterSelectList.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
18 | {{ button.label }}
19 |
20 |
21 |
22 |
23 |
64 |
65 |
70 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/base_bot.py:
--------------------------------------------------------------------------------
1 | from worker.log import logger
2 | from worker.core_api import CoreApi
3 | from urllib.parse import parse_qs
4 | import datetime
5 |
6 |
7 | class BaseBot:
8 | type = "BASE_BOT"
9 | name = "Base Bot"
10 | description = "Base abstract type for all bots"
11 |
12 | def __init__(self):
13 | self.core_api = CoreApi()
14 |
15 | def execute(self):
16 | pass
17 |
18 | def get_filter_dict(self, parameters) -> dict:
19 | filter_dict = {}
20 | if item_filter := parameters.pop("ITEM_FILTER", None):
21 | filter_dict = {k: v[0] if len(v) == 1 else v for k, v in parse_qs(item_filter).items()}
22 |
23 | filter_dict |= {k.lower(): v for k, v in parameters.items()}
24 | if "timestamp" not in filter_dict:
25 | limit = (datetime.datetime.now() - datetime.timedelta(days=7)).isoformat()
26 | filter_dict["timestamp"] = limit
27 |
28 | return filter_dict
29 |
30 | def update_filter_for_pagination(self, filter_dict, limit=100):
31 | filter_dict["limit"] = limit
32 | if "offset" in filter_dict:
33 | filter_dict["offset"] += limit
34 | else:
35 | filter_dict["offset"] = limit
36 | return filter_dict
37 |
38 | def get_stories(self, parameters) -> list:
39 | filter_dict = self.get_filter_dict(parameters)
40 | data = self.core_api.get_news_items_aggregate(filter_dict)
41 | if not data:
42 | logger.error("Error getting news items")
43 | return data
44 |
45 | def refresh(self):
46 | logger.info(f"Refreshing Bot: {self.type} ...")
47 |
48 | try:
49 | self.execute()
50 | except Exception:
51 | logger.log_debug_trace(f"Refresh Bots Failed: {self.type}")
52 |
--------------------------------------------------------------------------------
/src/gui/src/api/assets.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function getAllAssetGroups(filter_data) {
4 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
5 | return apiService.get(`/asset-groups?${filter}`)
6 | }
7 |
8 | export function getAssetGroup(asset_group_id) {
9 | return apiService.get(`/asset-groups/${asset_group_id}`)
10 | }
11 |
12 | export function createAssetGroup(group) {
13 | return apiService.post('/asset-groups', group)
14 | }
15 |
16 | export function updateAssetGroup(group) {
17 | return apiService.put(`/asset-groups/${group}`, group)
18 | }
19 |
20 | export function deleteAssetGroup(group) {
21 | return apiService.delete(`/asset-groups/${group}`)
22 | }
23 |
24 | export function getAllAssets(filter_data) {
25 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
26 | return apiService.get(`/assets?${filter}`)
27 | }
28 |
29 | export function getAsset(asset_id) {
30 | return apiService.get(`/assets/${asset_id}`)
31 | }
32 |
33 | export function createAsset(asset) {
34 | return apiService.post('/assets', asset)
35 | }
36 |
37 | export function solveVulnerability(data) {
38 | return apiService.post(
39 | `/assets/${data.asset_id}/vulnerabilities/${data.report_item_id}`,
40 | data
41 | )
42 | }
43 |
44 | export function updateAsset(asset) {
45 | return apiService.put(`/assets/${asset}`, asset)
46 | }
47 |
48 | export function deleteAsset(asset) {
49 | return apiService.delete(`/assets/${asset}`)
50 | }
51 |
52 | export function findAttributeCPE() {
53 | return apiService.get('/asset-attributes/cpe')
54 | }
55 |
56 | export function getCPEAttributeEnums(filter_data) {
57 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
58 | return apiService.get(`/asset-attributes/cpe/enums?${filter}`)
59 | }
60 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/story_bot.py:
--------------------------------------------------------------------------------
1 | from .base_bot import BaseBot
2 | from worker.log import logger
3 | from story_clustering.clustering import initial_clustering, incremental_clustering
4 |
5 |
6 | class StoryBot(BaseBot):
7 | type = "STORY_BOT"
8 | name = "Story Clustering Bot"
9 | description = "Bot for clustering NewsItems to stories via naturale language processing"
10 |
11 | def __init__(self):
12 | import story_clustering # noqa: F401
13 |
14 | super().__init__()
15 |
16 | def execute(self, parameters=None):
17 | if not parameters:
18 | return
19 | try:
20 | if not (data := self.get_stories(parameters)):
21 | return "Error getting news items"
22 |
23 | logger.info(f"Clustering {len(data)} news items")
24 | if all(len(aggregate["news_items"]) == 1 for aggregate in data):
25 | clustering_results = initial_clustering(data)
26 | else:
27 | already_clustered, to_cluster = self.separate_data(data)
28 | clustering_results = incremental_clustering(to_cluster, already_clustered)
29 |
30 | logger.info(f"Clustering results: {clustering_results['event_clusters']}")
31 | self.core_api.news_items_grouping_multiple(clustering_results["event_clusters"])
32 |
33 | except Exception:
34 | logger.log_debug_trace(f"Error running Bot: {self.type}")
35 |
36 | def separate_data(self, data):
37 | already_clustered = []
38 | to_cluster = []
39 |
40 | for aggregate in data:
41 | if len(aggregate["news_items"]) > 1:
42 | already_clustered.append(aggregate)
43 | else:
44 | to_cluster.append(aggregate)
45 |
46 | return already_clustered, to_cluster
47 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/IconNavigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
65 |
--------------------------------------------------------------------------------
/src/core/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "taranis-core"
3 | description = "Taranis Core"
4 | readme = "README.md"
5 | license = {file = "LICENSE.md"}
6 | maintainers = [{ name = "AIT", email = "benjamin.akhras@ait.ac.at" }]
7 |
8 | classifiers = [
9 | "Programming Language :: Python",
10 | "Programming Language :: Python :: 3",
11 | "Programming Language :: Python :: 3.11",
12 | "Framework :: Flask",
13 | "Topic :: Security",
14 | ]
15 | requires-python = ">=3.11"
16 | dependencies = [
17 | "Flask==2.3.3",
18 | "Flask-Cors",
19 | "Flask-JWT-Extended",
20 | "Flask-Migrate",
21 | "flask-restx",
22 | "Flask-SQLAlchemy",
23 |
24 | "click",
25 | "celery",
26 | "SQLAlchemy==1.4.40",
27 | "pydantic-settings",
28 | "python-dotenv",
29 | "gunicorn",
30 | "psycogreen",
31 | "psycopg2",
32 | "swagger-ui-py",
33 | "requests",
34 | "authlib",
35 | "sseclient",
36 | "PyYAML==5.3.1"
37 | ]
38 | dynamic = ["version"]
39 |
40 | [tool.setuptools]
41 | packages = ["core"]
42 | include-package-data = true
43 |
44 | [build-system]
45 | requires = [
46 | "setuptools>=45",
47 | "setuptools_scm[toml]>=6.2",
48 | "wheel"
49 | ]
50 | build-backend = "setuptools.build_meta"
51 |
52 | [tool.setuptools_scm]
53 | fallback_version = "0.0.0"
54 | root = "../.."
55 |
56 | [project.optional-dependencies]
57 | dev = ["black", "flake8", "pytest", "pytest-celery", "sqlalchemy-stubs", "schemathesis", "build", "wheel", "setuptools", "setuptools_scm"]
58 |
59 | [project.urls]
60 | "Source Code" = "https://github.com/ait-cs-IaaS/Taranis-NG"
61 |
62 | [tool.black]
63 | line-length = "142"
64 | target-version = ["py311"]
65 |
66 | [tool.pytest.ini_options]
67 | filterwarnings = [
68 | "ignore:.*_app_ctx_stack.*:DeprecationWarning",
69 | "ignore::DeprecationWarning"
70 | ]
71 |
--------------------------------------------------------------------------------
/src/gui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taranis-ui",
3 | "private": true,
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build && npm run patch_config",
7 | "preview": "vite preview",
8 | "prepack": "node ./extras/update_version.js",
9 | "postpack": "node ./extras/remove_version.js",
10 | "lint": "eslint --ext .js,.vue --fix src",
11 | "package": "npm run prepack && npm pack && npm run postpack",
12 | "format": "prettier --write \"src/**/*.{js,vue}\"",
13 | "lint_and_format": "npm run lint && npm run format",
14 | "patch_config": "node ./extras/patch_config_json.js",
15 | "test": "npm run lint && npm run format"
16 | },
17 | "dependencies": {
18 | "@codemirror/lang-html": "^6.4.6",
19 | "@codemirror/lang-json": "^6.0.1",
20 | "@intlify/unplugin-vue-i18n": "^1.4.0",
21 | "@mdi/font": "^7.3.67",
22 | "@sentry/vue": "^7.74.0",
23 | "axios": "^1.5.1",
24 | "codemirror": "^6.0.1",
25 | "js-base64": "^3.7.5",
26 | "pinia": "^2.1.7",
27 | "pinia-plugin-persistedstate": "^3.2.0",
28 | "vue": "^3.3.4",
29 | "vue-chartjs": "5.2.0",
30 | "vue-codemirror": "^6.1.1",
31 | "vue-datepicker-next": "^1.0.3",
32 | "vue-dompurify-html": "^4.1.4",
33 | "vue-i18n": "^9.5.0",
34 | "vue-pdf-embed": "1.2.1",
35 | "vue-router": "^4.2.5",
36 | "vue-shortkey": "^3.1.7",
37 | "vue2-dropzone-vue3": "^1.1.0",
38 | "vue3-keypress": "^4.0.1",
39 | "vuetify": "^3.3.21"
40 | },
41 | "devDependencies": {
42 | "@vitejs/plugin-vue": "^4.4.0",
43 | "@vue/eslint-config-prettier": "^8.0.0",
44 | "@vue/eslint-config-typescript": "^12.0.0",
45 | "prettier": "^3.0.3",
46 | "sass": "^1.69.3",
47 | "typescript": "^5.2.2",
48 | "vite": "^4.4.11",
49 | "vite-plugin-vuetify": "^1.0.2",
50 | "vue-tsc": "^1.8.19"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/grouping_bot.py:
--------------------------------------------------------------------------------
1 | import re
2 | from .base_bot import BaseBot
3 | from worker.log import logger
4 | from collections import defaultdict
5 |
6 |
7 | class GroupingBot(BaseBot):
8 | type = "GROUPING_BOT"
9 | name = "Grouping Bot"
10 | description = "Bot for grouping news items into aggregates"
11 | default_regex = r"CVE-\d{4}-\d{4,7}"
12 |
13 | def execute(self, parameters=None):
14 | if not parameters:
15 | return
16 | try:
17 | regexp = parameters.get("REGULAR_EXPRESSION", None)
18 | if not regexp:
19 | return
20 |
21 | if not (data := self.get_stories(parameters)):
22 | return "Error getting news items"
23 |
24 | findings = defaultdict(list)
25 | for aggregate in data:
26 | for news_item in aggregate["news_items"]:
27 | content = news_item["news_item_data"]["content"]
28 | title = news_item["news_item_data"]["title"]
29 | review = news_item["news_item_data"]["review"]
30 |
31 | analyzed_content = set((title + review + content).split())
32 | for element in analyzed_content:
33 | if finding := re.search(f"({regexp})", element.strip(".,")):
34 | findings[finding[1]].append(aggregate["id"])
35 | break
36 |
37 | if not findings:
38 | return
39 |
40 | for group, ids in findings.items():
41 | if len(ids) > 1:
42 | logger.debug(f"Grouping: {group} with: {ids}")
43 | self.core_api.news_items_grouping(ids)
44 |
45 | except Exception:
46 | logger.log_debug_trace(f"Error running Bot: {self.type}")
47 |
--------------------------------------------------------------------------------
/src/gui/src/assets/themes.js:
--------------------------------------------------------------------------------
1 | const defaultDark = Object.freeze({
2 | // default dark theme for all
3 | primary: '#4092dd',
4 | secondary: '#34a5e8',
5 | accent: '#82B1FF',
6 | info: '#2196F3',
7 | error: '#FF5252',
8 | success: '#4CAF50',
9 | warning: '#FFC107',
10 | 'cx-app-header': '#444',
11 | 'cx-toolbar-filter': '#363636',
12 | // 'cx-combo-gray': '#f2f2f2',
13 | 'cx-user-menu': '#3a3a3a',
14 | 'cx-drawer-bg': '#424242',
15 | // 'cx-drawer-text': '#fff',
16 | 'cx-drawer-text-invert': '#fff',
17 | // 'cx-line': '#f00',
18 | 'cx-favorites': '#ff9d48',
19 | 'cx-filter': '#9f02ff'
20 | })
21 |
22 | const taranisDefault = Object.freeze({
23 | light: {
24 | primary: '#4092dd',
25 | secondary: '#34a5e8',
26 | accent: '#82B1FF',
27 | info: '#2196F3',
28 | error: '#FF5252',
29 | success: '#4CAF50',
30 | warning: '#FFC107',
31 | 'cx-app-header': '#c7c7c7',
32 | 'cx-toolbar-filter': '#ffffff',
33 | 'cx-combo-gray': '#f2f2f2',
34 | 'cx-user-menu': '#d9d9d9',
35 | 'cx-drawer-bg': '#4092dd',
36 | 'cx-drawer-text': '#fff',
37 | 'cx-drawer-text-invert': '#000',
38 | 'cx-line': '#fafafa',
39 | 'cx-favorites': '#ff9d48',
40 | 'cx-filter': '#9f02ff',
41 | 'cx-wordlist': '#FFC107'
42 | },
43 | dark: defaultDark
44 | })
45 |
46 | const taranisUsersDefault = Object.freeze({
47 | light: {
48 | primary: '#4092dd',
49 | secondary: '#34a5e8',
50 | accent: '#82B1FF',
51 | info: '#2196F3',
52 | error: '#FF5252',
53 | success: '#4CAF50',
54 | warning: '#FFC107',
55 | 'cx-app-header': '#c7c7c7',
56 | 'cx-toolbar-filter': '#ffffff',
57 | 'cx-combo-gray': '#f2f2f2',
58 | 'cx-user-menu': '#d9d9d9'
59 | },
60 | dark: defaultDark
61 | })
62 |
63 | export default Object.freeze({
64 | taranisDefault,
65 | taranisUsersDefault
66 | })
67 |
--------------------------------------------------------------------------------
/src/core/core/api/auth.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote
2 | from flask import redirect, make_response, request
3 | from flask_restx import Resource, reqparse, Namespace, Api
4 | from flask_jwt_extended import jwt_required
5 |
6 | from core.config import Config
7 | from core.managers import auth_manager
8 | from core.managers.log_manager import logger
9 |
10 |
11 | class Login(Resource):
12 | @jwt_required()
13 | def get(self):
14 | return make_response(redirect(quote(request.args.get(key="gotoUrl", default="/"))))
15 |
16 | def post(self):
17 | parser = reqparse.RequestParser()
18 | logger.log_debug(auth_manager.get_required_credentials())
19 | for credential in auth_manager.get_required_credentials():
20 | parser.add_argument(credential, location=["form", "values", "json"])
21 | credentials = parser.parse_args()
22 | return auth_manager.authenticate(credentials)
23 |
24 |
25 | class Refresh(Resource):
26 | @jwt_required()
27 | def get(self):
28 | return auth_manager.refresh(auth_manager.get_user_from_jwt())
29 |
30 |
31 | class Logout(Resource):
32 | def get(self):
33 | token = request.args["jwt"] if "jwt" in request.args else None
34 | response = auth_manager.logout(token)
35 |
36 | if goto_url := request.args.get("gotoUrl"):
37 | url = Config.OPENID_LOGOUT_URL.replace("GOTO_URL", goto_url) if Config.OPENID_LOGOUT_URL else goto_url
38 | return redirect(quote(url))
39 |
40 | return response
41 |
42 |
43 | def initialize(api: Api):
44 | namespace = Namespace("Auth", description="Authentication related operations")
45 | namespace.add_resource(Login, "/login")
46 | namespace.add_resource(Refresh, "/refresh")
47 | namespace.add_resource(Logout, "/logout")
48 | api.add_namespace(namespace, path="/auth")
49 |
--------------------------------------------------------------------------------
/docker/gunicorn.conf.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import os
3 | import logging
4 | from gunicorn import glogging
5 |
6 | workers_per_core = int(os.getenv("WORKERS_PER_CORE", "2"))
7 | default_web_concurrency = int(workers_per_core * multiprocessing.cpu_count())
8 | web_concurrency = os.getenv("WEB_CONCURRENCY", default_web_concurrency)
9 | host = os.getenv("HOST", "0.0.0.0")
10 | port = os.getenv("PORT", "80")
11 | bind = os.getenv("BIND", f"{host}:{port}")
12 | use_loglevel = os.getenv("LOG_LEVEL", "info")
13 | wsgi_app = os.getenv("APP_MODULE", "run:app")
14 | use_reload = False
15 | post_fork = "core.post_fork"
16 | on_starting = "core.on_starting_and_exit"
17 | on_exit = "core.on_starting_and_exit"
18 |
19 |
20 | if os.getenv("DEBUG", "false").lower() == "true":
21 | use_loglevel = "debug"
22 | use_reload = True
23 |
24 |
25 | # Gunicorn config variables
26 | loglevel = use_loglevel
27 | workers = web_concurrency
28 | reload = use_reload
29 | keepalive = 120
30 | timeout = 600
31 | errorlog = "-"
32 | accesslog = "-"
33 | log_file = "-"
34 | disable_redirect_access_to_syslog = True
35 |
36 | # For debugging and testing
37 | log_data = {
38 | "loglevel": loglevel,
39 | "workers": workers,
40 | "bind": bind,
41 | # Additional, non-gunicorn variables
42 | "workers_per_core": workers_per_core,
43 | "host": host,
44 | "port": port,
45 | }
46 |
47 |
48 | class CustomGunicornLogger(glogging.Logger):
49 | def setup(self, cfg):
50 | super().setup(cfg)
51 |
52 | # Add filters to Gunicorn logger
53 | logger = logging.getLogger("gunicorn.access")
54 | logger.addFilter(HealthCheckFilter())
55 |
56 |
57 | class HealthCheckFilter(logging.Filter):
58 | def filter(self, record):
59 | return "GET /api/isalive" not in record.getMessage()
60 |
61 |
62 | accesslog = "-"
63 | logger_class = CustomGunicornLogger
64 |
--------------------------------------------------------------------------------
/src/core/core/managers/data_manager.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from werkzeug.datastructures import FileStorage
3 | from core.config import Config
4 | from shutil import copy
5 |
6 |
7 | def get_files_by_suffix(suffix: str) -> list[Path]:
8 | return [file_path for file_path in Path(Config.DATA_FOLDER).rglob(f"*{suffix}") if file_path.is_file()]
9 |
10 |
11 | def add_file_to_subfolder(file: FileStorage, subfolder: str) -> None:
12 | if file.filename is None:
13 | raise ValueError("File must have a filename.")
14 |
15 | dest_folder = Path(Config.DATA_FOLDER) / subfolder
16 | dest_folder.mkdir(parents=True, exist_ok=True)
17 |
18 | dest_path = dest_folder / file.filename
19 | file.save(dest_path)
20 |
21 |
22 | def is_file_in_subfolder(subfolder: str, file_name: str) -> bool:
23 | full_path = Path(Config.DATA_FOLDER) / subfolder / file_name
24 | return full_path.is_file()
25 |
26 |
27 | def sync_presenter_templates_to_data() -> None:
28 | src = Path(__file__).parent.parent / "static" / "presenter_templates"
29 | dest = Path(Config.DATA_FOLDER) / "presenter_templates"
30 | dest.mkdir(parents=True, exist_ok=True)
31 |
32 | for file in filter(Path.is_file, src.glob("*")):
33 | dest_path = dest / file.name
34 | if not dest_path.exists():
35 | copy(file, dest_path)
36 |
37 |
38 | def get_presenter_template_path(presenter_template: str) -> str:
39 | path = Path(Config.DATA_FOLDER) / "presenter_templates" / presenter_template
40 | return path.absolute().as_posix()
41 |
42 |
43 | def get_presenter_templates() -> list[str]:
44 | path = Path(Config.DATA_FOLDER) / "presenter_templates"
45 | return [file.name for file in filter(Path.is_file, path.glob("*"))]
46 |
47 |
48 | def initialize(first_worker: bool) -> None:
49 | if first_worker:
50 | sync_presenter_templates_to_data()
51 |
--------------------------------------------------------------------------------
/src/core/core/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import contextlib
3 | from flask import Flask
4 | from flask_cors import CORS
5 |
6 | from core.managers import (
7 | db_manager,
8 | auth_manager,
9 | api_manager,
10 | log_manager,
11 | queue_manager,
12 | data_manager,
13 | )
14 |
15 | FLAG_FILENAME = "worker_init.flag"
16 | FIRST_WORKER = "gunicorn" not in os.environ.get("SERVER_SOFTWARE", "")
17 |
18 |
19 | def create_app():
20 | app = Flask(__name__)
21 | app.config.from_object("core.config.Config")
22 |
23 | with app.app_context():
24 | initialize_managers(app)
25 |
26 | return app
27 |
28 |
29 | def initialize_managers(app):
30 | global FIRST_WORKER
31 | CORS(app)
32 |
33 | if FIRST_WORKER:
34 | log_manager.logger.info(f"Connecting Database: {app.config.get('SQLALCHEMY_DATABASE_URI')}")
35 | db_manager.initialize(app, FIRST_WORKER)
36 | auth_manager.initialize(app)
37 | api_manager.initialize(app)
38 | queue_manager.initialize(app, FIRST_WORKER)
39 | data_manager.initialize(FIRST_WORKER)
40 |
41 | if FIRST_WORKER:
42 | log_manager.logger.info("All Managers initialized")
43 |
44 |
45 | def post_fork(server, worker):
46 | global FIRST_WORKER
47 | if not create_flag_file():
48 | FIRST_WORKER = False
49 | return
50 | FIRST_WORKER = True
51 | log_manager.logger.debug(f"Worker {worker.pid} is the first worker and will perform the one-time tasks.")
52 |
53 |
54 | def on_starting_and_exit(server):
55 | with contextlib.suppress(FileNotFoundError):
56 | os.remove(FLAG_FILENAME)
57 |
58 |
59 | def create_flag_file():
60 | flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
61 | try:
62 | file_descriptor = os.open(FLAG_FILENAME, flags)
63 | os.close(file_descriptor)
64 | return True
65 | except FileExistsError:
66 | return False
67 |
--------------------------------------------------------------------------------
/src/core/core/model/permission.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import or_
2 |
3 | from core.managers.db_manager import db
4 | from core.model.base_model import BaseModel
5 |
6 |
7 | class Permission(BaseModel):
8 | id = db.Column(db.String, primary_key=True)
9 | name = db.Column(db.String(), unique=True, nullable=False)
10 | description = db.Column(db.String())
11 |
12 | roles = db.relationship("Role", secondary="role_permission")
13 |
14 | def __init__(self, name, description, id=None):
15 | self.id = id
16 | self.name = name
17 | self.description = description
18 |
19 | @classmethod
20 | def add(cls, id, name, description) -> str:
21 | if permission := cls.get(id):
22 | return f"{permission.name} already exists."
23 | permission = cls(id=id, name=name, description=description)
24 | db.session.add(permission)
25 | db.session.commit()
26 | return f"Successfully created {permission.id}"
27 |
28 | @classmethod
29 | def get_all(cls):
30 | return cls.query.order_by(db.asc(Permission.id)).all()
31 |
32 | @classmethod
33 | def get_all_ids(cls):
34 | return [permission.id for permission in cls.get_all()]
35 |
36 | @classmethod
37 | def get_by_filter(cls, search):
38 | query = cls.query
39 |
40 | if search is not None:
41 | query = query.filter(
42 | or_(
43 | Permission.name.ilike(f"%{search}%"),
44 | Permission.description.ilike(f"%{search}%"),
45 | )
46 | )
47 |
48 | return query.order_by(db.asc(Permission.id)).all(), query.count()
49 |
50 | @classmethod
51 | def get_all_json(cls, search):
52 | permissions, count = cls.get_by_filter(search)
53 | items = [permission.to_dict() for permission in permissions]
54 | return {"total_count": count, "items": items}
55 |
--------------------------------------------------------------------------------
/src/gui/src/i18n/sk/messages.js:
--------------------------------------------------------------------------------
1 | export const messages_sk = {
2 | validations: {
3 | custom: {
4 | username: {
5 | required: 'Vyplňte prosím prihlasovacie meno'
6 | },
7 | password: {
8 | required: 'Heslo je povinné'
9 | },
10 | url: {
11 | required: 'URL je povinné'
12 | },
13 | key: {
14 | required: 'API kľúč je povinný'
15 | },
16 | name: {
17 | required: 'Názov je povinný'
18 | },
19 | parameter: {
20 | required: 'Parameter je povinný'
21 | },
22 | password_check: {
23 | required: 'Heslo je povinné',
24 | confirmed: 'Heslá sa nezhodujú'
25 | }
26 | }
27 | },
28 |
29 | login: {
30 | title: 'Prihláste sa',
31 | username: 'Meno',
32 | password: 'Heslo',
33 | submit: 'Prihlásiť',
34 | error: 'Nesprávne meno alebo heslo'
35 | },
36 |
37 | user_menu: {
38 | settings: 'Nastavenia profilu',
39 | logout: 'Odhlásiť sa'
40 | },
41 |
42 | main_menu: {
43 | assess: 'Zistiť',
44 | analyze: 'Analyzovať',
45 | publish: 'Publikovať',
46 | config: 'Konfigurácia',
47 | dashboard: 'Dashboard'
48 | },
49 |
50 | nav_menu: {
51 | newsitems: 'Nové zistenia',
52 | products: 'Produkty',
53 | publications: 'Publikácie',
54 | recent: 'Najnovšie',
55 | popular: 'Populárne',
56 | favourites: 'Obľúbené',
57 | configuration: 'Konfigurácia',
58 | osint_sources: 'OSINT zdroje',
59 | collectors: 'Zberače údajov'
60 | },
61 | osint_source: {
62 | add_new: 'Pridať nový OSINT zdroj',
63 | collector: 'Zberač údajov',
64 | add: 'Pridať',
65 | cancel: 'Zrušiť',
66 | validation_error: 'Prosím vyplňte všetky povinné polia',
67 | error: 'Nepodarilo sa vytvoriť zadaný zdroj.',
68 | name: 'Meno',
69 | description: 'Popis',
70 | successful: 'Nový OSINT zdroj bol úspešne pridaný'
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/gui/src/components/common/FormParameters.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 | mdi-help-circle
37 |
38 |
39 | {{ parameter.description }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
72 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/bot_tasks.py:
--------------------------------------------------------------------------------
1 | from celery import Task
2 |
3 | import worker.bots
4 | from worker.log import logger
5 | from worker.core_api import CoreApi
6 |
7 |
8 | class BotTask(Task):
9 | name = "bot_task"
10 | max_retries = 3
11 | default_retry_delay = 60
12 | time_limit = 1800
13 |
14 | def __init__(self):
15 | self.core_api = CoreApi()
16 | self.bots = {
17 | "analyst_bot": worker.bots.AnalystBot(),
18 | "grouping_bot": worker.bots.GroupingBot(),
19 | "tagging_bot": worker.bots.TaggingBot(),
20 | "wordlist_bot": worker.bots.WordlistBot(),
21 | "wordlist_updater_bot": worker.bots.WordlistUpdaterBot(),
22 | "nlp_bot": worker.bots.NLPBot(),
23 | "story_bot": worker.bots.StoryBot(),
24 | "ioc_bot": worker.bots.IOCBot(),
25 | "summary_bot": worker.bots.SummaryBot(),
26 | }
27 |
28 | def run(self, *args, bot_id: str, filter: dict | None = None):
29 | logger.info(f"Starting bot task {self.name}")
30 | bot_config = self.core_api.get_bot_config(bot_id)
31 | if not bot_config:
32 | logger.error(f"Bot with id {bot_id} not found")
33 | return
34 |
35 | self.execute_by_config(bot_config, filter)
36 | return
37 |
38 | def execute_by_config(self, bot_config: dict, filter: dict | None = None):
39 | bot_type = bot_config.get("type")
40 | if not bot_type:
41 | logger.error("Bot has no type")
42 | return
43 |
44 | bot = self.bots.get(bot_type)
45 | if not bot:
46 | return "Bot type not implemented"
47 |
48 | bot_params = bot_config.get("parameters")
49 | if not bot_params:
50 | logger.error("Bot with has no params")
51 | return
52 |
53 | if filter:
54 | bot_params |= filter
55 | return bot.execute(bot_params)
56 |
--------------------------------------------------------------------------------
/src/core/core/auth/base_authenticator.py:
--------------------------------------------------------------------------------
1 | from flask_jwt_extended import create_access_token
2 |
3 | from core.managers.log_manager import logger
4 | from core.model.token_blacklist import TokenBlacklist
5 | from core.model.user import User
6 |
7 |
8 | class BaseAuthenticator:
9 | def get_required_credentials(self):
10 | return []
11 |
12 | def get_authenticator_name(self):
13 | return ""
14 |
15 | def __str__(self):
16 | return f"Authenticator: {self.get_authenticator_name()}"
17 |
18 | def authenticate(self, credentials):
19 | return BaseAuthenticator.generate_error()
20 |
21 | def refresh(self, user):
22 | return BaseAuthenticator.generate_jwt(user.username)
23 |
24 | @staticmethod
25 | def logout(token):
26 | if token is not None:
27 | TokenBlacklist.add(token)
28 |
29 | @staticmethod
30 | def initialize(app):
31 | pass
32 |
33 | @staticmethod
34 | def generate_error() -> tuple[dict[str, str], int]:
35 | return {"error": "Authentication failed"}, 401
36 |
37 | @staticmethod
38 | def generate_jwt(username: str) -> tuple[dict[str, str], int]:
39 | if user := User.find_by_name(username):
40 | logger.store_user_activity(user, "LOGIN", "Successful")
41 | access_token = create_access_token(
42 | identity=user.username,
43 | additional_claims={
44 | "user_claims": {
45 | "id": user.id,
46 | "name": user.name,
47 | "organization_name": user.get_current_organization_name(),
48 | "permissions": user.get_permissions(),
49 | }
50 | },
51 | )
52 |
53 | return {"access_token": access_token}, 200
54 |
55 | logger.store_auth_error_activity(f"User doesn't exists: {username}")
56 | return BaseAuthenticator.generate_error()
57 |
--------------------------------------------------------------------------------
/src/worker/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "taranis-worker"
3 | description = "Taranis Queue Worker"
4 | readme = "README.md"
5 | license = {file = "LICENSE.md"}
6 | maintainers = [{ name = "AIT", email = "benjamin.akhras@ait.ac.at" }]
7 | classifiers = [
8 | "Programming Language :: Python",
9 | "Programming Language :: Python :: 3",
10 | "Programming Language :: Python :: 3.11",
11 | "Framework :: Celery",
12 | "Topic :: Security",
13 | ]
14 | requires-python = ">=3.11"
15 | dependencies = [
16 | # Core
17 | "celery==5.3.1",
18 | "click==8.1.6",
19 | "python-dotenv==1.0.0",
20 | "pydantic-settings",
21 | "requests==2.31.0",
22 | # Collector
23 | "beautifulsoup4==4.12.2",
24 | "trafilatura==1.6.1",
25 | "feedparser==6.0.10",
26 | "pyopenssl",
27 |
28 | # Publisher
29 | "pymisp==2.4.173",
30 | "paramiko==3.2.0",
31 | "python-gnupg==0.5.1",
32 | "tweepy==4.14.0",
33 |
34 | # Presenters
35 | "weasyprint==59.0",
36 |
37 | # Bots
38 | "ioc-finder",
39 | "ioc-fanger",
40 | "flair==0.12.2",
41 | "transformers==4.31.0",
42 | "py3langid==0.2.2",
43 | "nltk==3.8.1",
44 | "taranis-story-clustering==0.5.2",
45 | ]
46 | dynamic = ["version"]
47 |
48 | [project.optional-dependencies]
49 | dev = ["black", "pytest", "pytest-celery", "watchdog", "build", "wheel", "setuptools", "setuptools_scm"]
50 |
51 | [project.urls]
52 | "Source Code" = "https://github.com/ait-cs-IaaS/Taranis-NG"
53 |
54 | [build-system]
55 | requires = [
56 | "setuptools>=45",
57 | "setuptools_scm[toml]>=6.2",
58 | "wheel"
59 | ]
60 | build-backend = "setuptools.build_meta"
61 |
62 | [tool.setuptools_scm]
63 | fallback_version = "0.0.0"
64 | root = "../.."
65 |
66 | [tool.black]
67 | line-length = "142"
68 | target-version = ["py311"]
69 |
70 |
71 | [tool.pytest.ini_options]
72 | filterwarnings = [
73 | "ignore:.*_app_ctx_stack.*:DeprecationWarning",
74 | "ignore::DeprecationWarning"
75 | ]
76 |
--------------------------------------------------------------------------------
/src/worker/worker/config.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings
2 | from typing import Literal, Any
3 | from pydantic import model_validator
4 |
5 |
6 | class Settings(BaseSettings):
7 | class Config:
8 | env_file = ".env"
9 | env_file_encoding = "utf-8"
10 |
11 | API_KEY: str = "supersecret"
12 | TARANIS_CORE_URL: str = "http://taranis/api"
13 | MODULE_ID: str = "Workers"
14 | COLORED_LOGS: bool = True
15 | DEBUG: bool = False
16 | SSL_VERIFICATION: bool = False
17 | WORKER_TYPES: list[Literal["Bots", "Collectors", "Presenters", "Publishers"]] = ["Bots", "Collectors", "Presenters", "Publishers"]
18 | QUEUE_BROKER_SCHEME: Literal["amqp", "amqps"] = "amqp"
19 | QUEUE_BROKER_HOST: str = "localhost"
20 | QUEUE_BROKER_PORT: int = 5672
21 | QUEUE_BROKER_USER: str = "taranis"
22 | QUEUE_BROKER_PASSWORD: str = "supersecret"
23 | QUEUE_BROKER_URL: str | None = None
24 | QUEUE_BROKER_VHOST: str = "/"
25 | CELERY: dict[str, Any] | None = None
26 |
27 | @model_validator(mode="after")
28 | def set_celery(self):
29 | if self.CELERY and len(self.CELERY) > 1:
30 | return self
31 | if self.QUEUE_BROKER_URL:
32 | broker_url = self.QUEUE_BROKER_URL
33 | else:
34 | broker_url = (
35 | f"{self.QUEUE_BROKER_SCHEME}://{self.QUEUE_BROKER_USER}:{self.QUEUE_BROKER_PASSWORD}"
36 | f"@{self.QUEUE_BROKER_HOST}:{self.QUEUE_BROKER_PORT}/{self.QUEUE_BROKER_VHOST}"
37 | )
38 | self.CELERY = {
39 | "broker_url": broker_url,
40 | "ignore_result": True,
41 | "broker_connection_retry_on_startup": True,
42 | "broker_connection_retry": False, # To suppress deprecation warning
43 | "beat_scheduler": "worker.scheduler:RESTScheduler",
44 | "enable_utc": True,
45 | "worker_hijack_root_logger": False,
46 | }
47 | return self
48 |
49 |
50 | Config = Settings()
51 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/ioc_bot.py:
--------------------------------------------------------------------------------
1 | from .base_bot import BaseBot
2 | from worker.log import logger
3 | from ioc_finder import find_iocs
4 | import ioc_fanger
5 |
6 |
7 | class IOCBot(BaseBot):
8 | type = "IOC_BOT"
9 | name = "IOC Bot"
10 | description = "Bot for finding indicators of compromise in news items"
11 | included_ioc_types = ["bitcoin_addresses", "cves", "md5s", "sha1s", "sha256s", "sha512s", "ssdeeps", "registry_key_paths", "ipv4_cidrs"]
12 |
13 | def __init__(self):
14 | super().__init__()
15 |
16 | def execute(self, parameters=None):
17 | if not parameters:
18 | return
19 | try:
20 | if not (data := self.get_stories(parameters)):
21 | return "Error getting news items"
22 |
23 | extracted_keywords = {}
24 |
25 | for i, aggregate in enumerate(data):
26 | if attributes := aggregate.get("news_item_attributes", {}):
27 | if self.type in [d["key"] for d in attributes if "key" in d]:
28 | continue
29 | if i % max(len(data) // 10, 1) == 0:
30 | logger.debug(f"Extracting IOCs from {aggregate['id']}: {i}/{len(data)}")
31 | aggregate_content = " ".join(news_item["news_item_data"]["content"] for news_item in aggregate["news_items"])
32 | if iocs := self.extract_ioc(aggregate_content):
33 | extracted_keywords[aggregate["id"]] = iocs
34 |
35 | logger.debug(extracted_keywords)
36 | self.core_api.update_tags(extracted_keywords, self.type)
37 |
38 | except Exception:
39 | logger.log_debug_trace(f"Error running Bot: {self.type}")
40 |
41 | def extract_ioc(self, text: str):
42 | ioc_data = find_iocs(text=text, included_ioc_types=self.included_ioc_types)
43 | result = {}
44 | for key, iocs in ioc_data.items():
45 | for ioc in iocs:
46 | result[ioc_fanger.fang(str(ioc))] = {"tag_type": key}
47 |
48 | return result
49 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/wordlist_updater_bot.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from .base_bot import BaseBot
4 | from worker.log import logger
5 |
6 |
7 | class WordlistUpdaterBot(BaseBot):
8 | type = "WORDLIST_UPDATER_BOT"
9 | name = "Wordlist Updater Bot"
10 | description = "Bot for updating word lists"
11 |
12 | def execute(self, parameters: dict):
13 | word_list_id = parameters.get("WORD_LIST_ID")
14 |
15 | if not word_list_id:
16 | logger.error("No word list id provided")
17 | return "No word list id provided"
18 |
19 | word_list = self.core_api.get_word_list(word_list_id)
20 |
21 | if not word_list:
22 | logger.error(f"Word list with id {word_list_id} not found")
23 | return f"Word list with id {word_list_id} not found"
24 |
25 | url = word_list["link"]
26 | logger.info(f"Updating word list {word_list['name']} from {url}")
27 | response = requests.get(url=url)
28 | if not response.ok:
29 | logger.error(f"Failed to download word list {word_list['name']} - from {url} : {response.status_code}")
30 | return
31 |
32 | content_type = response.headers.get("content-type", "")
33 | if content_type == "text/csv" or url.endswith(".csv"):
34 | content_type = "text/csv"
35 | content = response.text
36 | elif content_type == "application/json" or url.endswith(".json"):
37 | content_type = "application/json"
38 | content = response.json()
39 | else:
40 | logger.error("Could not determine content type.")
41 | return
42 |
43 | logger.debug(f"Updating word list {word_list['name']} with {len(content)} entries - {content_type}")
44 |
45 | response = self.core_api.update_word_list(word_list["id"], content, content_type)
46 |
47 | if not response:
48 | logger.error(f"Failed to update word list {word_list['name']}")
49 | return
50 |
51 | logger.info(response)
52 | return response
53 |
--------------------------------------------------------------------------------
/src/core/core/api/publishers.py:
--------------------------------------------------------------------------------
1 | from flask_restx import Resource, reqparse, Namespace, Api
2 | from flask import request
3 |
4 | from core.managers.auth_manager import auth_required, api_key_required
5 | from core.model import publisher_preset
6 |
7 |
8 | class PublisherPresets(Resource):
9 | @auth_required("CONFIG_PUBLISHER_PRESET_ACCESS")
10 | def get(self):
11 | search = request.args.get(key="search", default=None)
12 | return publisher_preset.PublisherPreset.get_all_json(search)
13 |
14 | @api_key_required
15 | def post(self):
16 | parser = reqparse.RequestParser()
17 | parser.add_argument("type")
18 | parameters = parser.parse_args()
19 | return publisher_preset.PublisherPreset.add(parameters)
20 |
21 |
22 | class PublisherPreset(Resource):
23 | @auth_required("CONFIG_PUBLISHER_PRESET_ACCESS")
24 | def get(self):
25 | search = request.args.get(key="search", default=None)
26 | return publisher_preset.PublisherPreset.get_all_json(search)
27 |
28 | @auth_required("CONFIG_PUBLISHER_PRESET_CREATE")
29 | def post(self):
30 | pub_result = publisher_preset.PublisherPreset.add(request.json)
31 | return {"id": pub_result.id, "message": "Publisher preset created successfully"}, 200
32 |
33 | @auth_required("CONFIG_PUBLISHER_PRESET_UPDATE")
34 | def put(self, id):
35 | pub_result = publisher_preset.PublisherPreset.update(id, request.json)
36 | if not pub_result:
37 | return {"message": "Publisher preset not found"}, 404
38 | return {"id": pub_result, "message": "Publisher preset updated successfully"}, 200
39 |
40 | @auth_required("CONFIG_PUBLISHER_PRESET_DELETE")
41 | def delete(self, id):
42 | return publisher_preset.PublisherPreset.delete(id)
43 |
44 |
45 | def initialize(api: Api):
46 | namespace = Namespace("publishers", description="Publishers API")
47 | namespace.add_resource(PublisherPresets, "/presets")
48 | namespace.add_resource(PublisherPreset, "/preset", "/preset/")
49 | api.add_namespace(namespace, path="/publishers")
50 |
--------------------------------------------------------------------------------
/src/gui/src/api/analyze.js:
--------------------------------------------------------------------------------
1 | import { apiService } from '@/main'
2 |
3 | export function getAllReportItems(filter_data) {
4 | const filter = apiService.getQueryStringFromNestedObject(filter_data)
5 | return apiService.get(`/analyze/report-items?${filter}`)
6 | }
7 |
8 | export function getReportItem(report_item_id) {
9 | return apiService.get(`/analyze/report-items/${report_item_id}`)
10 | }
11 |
12 | export function createReportItem(data) {
13 | return apiService.post('/analyze/report-items', data)
14 | }
15 |
16 | export function deleteReportItem(report_item) {
17 | return apiService.delete(`/analyze/report-items/${report_item.id}`)
18 | }
19 |
20 | export function updateReportItem(report_item_id, data) {
21 | return apiService.put(`/analyze/report-items/${report_item_id}`, data)
22 | }
23 |
24 | export function addAggregatesToReportItem(report_item_id, data) {
25 | return apiService.post(
26 | `/analyze/report-items/${report_item_id}/aggregates`,
27 | data
28 | )
29 | }
30 |
31 | export function setAggregatesToReportItem(report_item_id, data) {
32 | return apiService.put(
33 | `/analyze/report-items/${report_item_id}/aggregates`,
34 | data
35 | )
36 | }
37 |
38 | export function getReportItemLocks(report_item_id) {
39 | return apiService.get(`/analyze/report-items/${report_item_id}/locks`)
40 | }
41 |
42 | export function lockReportItem(report_item_id, data) {
43 | return apiService.put(`/analyze/report-items/${report_item_id}/lock`, data)
44 | }
45 |
46 | export function unlockReportItem(report_item_id, data) {
47 | return apiService.put(`/analyze/report-items/${report_item_id}/unlock`, data)
48 | }
49 |
50 | export function getAllReportTypes() {
51 | return apiService.get('/analyze/report-types')
52 | }
53 |
54 | export function getAttributeEnums(filter) {
55 | return apiService.get(
56 | `/analyze/report-item-attributes/${filter.attribute_id}/enums?search=${filter.search}&offset=${filter.offset}&limit=${filter.limit}`
57 | )
58 | }
59 |
60 | export function removeAttachment(data) {
61 | return apiService.delete(
62 | `/analyze/report-items/${data.report_item_id}/file-attributes/${data.attribute_id}`
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/gui/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
60 |
61 |
62 |
63 |
64 |
85 |
--------------------------------------------------------------------------------
/src/gui/src/stores/MainStore.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { getLocalConfig } from '@/services/config'
3 | import { ref, computed } from 'vue'
4 |
5 | export const useMainStore = defineStore(
6 | 'main',
7 | () => {
8 | const user = ref({
9 | id: '',
10 | name: '',
11 | organization_name: '',
12 | permissions: []
13 | })
14 | const vertical_view = ref(false)
15 | const itemCountTotal = ref(0)
16 | const itemCountFiltered = ref(0)
17 | const drawerVisible = ref(true)
18 | const coreAPIURL = ref('/api')
19 | const sentryDSN = ref('')
20 | const gitInfo = ref('')
21 | const buildDate = ref(new Date().toISOString())
22 | const notification = ref({ message: '', type: '', show: false })
23 |
24 | // Getters
25 | const getItemCount = computed(() => {
26 | return { total: itemCountTotal.value, filtered: itemCountFiltered.value }
27 | })
28 |
29 | const updateFromLocalConfig = async () => {
30 | const config = await getLocalConfig()
31 | buildDate.value = config.BUILD_DATE ?? new Date().toISOString()
32 | gitInfo.value = config.GIT_INFO ?? ''
33 | coreAPIURL.value = config.TARANIS_CORE_API ?? '/api'
34 | sentryDSN.value = config.TARANIS_SENTRY_DSN ?? ''
35 | }
36 |
37 | // Actions
38 | const toggleDrawer = () => {
39 | drawerVisible.value = !drawerVisible.value
40 | }
41 |
42 | const resetItemCount = () => {
43 | itemCountTotal.value = 0
44 | itemCountFiltered.value = 0
45 | }
46 |
47 | const reset_user = () => {
48 | user.value = {
49 | id: '',
50 | name: '',
51 | organization_name: '',
52 | permissions: []
53 | }
54 | }
55 |
56 | return {
57 | user,
58 | vertical_view,
59 | drawerVisible,
60 | itemCountTotal,
61 | itemCountFiltered,
62 | coreAPIURL,
63 | gitInfo,
64 | buildDate,
65 | sentryDSN,
66 | notification,
67 | getItemCount,
68 | updateFromLocalConfig,
69 | toggleDrawer,
70 | resetItemCount,
71 | reset_user
72 | }
73 | },
74 | {
75 | persist: true
76 | }
77 | )
78 |
--------------------------------------------------------------------------------
/src/gui/src/utils/ListFilters.js:
--------------------------------------------------------------------------------
1 | export function filterSearch(fields, searchString) {
2 | let match = false
3 |
4 | const regexStr = searchString
5 | .trim()
6 | .match(/\\?.|^$/g)
7 | .reduce(
8 | (previousValue, currentValue) => {
9 | if (currentValue === '"') {
10 | previousValue.quote ^= 1
11 | } else if (!previousValue.quote && currentValue === ' ') {
12 | previousValue.a.push('')
13 | } else {
14 | previousValue.a[previousValue.a.length - 1] += currentValue.replace(
15 | /\\(.)/,
16 | '$1'
17 | )
18 | }
19 | return previousValue
20 | },
21 | { a: [''] }
22 | )
23 | .a.join('|')
24 |
25 | const searchRegEx = new RegExp(regexStr, 'gi')
26 |
27 | for (let i = 0; i < fields.length; i++) {
28 | if ([...fields[i].matchAll(searchRegEx)].length > 0) {
29 | match = true
30 | break
31 | }
32 | }
33 |
34 | return match
35 | }
36 |
37 | export function filterDateRange(publishedDate, selectedType, dateRange) {
38 | let range = []
39 | const today = new Date()
40 | switch (selectedType) {
41 | case 'today':
42 | range = [today.setHours(0, 0, 0, 0), today.setHours(23, 59, 59, 999)]
43 | break
44 | case 'week': {
45 | const currentDate = new Date()
46 | const timediff = today.getDate() - 7
47 | const oneWeekAgo = currentDate.setDate(timediff)
48 | range = [new Date(oneWeekAgo), new Date(today.setHours(23, 59, 59, 999))]
49 | break
50 | }
51 | case 'range':
52 | range = [
53 | new Date(dateRange[0]).setHours(0, 0, 0, 0),
54 | new Date(dateRange[1]).setHours(23, 59, 59, 999)
55 | ]
56 | break
57 | }
58 |
59 | return publishedDate >= range[0] && publishedDate <= range[1]
60 | }
61 |
62 | export function filterTags(itemTags, selectedTags, andOperator) {
63 | if (!selectedTags.length) return true
64 |
65 | const selectedTagExists = (selectedTag) =>
66 | itemTags.some((itemTag) => itemTag.label === selectedTag)
67 |
68 | return andOperator
69 | ? selectedTags.every(selectedTagExists)
70 | : selectedTags.some(selectedTagExists)
71 | }
72 |
--------------------------------------------------------------------------------
/src/gui/src/assets/centralize.css:
--------------------------------------------------------------------------------
1 | #app .user-settings-dialog .v-tabs .v-window.v-tabs-items {
2 | background-color: transparent !important;
3 | }
4 |
5 | #app .user-settings-dialog [role="tablist"] {
6 | top: 0;
7 | }
8 |
9 | .total-count-text {
10 | font-size: 13px;
11 | }
12 |
13 | .wordlist {
14 | background-color: yellow;
15 | font-weight: bold;
16 | }
17 |
18 | #app.theme--dark .wordlist {
19 | color: black;
20 | font-weight: bold;
21 | }
22 |
23 | #app.hide-wordlist span.wordlist {
24 | font-weight: unset;
25 | background: none;
26 | }
27 |
28 | thead tr th.text-left {
29 | min-width: 300px;
30 | }
31 |
32 | div.v-data-table>header {
33 | z-index: 1;
34 | top: 50px;
35 | box-shadow: 0 1px 0 gray;
36 | }
37 |
38 | [role='tablist'][data-booted='true'] {
39 | top: 0;
40 | }
41 |
42 | /* --- DashboardView.vue --- */
43 | .v-sheet--offset {
44 | top: -8px;
45 | position: relative;
46 | }
47 |
48 | .wordCloud {
49 | height: 200px;
50 | }
51 |
52 | .v-card .v-btn.next {
53 | position: absolute;
54 | right: 16px;
55 | }
56 |
57 | .v-card .chips {
58 | position: relative;
59 | text-align: right;
60 | right: 12px;
61 | }
62 |
63 | /* --- EnterView.vue --- */
64 | .nri {
65 | display: none;
66 | }
67 |
68 | .v-chip-group .v-chip.filter {
69 | height: 20px;
70 | margin-top: 5px;
71 | }
72 |
73 | /* --- MainMenu.vue --- */
74 | .app-header .v-toolbar__content {
75 | padding-right: 0;
76 | }
77 |
78 | header.app-header.border {
79 | border-bottom: 1px solid #b6b6b6 !important;
80 | }
81 |
82 | header.app-header.border.dark {
83 | border-bottom: 1px solid #484848 !important;
84 | }
85 |
86 | svg.logo {
87 | fill-rule: evenodd;
88 | clip-rule: evenodd;
89 | stroke-linejoin: round;
90 | stroke-miterlimit: 2;
91 | }
92 |
93 | .logo {
94 | --color-1: black;
95 | --color-2: white;
96 | --color-3: #4092dd;
97 | margin-top: 0.3em;
98 | }
99 |
100 | .logo.dark {
101 | --color-1: white;
102 | --color-2: black;
103 | }
104 |
105 | .app-header img.drw-btn {
106 | height: 18px;
107 | }
108 |
--------------------------------------------------------------------------------
/src/core/tests/functional/helpers.py:
--------------------------------------------------------------------------------
1 | class BaseTest:
2 | base_uri = "/api/v1"
3 |
4 | def assert_get_ok(self, client, uri, auth_header):
5 | response = client.get(f"{self.base_uri}/{uri}", headers=auth_header)
6 | return self.assert_json_ok(response)
7 |
8 | def assert_post_ok(self, client, uri, json_data, auth_header):
9 | response = client.post(f"{self.base_uri}/{uri}", json=json_data, headers=auth_header)
10 | return self.assert_json_ok(response)
11 |
12 | def assert_post_data_ok(self, client, uri, data, auth_header):
13 | auth_header["Content-type"] = "multipart/form-data"
14 | response = client.post(f"{self.base_uri}/{uri}", data=data, headers=auth_header)
15 | return self.assert_json_ok(response)
16 |
17 | def assert_get_files_ok(self, client, uri, auth_header):
18 | response = client.get(f"{self.base_uri}/{uri}", headers=auth_header)
19 | return self.assert_file_ok(response)
20 |
21 | def assert_put_ok(self, client, uri, json_data, auth_header):
22 | response = client.put(f"{self.base_uri}/{uri}", json=json_data, headers=auth_header)
23 | return self.assert_json_ok(response)
24 |
25 | def assert_delete_ok(self, client, uri, auth_header):
26 | response = client.delete(f"{self.base_uri}/{uri}", headers=auth_header)
27 | return self.assert_json_ok(response)
28 |
29 | def assert_file_ok(self, response):
30 | return self.assert_ok(response, "text/html; charset=utf-8")
31 |
32 | def assert_json_ok(self, response):
33 | return self.assert_ok(response, "application/json")
34 |
35 | def assert_ok(self, response, content_type):
36 | assert response
37 | assert response.content_type == content_type
38 | assert response.data
39 | assert 200 <= response.status_code < 300
40 | return response
41 |
42 | def assert_get_failed(self, client, uri):
43 | response = client.get(f"{self.base_uri}/{uri}")
44 | assert response
45 | assert response.content_type == "application/json"
46 | assert response.get_json()["error"] == "not authorized"
47 | assert response.status_code == 401
48 | return response
49 |
--------------------------------------------------------------------------------
/src/gui/src/stores/AnalyzeStore.js:
--------------------------------------------------------------------------------
1 | import { getAllReportItems, getAllReportTypes } from '@/api/analyze'
2 |
3 | import { defineStore } from 'pinia'
4 | import { useFilterStore } from './FilterStore'
5 | import { useI18n } from 'vue-i18n'
6 |
7 | export const useAnalyzeStore = defineStore('analyze', {
8 | state: () => ({
9 | report_items: { total_count: 0, items: [] },
10 | report_item_types: { total_count: 0, items: [] },
11 | selection_report: [],
12 | current_report_item_group_id: null
13 | }),
14 | getters: {
15 | getReportItemsList() {
16 | return this.report_items.items.map((item) => {
17 | return {
18 | title: item.title,
19 | value: item.id
20 | }
21 | })
22 | },
23 | getReportItemsTableData() {
24 | const { d } = useI18n()
25 |
26 | return this.report_items.items.map((item) => {
27 | return {
28 | id: item.id,
29 | completed: item.completed,
30 | title: item.title,
31 | created: d(item.created, 'long'),
32 | type: this.report_item_types.items.find(
33 | (type) => type.id === item.report_item_type_id
34 | )?.title,
35 | stories: item.stories
36 | }
37 | })
38 | }
39 | },
40 | actions: {
41 | async loadReportItems(data) {
42 | const response = await getAllReportItems(data)
43 | this.report_items = response.data
44 | },
45 |
46 | async updateReportItems() {
47 | const filter = useFilterStore()
48 | const response = await getAllReportItems(filter.reportFilter)
49 | this.report_items = response.data
50 | },
51 |
52 | async loadReportTypes(data) {
53 | const response = await getAllReportTypes(data)
54 | this.report_item_types = response.data
55 | },
56 |
57 | addSelectionReport(selected_item) {
58 | this.selection_report.push(selected_item)
59 | },
60 |
61 | removeSelectionReport(selectedItem) {
62 | for (let i = 0; i < this.selection_report.length; i++) {
63 | if (this.selection_report[i].id === selectedItem.id) {
64 | this.selection_report.splice(i, 1)
65 | break
66 | }
67 | }
68 | }
69 | }
70 | })
71 |
--------------------------------------------------------------------------------
/src/gui/src/components/popups/PopupShareItems.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Share Items
4 |
5 | Select a report to share the item with:
6 | {{ reportItemSelection }}
7 |
14 |
15 |
16 |
22 | abort
23 |
24 |
25 |
31 | share
32 |
33 |
34 |
35 |
36 |
37 |
84 |
--------------------------------------------------------------------------------
/.github/workflows/docker_build_gui.yaml:
--------------------------------------------------------------------------------
1 | name: GUI Docker image
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'src/gui/**'
7 | - 'docker/Dockerfile.gui'
8 | branches:
9 | - master
10 | - awake
11 | tags:
12 | - "**"
13 |
14 |
15 | permissions:
16 | packages: write
17 | contents: write
18 |
19 | jobs:
20 | build_npm:
21 | name: npm build
22 | runs-on: ubuntu-22.04
23 | steps:
24 | - uses: actions/checkout@v3
25 | - uses: actions/setup-node@v3
26 | with:
27 | node-version: 'latest'
28 | cache: 'npm'
29 | cache-dependency-path: |
30 | src/gui/package-lock.json
31 | - name: Install dependencies
32 | working-directory: src/gui
33 | run: npm ci
34 | - name: Rebuild the dist/ directory
35 | working-directory: src/gui
36 | run: npm run build
37 | - name: Package dist/
38 | working-directory: src/gui
39 | run: npm run package
40 | - name: Release
41 | uses: softprops/action-gh-release@v1
42 | if: startsWith(github.ref, 'refs/tags/v')
43 | with:
44 | files: |
45 | src/gui/taranis-ui*.tgz
46 |
47 | build_docker:
48 | name: build docker
49 | runs-on: ubuntu-22.04
50 | steps:
51 | - uses: actions/checkout@v3
52 | - name: Login to GitHub Container Registry
53 | uses: docker/login-action@v2
54 | with:
55 | registry: ghcr.io
56 | username: ${{ github.actor }}
57 | password: ${{ secrets.GITHUB_TOKEN }}
58 | - name: set build env
59 | run: |
60 | echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >>${GITHUB_ENV}
61 | echo "GIT_INFO=$(./docker/git_info.sh)" >>${GITHUB_ENV}
62 | - name: Set up Docker Buildx
63 | uses: docker/setup-buildx-action@v2
64 | - name: Build and push gui
65 | uses: docker/build-push-action@v4
66 | with:
67 | file: docker/Dockerfile.gui
68 | push: true
69 | tags: |
70 | ghcr.io/${{ env.GHCR_OWNER }}/taranis-gui:latest
71 | ghcr.io/${{ env.GHCR_OWNER }}/taranis-gui:${{ github.ref_name }}
72 | build-args: |
73 | git_info=${{ env.GIT_INFO }}
74 |
--------------------------------------------------------------------------------
/src/core/core/static/presenter_templates/text_template.txt:
--------------------------------------------------------------------------------
1 | {% for report_item in data.report_items %}
2 | {% if report_item.type != "Vulnerability Report" %}
3 | This template cannot be used for item of type "{{ report_item.type }}". It can only handle "Vulnerability Report".
4 | {% else %}
5 | # VULNERABILITY REPORT: {{ report_item.name_prefix }}{{ report_item.name }}
6 |
7 | ## Vulnerability
8 |
9 | CVSS VECTOR: {% if report_item.attributes.cvss %}{{ report_item.attributes.cvss | default('unspecified') }}{% endif %}
10 | TLP: {{ report_item.attributes.tlp }}
11 | CONFIDENTIALITY: {{ report_item.attributes.confidentiality | default('Public') }}
12 | DESCRIPTION: {{ report_item.attributes.description }}
13 | FIRST PUBLISHED: {{ report_item.attributes.exposure_date }}
14 | UPDATED: {{ report_item.attributes.update_date }}
15 | CVE: {% for cve in report_item.attributes.cve %}{{ cve }}{{ ", " if loop.last else "" }}{% endfor %}
16 | IMPACT:
17 | {% for impact in report_item.attributes.impact -%}
18 | - {{ impact }}
19 | {% endfor %}
20 |
21 | ## Identify and act
22 |
23 | {% if report_item.attributes.ioc is defined and report_item.attributes.ioc|length > 0 -%}
24 |
25 | ### IOC
26 |
27 | {% for ioc in report_item.attributes.ioc -%}
28 | - {{ ioc }}
29 | {% endfor %}
30 | {%- endif %}
31 | {% if report_item.attributes.affected_systems is defined and report_item.attributes.affected_systems|length > 0 -%}
32 |
33 | ### Affected systems
34 |
35 | {% for entry in report_item.attributes.affected_systems -%}
36 | - {{ entry }}
37 | {% endfor %}
38 | {%- endif %}
39 | {% if report_item.attributes.recommendations is defined and report_item.attributes.recommendations != "" -%}
40 |
41 | ### Recommendations
42 |
43 | {{ report_item.attributes.recommendations }}
44 | {% endif %}
45 | {% if report_item.attributes.links is defined and report_item.attributes.links|length > 1 %}
46 |
47 | ### Links
48 |
49 | {% for entry in report_item.attributes.links %}
50 | - {{ entry }}
51 | {% endfor %}
52 | {% endif %}
53 | {% if false %}
54 |
55 | ## Associated news items
56 |
57 | {% for news_item in report_item.news_items %}
58 | - Title: {{ news_item.title }}
59 | Author: {{ news_item.author }}
60 | Source: {{ news_item.source }}
61 | Link: {{ news_item.link }}
62 | Published: {{ news_item.published }}
63 | {% endfor %}
64 | {% endif %}
65 |
66 | {% endif %}
67 | {% endfor %}
68 |
--------------------------------------------------------------------------------
/src/gui/src/components/assess/filter/filterSortList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 | {{ option.label }}
14 |
15 |
16 |
17 |
18 |
89 |
--------------------------------------------------------------------------------
/src/core/core/model/base_model.py:
--------------------------------------------------------------------------------
1 | from typing import Any, TypeVar, Type
2 |
3 | from enum import Enum
4 | from core.managers.db_manager import db
5 | from datetime import datetime
6 | import json
7 |
8 | T = TypeVar("T", bound="BaseModel")
9 |
10 |
11 | class BaseModel(db.Model):
12 | __abstract__ = True
13 |
14 | def __str__(self) -> str:
15 | return f"{self.__class__.__name__} {self.to_json()}"
16 |
17 | def __repr__(self) -> str:
18 | return f"{self.__class__.__name__} {self.to_json()}"
19 |
20 | @classmethod
21 | def delete(cls: Type[T], id) -> tuple[dict[str, Any], int]:
22 | if cls.query.filter_by(id=id).delete():
23 | db.session.commit()
24 | return {"message": f"{cls.__name__} {id} deleted"}, 200
25 |
26 | return {"error": f"{cls.__name__} {id} not found"}, 404
27 |
28 | @classmethod
29 | def add(cls: Type[T], data) -> T:
30 | item = cls.from_dict(data)
31 | db.session.add(item)
32 | db.session.commit()
33 | return item
34 |
35 | @classmethod
36 | def add_multiple(cls: Type[T], json_data) -> list[T]:
37 | result = []
38 | for data in json_data:
39 | item = cls.from_dict(data)
40 | db.session.add(item)
41 | result.append(item)
42 |
43 | db.session.commit()
44 | return result
45 |
46 | @classmethod
47 | def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
48 | return cls(**data)
49 |
50 | @classmethod
51 | def load_multiple(cls: Type[T], json_data: list[dict[str, Any]]) -> list[T]:
52 | return [cls.from_dict(data) for data in json_data]
53 |
54 | def to_dict(self) -> dict[str, Any]:
55 | data = {c.name: getattr(self, c.name) for c in self.__table__.columns}
56 | for key, value in data.items():
57 | if isinstance(value, datetime):
58 | data[key] = value.isoformat()
59 | elif isinstance(value, Enum):
60 | data[key] = value.value
61 | return data
62 |
63 | def to_json(self) -> str:
64 | return json.dumps(self.to_dict())
65 |
66 | @classmethod
67 | def get(cls: Type[T], id) -> T | None:
68 | if isinstance(id, int) and (id < 0 or id > 2**63 - 1):
69 | return None
70 | return cls.query.get(id)
71 |
72 | @classmethod
73 | def get_all(cls: Type[T]) -> list[T] | None:
74 | return cls.query.all()
75 |
--------------------------------------------------------------------------------
/src/worker/worker/publishers/publisher_tasks.py:
--------------------------------------------------------------------------------
1 | from celery import Task
2 |
3 | import worker.publishers
4 | from worker.publishers.base_publisher import BasePublisher
5 | from worker.log import logger
6 | from worker.core_api import CoreApi
7 | from requests.exceptions import ConnectionError
8 |
9 |
10 | class PublisherTask(Task):
11 | name = "publisher_task"
12 | max_retries = 3
13 | default_retry_delay = 60
14 | time_limit = 60
15 |
16 | def __init__(self):
17 | self.core_api = CoreApi()
18 | self.publishers = {
19 | "email_publisher": worker.publishers.EMAILPublisher(),
20 | "twitter_publisher": worker.publishers.TWITTERPublisher(),
21 | "wordpress_publisher": worker.publishers.WORDPRESSPublisher(),
22 | "ftp_publisher": worker.publishers.FTPPublisher(),
23 | }
24 |
25 | def get_product(self, product_id: int) -> tuple[dict[str, str] | None, str | None]:
26 | try:
27 | product = self.core_api.get_product(product_id)
28 | except ConnectionError as e:
29 | logger.critical(e)
30 | return None, str(e)
31 |
32 | if not product:
33 | logger.error(f"Product with id {product_id} not found")
34 | return None, f"Product with id {product_id} not found"
35 | return product, None
36 |
37 | def get_publisher(self, product) -> tuple[BasePublisher | None, str | None]:
38 | publisher_type = product.get("type")
39 | if not publisher_type:
40 | logger.error(f"Product {product['id']} has no publisher_type")
41 | return None, f"Product {product['id']} has no publisher_type"
42 |
43 | if publisher := self.publishers.get(publisher_type):
44 | return publisher, None
45 |
46 | return None, f"Publisher {publisher_type} not implemented"
47 |
48 | def run(self, product_id: int):
49 | err = None
50 |
51 | product, err = self.get_product(product_id)
52 | if err or not product:
53 | return err
54 |
55 | logger.debug(f"Rendering product {product}")
56 |
57 | publisher, err = self.get_publisher(product)
58 | if err or not publisher:
59 | return err
60 |
61 | published_product = publisher.publish(product)
62 | if not published_product:
63 | return "Error generating product"
64 | if "error" in published_product:
65 | return published_product["error"]
66 |
67 | return "Product published successfully"
68 |
--------------------------------------------------------------------------------
/src/gui/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import { createVuetify } from 'vuetify'
2 | import 'vuetify/lib/styles/main.sass'
3 | import { aliases, mdi } from 'vuetify/iconsets/mdi'
4 | import * as components from 'vuetify/components'
5 | import * as directives from 'vuetify/directives'
6 | import { VDataTable, VDataTableServer } from 'vuetify/labs/VDataTable'
7 | import { VInfiniteScroll } from 'vuetify/labs/VInfiniteScroll'
8 |
9 | const dark = {
10 | dark: true,
11 | colors: {
12 | primary: '#7468E8',
13 | secondary: '#34a5e8',
14 | accent: '#cfcde5',
15 | info: '#2196F3',
16 | error: '#FF5252',
17 | success: '#4CAF50',
18 | warning: '#FFC107',
19 | grey: '#C9C9C9',
20 | background: '#f3f3f3',
21 | 'cx-app-header': '#E6E6E6',
22 | 'cx-toolbar-filter': '#ffffff',
23 | 'cx-combo-gray': '#f2f2f2',
24 | 'cx-user-menu': '#d9d9d9',
25 | 'cx-drawer-bg': '#ffffff',
26 | 'cx-drawer-text': '#000000',
27 | 'cx-drawer-text-invert': '#000',
28 | 'cx-line': '#fafafa',
29 | 'cx-favorites': '#ff9d48',
30 | 'cx-filter': '#9f02ff',
31 | 'cx-wordlist': '#FFC107',
32 | 'dark-grey': '#575757',
33 | 'awake-green-color': '#5d9458',
34 | 'awake-yellow-color': '#E9C645',
35 | 'awake-red-color': '#b65f5f'
36 | }
37 | }
38 |
39 | const light = {
40 | dark: false,
41 | colors: {
42 | primary: '#7468E8',
43 | secondary: '#E9C645',
44 | accent: '#cfcde5',
45 | info: '#2196F3',
46 | error: '#ba3b3b',
47 | success: '#4CAF50',
48 | warning: '#FFC107',
49 | grey: '#C9C9C9',
50 | background: '#f3f3f3',
51 | 'cx-app-header': '#E6E6E6',
52 | 'cx-toolbar-filter': '#ffffff',
53 | 'cx-combo-gray': '#f2f2f2',
54 | 'cx-user-menu': '#d9d9d9',
55 | 'cx-drawer-bg': '#ffffff',
56 | 'cx-drawer-text': '#000000',
57 | 'cx-drawer-text-invert': '#000',
58 | 'cx-line': '#fafafa',
59 | 'cx-favorites': '#ff9d48',
60 | 'cx-filter': '#9f02ff',
61 | 'cx-wordlist': '#FFC107',
62 | 'dark-grey': '#575757',
63 | 'awake-green-color': '#5d9458',
64 | 'awake-yellow-color': '#E9C645',
65 | 'awake-red-color': '#b65f5f'
66 | }
67 | }
68 |
69 | const theme = {
70 | defaultTheme: 'light',
71 | themes: {
72 | dark,
73 | light
74 | }
75 | }
76 |
77 | export const vuetify = createVuetify({
78 | components: {
79 | ...components,
80 | VDataTable,
81 | VDataTableServer,
82 | VInfiniteScroll
83 | },
84 | directives,
85 | theme: theme,
86 | icons: {
87 | defaultSet: 'mdi',
88 | aliases,
89 | sets: {
90 | mdi
91 | }
92 | }
93 | })
94 |
--------------------------------------------------------------------------------
/.github/workflows/docker_build_worker.yaml:
--------------------------------------------------------------------------------
1 | name: Worker Docker image
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'src/worker/**'
7 | - 'docker/Dockerfile.worker'
8 | branches:
9 | - master
10 | - awake
11 | tags:
12 | - "**"
13 |
14 | permissions:
15 | packages: write
16 |
17 | jobs:
18 | test:
19 | name: pytest
20 | runs-on: ubuntu-22.04
21 | steps:
22 | - uses: actions/checkout@v3
23 | - uses: actions/setup-python@v4
24 | with:
25 | python-version: "3.11"
26 |
27 | - name: Cache pip dependencies
28 | uses: actions/cache@v3
29 | with:
30 | path: |
31 | ~/.cache/pip
32 | key: ${{ hashFiles('src/worker/pyproject.toml') }}
33 |
34 | - name: Install dependencies
35 | working-directory: src/worker
36 | run: pip install -e .[dev]
37 |
38 | - name: test
39 | working-directory: src/worker
40 | run: |
41 | pip install -e .[dev]
42 | echo "Starting Pytest"
43 | pytest
44 |
45 | build_wheel:
46 | name: build wheel
47 | runs-on: ubuntu-22.04
48 | needs: test
49 | steps:
50 | - uses: actions/checkout@v3
51 | - uses: actions/setup-python@v4
52 | with:
53 | python-version: "3.11"
54 | - name: build
55 | working-directory: src/worker
56 | run: |
57 | python -m pip install --upgrade pip setuptools wheel build
58 | python -m build
59 | - name: Release
60 | uses: softprops/action-gh-release@v1
61 | if: startsWith(github.ref, 'refs/tags/v')
62 | with:
63 | files: |
64 | src/worker/dist/taranis_worker-*.whl
65 | src/worker/dist/taranis_worker-*.tar.gz
66 |
67 | build_docker:
68 | name: build docker
69 | runs-on: ubuntu-22.04
70 | needs: test
71 | steps:
72 | - name: Login to GitHub Container Registry
73 | uses: docker/login-action@v2
74 | with:
75 | registry: ghcr.io
76 | username: ${{ github.actor }}
77 | password: ${{ secrets.GITHUB_TOKEN }}
78 | - name: get ghcr owner repository
79 | run: |
80 | echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >>${GITHUB_ENV}
81 | - name: Build and push worker image
82 | uses: docker/build-push-action@v4
83 | with:
84 | file: docker/Dockerfile.worker
85 | push: true
86 | tags: |
87 | ghcr.io/${{ env.GHCR_OWNER }}/taranis-worker:latest
88 | ghcr.io/${{ env.GHCR_OWNER }}/taranis-worker:${{ github.ref_name }}
89 |
--------------------------------------------------------------------------------
/src/worker/worker/bots/analyst_bot.py:
--------------------------------------------------------------------------------
1 | import re
2 | import datetime
3 |
4 | from .base_bot import BaseBot
5 | from worker.log import logger
6 |
7 |
8 | class AnalystBot(BaseBot):
9 | type = "ANALYST_BOT"
10 | name = "Analyst Bot"
11 | description = "Bot for news items analysis"
12 |
13 | regexp = []
14 | attr_name = []
15 | news_items = []
16 | news_items_data = []
17 |
18 | def execute(self, parameters=None):
19 | if not parameters:
20 | return
21 | try:
22 | regex = parameters.get("REGULAR_EXPRESSION", "")
23 | attr = parameters.get("ATTRIBUTE_NAME", "")
24 | if not regex or not attr:
25 | return
26 |
27 | self.regexp = regex.replace(" ", "").split(",")
28 | self.attr_name = attr.replace(" ", "").split(",")
29 |
30 | bots_params = dict(zip(self.regexp, self.attr_name))
31 | limit = (datetime.datetime.now() - datetime.timedelta(days=7)).isoformat()
32 |
33 | if news_items_data := self.core_api.get_news_items_data(limit):
34 | for item in news_items_data:
35 | if not item:
36 | continue
37 | news_item_id = item["id"]
38 | title = item["title"]
39 | review = item["review"]
40 | content = item["content"]
41 |
42 | analyzed_text = set((title + review + content).split())
43 |
44 | for element in analyzed_text:
45 | attributes = []
46 | for key, value in bots_params.items():
47 | if finding := re.search(f"({value})", element.strip(".,")):
48 | value = finding[1]
49 | binary_mime_type = ""
50 | binary_value = ""
51 |
52 | news_attribute = {
53 | "key": key,
54 | "value": value,
55 | "binary_mime_type": binary_mime_type,
56 | "binary_value": binary_value,
57 | }
58 |
59 | attributes.append(news_attribute)
60 |
61 | self.core_api.update_news_item_attributes(
62 | news_item_id,
63 | attributes,
64 | )
65 |
66 | except Exception:
67 | logger.log_debug_trace(f"Error running Bot: {self.type}")
68 |
--------------------------------------------------------------------------------
/src/core/tests/functional/osint_sources_test_data_v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "data": [
4 | {
5 | "name": "BSI Bund",
6 | "description": "",
7 | "type": "rss_collector",
8 | "parameters": [
9 | {
10 | "FEED_URL": "https://www.bsi.bund.de/SiteGlobals/Functions/RSSFeed/RSSNewsfeed/RSSNewsfeed.xml"
11 | }
12 | ]
13 | },
14 | {
15 | "name": "cert.at Aktuelles",
16 | "description": "",
17 | "type": "rss_collector",
18 | "parameters": [
19 | {
20 | "FEED_URL": "https://cert.at/cert-at.de.current.rss_2.0.xml"
21 | },
22 | {
23 | "CONTENT_LOCATION": "description"
24 | }
25 | ]
26 | },
27 | {
28 | "name": "cert.at Blog",
29 | "description": "",
30 | "type": "rss_collector",
31 | "parameters": [
32 | {
33 | "FEED_URL": "https://cert.at/cert-at.de.blog.rss_2.0.xml"
34 | },
35 | {
36 | "CONTENT_LOCATION": "description"
37 | }
38 | ]
39 | },
40 | {
41 | "name": "cert.at Tagesberichte",
42 | "description": "",
43 | "type": "rss_collector",
44 | "parameters": [
45 | {
46 | "FEED_URL": "https://cert.at/cert-at.de.daily.rss_2.0.xml"
47 | },
48 | {
49 | "CONTENT_LOCATION": "description"
50 | }
51 | ]
52 | },
53 | {
54 | "name": "cert.at Warnings",
55 | "description": "",
56 | "type": "rss_collector",
57 | "parameters": [
58 | {
59 | "FEED_URL": "https://cert.at/cert-at.de.warnings.rss_2.0.xml"
60 | },
61 | {
62 | "CONTENT_LOCATION": "description"
63 | },
64 | {
65 | "REFRESH_INTERVAL": "60"
66 | }
67 | ]
68 | },
69 | {
70 | "name": "CZ.NIC",
71 | "description": "",
72 | "type": "rss_collector",
73 | "parameters": [
74 | {
75 | "FEED_URL": "http://en.blog.nic.cz:80/feed/"
76 | },
77 | {
78 | "CONTENT_LOCATION": "content:encoded"
79 | }
80 | ]
81 | }
82 | ]
83 | }
--------------------------------------------------------------------------------
/src/core/core/static/presenter_templates/misp_template.json:
--------------------------------------------------------------------------------
1 | {
2 | {% for report_item in data.report_items %}
3 | "threat_level_id": {% if report_item.attributes.event_threat_level == 'High' %}"1"{% endif %}{% if report_item.attributes.event_threat_level == 'Medium' %}"2"{% endif %}{% if report_item.attributes.event_threat_level == 'Low' %}"3"{% endif %}{% if report_item.attributes.event_threat_level == 'Undefined' %}"4"{% endif %},
4 | "info": {% if report_item.attributes.event_info %}"{{ report_item.attributes.event_info }}"{% endif %},
5 | "published": false,
6 | "distribution": {% if report_item.attributes.event_distribution == 'Your Organisation only' %}"0"{% endif %}{% if report_item.attributes.event_distribution == 'This Community Only' %}"1"{% endif %}{% if report_item.attributes.event_distribution == 'Conected Communities' %}"2"{% endif %}{% if report_item.attributes.event_distribution == 'All Communities' %}"3"{% endif %}{% if report_item.attributes.event_distribution == 'Sharing Group' %}"4"{% endif %},
7 | "analysis": {% if report_item.attributes.event_analysis == 'Initial' %}"0"{% endif %}{% if report_item.attributes.event_analysis == 'Ongoing' %}"1"{% endif %}{% if report_item.attributes.event_analysis == 'Complete' %}"2"{% endif %},
8 | "Attribute": [
9 | {
10 | "type": {% if report_item.attributes.attribute_type %}"{{ report_item.attributes.attribute_type }}"{% endif %},
11 | "category": {% if report_item.attributes.attribute_category %}"{{ report_item.attributes.attribute_category }}"{% endif %},
12 | "to_ids": {% if report_item.attributes.attribute_additional_information == 'For Intrusion Detection System' %}true{% else %}false{% endif %},
13 | "disable_correlation": {% if report_item.attributes.attribute_additional_information == 'Disable Correlation' %}true{% else %}false{% endif %},
14 | "distribution": {% if report_item.attributes.attribute_distribution == 'Your Organisation Only' %}"0"{% endif %}{% if report_item.attributes.attribute_distribution == 'This Community Only' %}"1"{% endif %}{% if report_item.attributes.attribute_distribution == 'Connected Communities' %}"2"{% endif %}{% if report_item.attributes.attribute_distribution == 'All Communities' %}"3"{% endif %}{% if report_item.attributes.attribute_distribution == 'Sharing Group' %}"4"{% endif %}{% if report_item.attributes.attribute_distribution == 'Inherit Event' %}"5"{% endif %},
15 | "comment": {% if report_item.attributes.attribute_contextual_comment %}"{{ report_item.attributes.attribute_contextual_comment }}"{% endif %},
16 | "value": {% if report_item.attributes.attribute_value %}"{{ report_item.attributes.attribute_value }}"{% endif %}
17 | }
18 | ]
19 | {% endfor %}
20 | }
21 |
--------------------------------------------------------------------------------
/src/gui/src/services/api_service.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { useAuthStore } from '@/stores/AuthStore'
3 |
4 | export class ApiService {
5 | constructor(baseURL) {
6 | this._axios = axios.create({
7 | baseURL: baseURL
8 | })
9 | this.authStore = useAuthStore()
10 | this.setHeader()
11 | }
12 |
13 | setHeader() {
14 | if (localStorage.ACCESS_TOKEN) {
15 | this._axios.defaults.headers.common.Authorization = `Bearer ${localStorage.ACCESS_TOKEN}`
16 | } else {
17 | this._axios.defaults.headers.common = {}
18 | }
19 | }
20 |
21 | getQueryStringFromObject(filterObject) {
22 | return Object.entries(filterObject)
23 | .filter(([, val]) => val != null)
24 | .map(([key, val]) => `${key}=${val}`)
25 | .join('&')
26 | }
27 |
28 | encodeAmpersand(value) {
29 | return value.replace(/&/g, '%2526')
30 | }
31 |
32 | getQueryStringFromNestedObject(filterObject) {
33 | if (!filterObject) {
34 | return ''
35 | }
36 | return Object.entries(filterObject)
37 | .filter(([, val]) => val != null)
38 | .map(([key, val]) => {
39 | if (Array.isArray(val)) {
40 | return val.map((v) => `${key}=${this.encodeAmpersand(v)}`).join('&')
41 | }
42 | if (typeof val === 'object') {
43 | return this.getQueryStringFromObject(val)
44 | }
45 | return `${key}=${val}`
46 | })
47 | .join('&')
48 | }
49 |
50 | async get(resource) {
51 | return await this._axios.get(resource).catch((error) => {
52 | if (error.response.status === 401) {
53 | console.error('Redirect to login')
54 |
55 | this.authStore.logout()
56 | }
57 | })
58 | }
59 |
60 | post(resource, data) {
61 | return this._axios.post(resource, data)
62 | }
63 |
64 | put(resource, data) {
65 | return this._axios.put(resource, data)
66 | }
67 |
68 | delete(resource) {
69 | return this._axios.delete(resource)
70 | }
71 |
72 | upload(resource, form_data) {
73 | return this._axios.post(resource, form_data, {
74 | headers: {
75 | 'Content-Type': 'multipart/form-data'
76 | }
77 | })
78 | }
79 |
80 | async download(resource, file_name) {
81 | try {
82 | const response = await this._axios.get(resource, {
83 | responseType: 'blob'
84 | })
85 | const fileURL = window.URL.createObjectURL(new Blob([response.data]))
86 | const fileLink = document.createElement('a')
87 | fileLink.href = fileURL
88 | fileLink.setAttribute('download', file_name)
89 | document.body.appendChild(fileLink)
90 | fileLink.click()
91 | document.body.removeChild(fileLink)
92 | } catch (error) {
93 | console.error(error)
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/worker/worker/scheduler.py:
--------------------------------------------------------------------------------
1 | import time
2 | from celery.beat import Scheduler, ScheduleEntry
3 | from datetime import datetime, timezone
4 | from worker.log import logger
5 |
6 | from worker.core_api import CoreApi
7 |
8 |
9 | class RESTScheduleEntry(ScheduleEntry):
10 | def is_due(self):
11 | return super().is_due()
12 |
13 | def next(self):
14 | return super().next()
15 |
16 |
17 | class RESTScheduler(Scheduler):
18 | Entry = RESTScheduleEntry
19 | schedule_from_core = {}
20 |
21 | def __init__(self, *args, **kwargs):
22 | super().__init__(*args, **kwargs)
23 | self.core_api = CoreApi()
24 | self.last_checked = datetime.now(timezone.utc)
25 | self.max_interval = 60
26 |
27 | def update_schedule_from_core(self):
28 | if not (core_schedule := self.core_api.get_schedule()):
29 | return
30 | for entry in core_schedule:
31 | entry["app"] = self.app
32 | if last_run_at := entry.get("last_run_at"):
33 | entry["last_run_at"] = datetime.fromisoformat(last_run_at)
34 | rest_entry = self.Entry(**entry)
35 |
36 | if schedule_entry := self.schedule.get(entry["name"]):
37 | if rest_entry.schedule != schedule_entry.schedule:
38 | logger.debug(f"Changed schedule of {entry['name']}")
39 | self.schedule[entry["name"]] = rest_entry
40 | else:
41 | logger.debug(f"Adding new entry: {rest_entry}")
42 | self.schedule[entry["name"]] = rest_entry
43 | return self.schedule
44 |
45 | def sync(self):
46 | current_datetime = datetime.now()
47 | estimates = {}
48 | for s in self.schedule.values():
49 | try:
50 | estimate = s.schedule.remaining_estimate(s.last_run_at)
51 | next_run_time = current_datetime + estimate
52 | estimates[s.name] = next_run_time.isoformat()
53 | except Exception:
54 | logger.log_debug_trace(f"Failed to sync {s.name}")
55 | # logger.debug(f"Updating next run times: {estimates}")
56 | self.core_api.update_next_run_time(estimates)
57 |
58 | def get_schedule(self):
59 | return self.schedule_from_core
60 |
61 | def set_schedule(self, schedule):
62 | self.schedule_from_core = schedule
63 |
64 | schedule = property(get_schedule, set_schedule)
65 |
66 | def tick(self):
67 | now = datetime.now(timezone.utc)
68 | if (now - self.last_checked).total_seconds() >= self.max_interval:
69 | self.last_checked = now
70 | self.update_schedule_from_core()
71 | self.sync()
72 | time.sleep(0.1)
73 | super().tick()
74 |
--------------------------------------------------------------------------------