├── 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 | 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 | 8 | 9 | 26 | -------------------------------------------------------------------------------- /src/gui/src/views/nav/UserNav.vue: -------------------------------------------------------------------------------- 1 | 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 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/gui/src/views/users/ClusterView.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /src/gui/src/components/common/DateInput.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/gui/src/views/users/AssetGroupView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | -------------------------------------------------------------------------------- /src/gui/src/components/common/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 57 | 58 | 64 | -------------------------------------------------------------------------------- /src/gui/src/components/common/TrendingCard.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 13 | 14 | 63 | -------------------------------------------------------------------------------- /src/gui/src/components/config/ImportExport.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /src/gui/src/views/users/ProductView.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 11 | 12 | 61 | -------------------------------------------------------------------------------- /src/gui/src/components/assess/card/votes.vue: -------------------------------------------------------------------------------- 1 | 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 | 11 | 12 | 67 | -------------------------------------------------------------------------------- /src/gui/src/views/users/ReportView.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------