├── invitations └── README.md ├── hermes_ui ├── moteur │ └── __init__.py ├── migrations │ ├── __init__.py │ ├── versions │ │ └── __init__.py │ ├── README │ ├── script.py.mako │ ├── alembic.ini │ └── env.py ├── templates │ ├── admin │ │ ├── master.html │ │ ├── static.html │ │ ├── model │ │ │ ├── inline_form.html │ │ │ ├── inline_field_list.html │ │ │ ├── row_actions.html │ │ │ ├── modals │ │ │ │ ├── create.html │ │ │ │ ├── edit.html │ │ │ │ └── details.html │ │ │ ├── create.html │ │ │ ├── edit.html │ │ │ ├── layout.html │ │ │ ├── details.html │ │ │ └── inline_list_base.html │ │ ├── file │ │ │ ├── form.html │ │ │ └── modals │ │ │ │ └── form.html │ │ ├── rediscli │ │ │ ├── console.html │ │ │ └── response.html │ │ ├── actions.html │ │ ├── adminlte │ │ │ └── forms.html │ │ └── layout.html │ └── security │ │ ├── _messages.html │ │ ├── _menu.html │ │ ├── _macros.html │ │ ├── forgot_password.html │ │ ├── change_password.html │ │ ├── register_user.html │ │ └── login_user.html ├── db │ ├── __init__.py │ └── polymorphic.py ├── babel.cfg ├── assets │ ├── styles │ │ ├── jstree │ │ │ └── proton │ │ │ │ ├── 30px.png │ │ │ │ ├── 32px.png │ │ │ │ ├── throbber.gif │ │ │ │ └── fonts │ │ │ │ └── titillium │ │ │ │ ├── titilliumweb-bold-webfont.eot │ │ │ │ ├── titilliumweb-bold-webfont.ttf │ │ │ │ ├── titilliumweb-bold-webfont.woff │ │ │ │ ├── titilliumweb-regular-webfont.eot │ │ │ │ ├── titilliumweb-regular-webfont.ttf │ │ │ │ ├── titilliumweb-regular-webfont.woff │ │ │ │ ├── titilliumweb-extralight-webfont.eot │ │ │ │ ├── titilliumweb-extralight-webfont.ttf │ │ │ │ └── titilliumweb-extralight-webfont.woff │ │ ├── AdminLTE-select2.min.css │ │ └── AdminLTE-select2.css │ ├── config.json │ └── scripts │ │ ├── Compoments │ │ ├── jquery.livequery.js │ │ └── jquery.asuggest.js │ │ ├── app.js │ │ └── app_help.js ├── models │ ├── __init__.py │ ├── configuration.py │ ├── source.py │ └── session.py ├── views │ ├── __init__.py │ ├── source.py │ ├── configuration.py │ └── automate.py ├── __init__.py ├── flask_extended.py ├── adminlte │ ├── models.py │ ├── admin.py │ └── views.py ├── package.json ├── webpack.config.js ├── incident.py └── marshmallow │ ├── legacy.py │ └── front.py ├── hermes ├── i18n.py ├── __init__.py ├── logger.py └── source.py ├── docs ├── ACTIONS.md ├── CHAPITRE-5.md ├── CHAPITRE-3.md ├── GMAIL.md ├── CRITERES.md ├── CHAPITRE-2.md ├── CHAPITRE-7.md ├── CHAPITRE-6.md └── CHAPITRE-1.md ├── upgrade.sh ├── setup.cfg ├── .travis.yml ├── docker-compose.yml ├── wsgi.py ├── test ├── test_action.py ├── test_detecteur.py ├── test_automate.py ├── test_critere.py └── test_session.py ├── Dockerfile ├── configuration.dist.yml ├── .dockerignore ├── .gitignore ├── setup.py ├── CODE_OF_CONDUCT.md ├── README-en.md └── README.md /invitations/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hermes_ui/moteur/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hermes_ui/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hermes_ui/migrations/versions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hermes_ui/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /hermes_ui/templates/admin/master.html: -------------------------------------------------------------------------------- 1 | {% extends admin_base_template %} 2 | -------------------------------------------------------------------------------- /hermes_ui/db/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | 5 | -------------------------------------------------------------------------------- /hermes_ui/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/static.html: -------------------------------------------------------------------------------- 1 | {% macro url() -%} 2 | {{ get_url('admin.static', *varargs, **kwargs) }} 3 | {%- endmacro %} 4 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/30px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/30px.png -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/32px.png -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/throbber.gif -------------------------------------------------------------------------------- /hermes_ui/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .automate import * 2 | from .configuration import * 3 | from .detecteur import * 4 | from .session import * 5 | from .source import * 6 | -------------------------------------------------------------------------------- /hermes_ui/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .automate import AutomateView 2 | from .configuration import ConfigurationView 3 | from .detecteur import * 4 | from .source import * 5 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.eot -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.ttf -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-bold-webfont.woff -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.eot -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.ttf -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-regular-webfont.woff -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/inline_form.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/lib.html' as lib with context %} 2 |
3 | {{ lib.render_form_fields(field.form, form_opts=form_opts) }} 4 |
5 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.eot -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.ttf -------------------------------------------------------------------------------- /hermes/i18n.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os 3 | 4 | localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale') 5 | translate = gettext.translation('hermes', localedir, fallback=True) 6 | _ = translate.gettext 7 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ousret/hermes/HEAD/hermes_ui/assets/styles/jstree/proton/fonts/titillium/titilliumweb-extralight-webfont.woff -------------------------------------------------------------------------------- /hermes_ui/assets/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "assetsPath": "static/build", 4 | "assetsURL": "/static/build/", 5 | "context": "./assets", 6 | "debug": false, 7 | "staticPath": "static/build", 8 | "staticURL": "/static/build/" 9 | } 10 | } -------------------------------------------------------------------------------- /docs/ACTIONS.md: -------------------------------------------------------------------------------- 1 |

Les actions

2 | 3 | Nous développons ici pour chaque type d'action : 4 | 5 | - Une courte description de ce qui est effectuée 6 | - Comment évalue-t-elle à réussite ou échec ? 7 | - La réponse de l'action (stockable dans une variable) 8 | 9 | -------------------------------------------------------------------------------- /hermes/__init__.py: -------------------------------------------------------------------------------- 1 | from hermes.analysis import ExtractionInteret 2 | from hermes.mail import MailToolbox, Mail, MailBody, MailAttachement 3 | from hermes.detecteur import Detecteur 4 | from hermes.automate import Automate 5 | from hermes.logger import logger 6 | from hermes.session import Session 7 | 8 | -------------------------------------------------------------------------------- /hermes/logger.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, realpath 2 | from logging.handlers import BufferingHandler 3 | 4 | from loguru import logger 5 | 6 | __path__ = dirname(realpath(__file__)) 7 | 8 | mem_handler = BufferingHandler(capacity=15000) 9 | logger.add(mem_handler, colorize=True, level='INFO', enqueue=False) 10 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/file/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | 4 | {% block body %} 5 | {% block header %}

{{ header_text }}

{% endblock %} 6 | {% block fa_form %} 7 | {{ lib.render_form(form, dir_url) }} 8 | {% endblock %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/_messages.html: -------------------------------------------------------------------------------- 1 | {%- with messages = get_flashed_messages(with_categories=true) -%} 2 | {% if messages %} 3 | 8 | {% endif %} 9 | {%- endwith %} -------------------------------------------------------------------------------- /hermes_ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import app 2 | from hermes.session import Session 3 | from hermes.logger import logger, __path__ 4 | 5 | logger.info('Chargement du dossiers contenant les configurations YAML') 6 | configurations_chargees = Session.charger(__path__+'/../configurations') 7 | logger.info('Les fichiers de configuration suivant ont été chargées: "{}"', str(configurations_chargees)) 8 | -------------------------------------------------------------------------------- /upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | export FLASK_APP=app.py 3 | git fetch --tags 4 | 5 | latestTag=$(git describe --tags `git rev-list --tags --max-count=1`) 6 | git checkout $latestTag 7 | 8 | if type "python3" > /dev/null; then 9 | python3 setup.py install --user 10 | else 11 | python setup.py install --user 12 | fi 13 | 14 | cd hermes_ui || exit 15 | flask db upgrade 16 | yarn build 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [compile_catalog] 2 | directory = hermes/locale 3 | domain = hermes 4 | statistics = true 5 | 6 | [extract_messages] 7 | add_comments = TRANSLATORS: 8 | output_file = hermes/locale/hermes.pot 9 | width = 80 10 | 11 | [init_catalog] 12 | domain = hermes 13 | input_file = hermes/locale/hermes.pot 14 | output_dir = hermes/locale 15 | 16 | [update_catalog] 17 | domain = hermes 18 | input_file = hermes/locale/hermes.pot 19 | output_dir = hermes/locale 20 | previous = true 21 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/inline_field_list.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/model/inline_list_base.html' as base with context %} 2 | 3 | {% macro render_field(field) %} 4 | {{ field }} 5 | 6 | {% if h.is_field_error(field.errors) %} 7 | 12 | {% endif %} 13 | {% endmacro %} 14 | 15 | {{ base.render_inline_fields(field, template, render_field, check) }} 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.8 3 | 4 | services: 5 | - docker 6 | 7 | before_install: 8 | - cp configuration.dist.yml configuration.yml 9 | - docker-compose build 10 | - docker-compose create 11 | - docker-compose up --no-start 12 | - docker ps 13 | 14 | install: 15 | - pip install nose codecov 16 | - pip install git+https://github.com/Ousret/python-emails.git 17 | - python setup.py install 18 | 19 | script: 20 | - "nosetests --with-coverage --cover-package=hermes test/*.py" 21 | 22 | after_success: 23 | - codecov 24 | -------------------------------------------------------------------------------- /hermes_ui/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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | hermes: 4 | build: ./ 5 | links: 6 | - mariadb 7 | depends_on: 8 | - mariadb 9 | environment: 10 | FLASK_ENV: PRODUCTION 11 | FLASK_DEBUG: 0 12 | ports: 13 | - "5000:5000" 14 | restart: on-failure 15 | 16 | mariadb: 17 | image: mariadb:10.4 18 | command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --innodb-flush-log-at-trx-commit=0 19 | environment: 20 | MYSQL_ROOT_PASSWORD: hermes 21 | MYSQL_DATABASE: hermes 22 | MYSQL_USER: hermes 23 | MYSQL_PASSWORD: hermes 24 | restart: on-failure 25 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/file/modals/form.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | 4 | {% block body %} 5 | {# content added to modal-content #} 6 | 11 | 16 | {% endblock %} 17 | 18 | {% block tail %} 19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/row_actions.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/lib.html' as lib with context %} 2 | {% import 'admin/adminlte/adminlte.html' as adminlte with context %} 3 | 4 | {% macro view_row(action, row_id, row) %} 5 | {{ adminlte.action_view_button(action, row_id, row, modal=False) }} 6 | {% endmacro %} 7 | 8 | {% macro view_row_popup(action, row_id, row) %} 9 | {{ adminlte.action_view_button(action, row_id, row, modal=True) }} 10 | {% endmacro %} 11 | 12 | {% macro edit_row(action, row_id, row) %} 13 | {{ adminlte.action_edit_button(action, row_id, row, modal=False) }} 14 | {% endmacro %} 15 | 16 | {% macro edit_row_popup(action, row_id, row) %} 17 | {{ adminlte.action_edit_button(action, row_id, row, modal=True) }} 18 | {% endmacro %} 19 | 20 | {% macro delete_row(action, row_id, row) %} 21 | {{ adminlte.action_delete_button(action, row_id, row) }} 22 | {% endmacro %} 23 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/modals/create.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/adminlte/forms.html' as forms with context %} 3 | 4 | {# store the jinja2 context for form_rules rendering logic #} 5 | {% set render_ctx = h.resolve_ctx() %} 6 | 7 | {% block body %} 8 | {% block create_form %} 9 | {% set modal_name = 'Create' %} 10 | {{ forms.form(modal_name, None, form, form_opts, return_url, action=url_for('.create_view', url=return_url), has_more=False, is_modal=True) }} 11 | {% endblock %} 12 | {% endblock %} 13 | 14 | {% block tail %} 15 | 16 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /hermes_ui/flask_extended.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | 4 | from flask import Flask as BaseFlask, Config as BaseConfig 5 | 6 | 7 | class Config(BaseConfig): 8 | """Flask config enhanced with a `from_yaml` method.""" 9 | 10 | def from_yaml(self, config_file): 11 | env = os.environ.get('FLASK_ENV', 'DEVELOPMENT') 12 | self['ENVIRONMENT'] = env.lower() 13 | 14 | with open(config_file) as f: 15 | c = yaml.load(f, Loader=yaml.FullLoader) 16 | 17 | c = c.get(env, c) 18 | 19 | for key in c.keys(): 20 | if key.isupper(): 21 | self[key] = c[key] 22 | 23 | 24 | class Flask(BaseFlask): 25 | """Extended version of `Flask` that implements custom config class""" 26 | 27 | def make_config(self, instance_relative=False): 28 | root_path = self.root_path 29 | if instance_relative: 30 | root_path = self.instance_path 31 | return Config(root_path, self.default_config) 32 | -------------------------------------------------------------------------------- /hermes_ui/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 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 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/_menu.html: -------------------------------------------------------------------------------- 1 | {% if security.registerable or security.recoverable or security.confirmable %} 2 |

Menu

3 | 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/modals/edit.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/adminlte/forms.html' as forms with context %} 3 | 4 | {# store the jinja2 context for form_rules rendering logic #} 5 | {% set render_ctx = h.resolve_ctx() %} 6 | 7 | {% block body %} 8 | {% block edit_form %} 9 | {% set modal_name = 'Edit' + ' #' + request.args.get('id') %} 10 | {{ forms.form(modal_name, None, form, form_opts, return_url, action=url_for('.edit_view', id=request.args.get('id'), url=return_url), has_more=False, is_modal=True) }} 11 | {% endblock %} 12 | {% endblock %} 13 | 14 | {% block tail %} 15 | 16 | 17 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/rediscli/console.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% import 'admin/static.html' as admin_static with context %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | {% endblock %} 21 | 22 | {% block tail %} 23 | {{ super() }} 24 | 25 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field_with_errors(field, icon_class) %} 2 |
3 |
4 | 5 | {{ field(placeholder=field.label.text, class_='form-control', **kwargs)|safe }} 6 |
7 | {% if field.errors %} 8 | {% for error in field.errors %} 9 | {{ error }} 10 | {% endfor %} 11 | {% endif %} 12 |
13 | {% endmacro %} 14 | 15 | {% macro render_checkbox_field(field) -%} 16 |
17 | {{ field(type='checkbox', **kwargs) }} 18 | 19 |
20 | {%- endmacro %} 21 | 22 | {% macro render_button(field) %} 23 | {{ field(class_='form-control', **kwargs)|safe }} 24 | {% endmacro %} 25 | 26 | {% macro render_field(field) %} 27 |

{{ field(class_='form-control', **kwargs)|safe }}

28 | {% endmacro %} -------------------------------------------------------------------------------- /docs/CHAPITRE-5.md: -------------------------------------------------------------------------------- 1 |

Création de la description d'un automate

2 | 3 | **Pré-requis:** Avoir mis en place au moins un détecteur 4 | 5 | ## ✨ Description d'un automate 6 | 7 | Cette section va vous permettre de créer un automate attaché à un de vos détecteurs. 8 | 9 | Allez sur le menu "Description des Automates" puis appuyez sur "Créer". 10 | 11 |

12 | Capture d’écran 2020-01-09 à 10 52 35 13 |

14 | 15 | Ce n'est pas ici que vous allez créer vos actions. 16 | 17 | Un automate ne peut pas être attaché à plus d'un détecteur. 18 | ⚠️ Par contre il est possible de créer un automate sans détecteur, on parlera de routine ou de plugin. 19 | 20 | ## Édition des actions 21 | 22 | Une fois la description de votre automate effectuée, nous vous invitons à revenir sur le menu "Éditeur d'Automate". 23 | 24 | ## Pour aller plus loin 25 | 26 | - [ ] [Mettre en oeuvre une suite d'action à appliquer après la détection](CHAPITRE-6.md) 27 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/rediscli/response.html: -------------------------------------------------------------------------------- 1 | {% macro render(item, depth=0) %} 2 | {% set type = type_name(item) %} 3 | 4 | {% if type == 'tuple' or type == 'list' %} 5 | {% if not item %} 6 | Empty {{ type }}. 7 | {% else %} 8 | {% for n in item %} 9 | {{ loop.index }}) {{ render(n, depth + 1) }}
10 | {% endfor %} 11 | {% endif %} 12 | {% elif type == 'bool' %} 13 | {% if depth == 0 and item %} 14 | OK 15 | {% else %} 16 | {{ item }} 17 | {% endif %} 18 | {% elif type == 'str' or type == 'unicode' %} 19 | "{{ item }}" 20 | {% elif type == 'bytes' %} 21 | "{{ item.decode('utf-8') }}" 22 | {% elif type == 'TextWrapper' %} 23 |
{{ item }}
24 | {% elif type == 'dict' %} 25 | {% for k, v in item.items() %} 26 | {{ loop.index }}) {{ k }} - {{ render(v, depth + 1) }}
27 | {% endfor %} 28 | {% else %} 29 | {{ item }} 30 | {% endif %} 31 | {% endmacro %} 32 | {{ render(result) }} -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% from 'admin/lib.html' import extra with context %} {# backward compatible #} 4 | {% import 'admin/adminlte/forms.html' as forms with context %} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | {{ lib.form_css() }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 |

{{ admin_view.name }}

14 |
15 |
16 |
17 |
18 |
19 | {% block create_form %} 20 | {% set modal_name = 'Create' %} 21 | {% set icon = 'fa fa-plus' %} 22 | {{ forms.form(modal_name, icon, form, form_opts, return_url, has_more=True, is_modal=False) }} 23 | {% endblock %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | 30 | {% block tail %} 31 | {{ super() }} 32 | {{ lib.form_js() }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% from 'admin/lib.html' import extra with context %} {# backward compatible #} 4 | {% import 'admin/adminlte/forms.html' as forms with context %} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | {{ lib.form_css() }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 |

{{ admin_view.name }}

14 |
15 |
16 |
17 |
18 |
19 | {% block edit_form %} 20 | {% set modal_name = 'Edit' + ' #' + request.args.get('id') %} 21 | {% set icon = 'fa fa-pencil' %} 22 | {{ forms.form(modal_name, icon, form, form_opts, return_url, has_more=False, is_modal=False) }} 23 | {% endblock %} 24 |
25 |
26 |
27 |
28 | {% endblock %} 29 | 30 | {% block tail %} 31 | {{ super() }} 32 | {{ lib.form_js() }} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /hermes_ui/db/polymorphic.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | 3 | 4 | def get_model_class(mapped_class_child): 5 | """ 6 | Charger la classe correspondant à un chemin complet (dot package) str 7 | Orienté pour le champs mapped class child SQLAlchemy 8 | :param str mapped_class_child: 9 | :rtype: type 10 | :raise AttributeError: 11 | """ 12 | decompose_type_action = mapped_class_child.split("'") # type: list[str] 13 | 14 | if len(decompose_type_action) != 3 or not decompose_type_action[-2].startswith('hermes_ui.models.'): 15 | return None 16 | 17 | from sys import modules 18 | 19 | target_module = modules['.'.join(decompose_type_action[-2].split('.')[0:-1])] 20 | 21 | return getattr(target_module, decompose_type_action[-2].split('.')[-1]) 22 | 23 | 24 | def get_child_polymorphic(parent_entity): 25 | """ 26 | :param db.Model parent_entity: 27 | :rtype: db.Model 28 | """ 29 | if not hasattr(parent_entity, 'mapped_class_child') or not hasattr(parent_entity, 'id'): 30 | raise TypeError('Cannot uncover child entity of non SQLAlchemy object. ' 31 | 'Should have mapped_class_child and id attr.') 32 | 33 | return db.session.query(get_model_class(parent_entity.mapped_class_child)).get(parent_entity.id) 34 | -------------------------------------------------------------------------------- /hermes_ui/views/source.py: -------------------------------------------------------------------------------- 1 | from hermes_ui.adminlte.views import BaseAdminView 2 | from flask_login import current_user 3 | from datetime import datetime 4 | 5 | 6 | class BoiteAuxLettresImapView(BaseAdminView): 7 | column_editable_list = ['designation', 'activation'] 8 | column_searchable_list = ['designation'] 9 | column_exclude_list = ['mot_de_passe'] 10 | column_details_exclude_list = None 11 | column_filters = ['designation'] 12 | form_excluded_columns = ['actions', 'createur', 'date_creation', 'date_modification', 'responsable_derniere_modification'] 13 | can_export = True 14 | can_view_details = False 15 | can_create = True 16 | can_edit = True 17 | can_delete = True 18 | edit_modal = True 19 | create_modal = True 20 | details_modal = False 21 | 22 | def on_model_change(self, form, model, is_created): 23 | """ 24 | 25 | :param form: 26 | :param hermes_ui.models.automate.Automate model: 27 | :param bool is_created: 28 | :return: 29 | """ 30 | if is_created is True: 31 | model.createur = current_user 32 | model.date_creation = datetime.now() 33 | 34 | model.date_modification = datetime.now() 35 | model.responsable_derniere_modification = current_user 36 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/actions.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/adminlte/adminlte.html' as adminlte with context %} 3 | 4 | {% macro dropdown(actions, btn_class='btn dropdown-toggle') -%} 5 | {{ adminlte.bulk_actions_dropdown_button(actions) }} 6 | {% endmacro %} 7 | 8 | {% macro form(actions, url) %} 9 | {% if actions %} 10 | 19 | {% endif %} 20 | {% endmacro %} 21 | 22 | {% macro script(message, actions, actions_confirmation) %} 23 | {% if actions %} 24 | 25 | 28 | {% endif %} 29 | {% endmacro %} 30 | -------------------------------------------------------------------------------- /hermes_ui/views/configuration.py: -------------------------------------------------------------------------------- 1 | from hermes_ui.adminlte.views import BaseAdminView 2 | from flask_login import current_user 3 | from datetime import datetime 4 | 5 | 6 | class ConfigurationView(BaseAdminView): 7 | column_editable_list = ['designation'] 8 | column_searchable_list = ['designation'] 9 | column_exclude_list = ['valeur'] 10 | form_excluded_columns = ['createur', 'date_creation', 'date_modification', 'responsable_derniere_modification'] 11 | column_details_exclude_list = None 12 | column_filters = ['designation', 'createur', 'date_creation', 'date_modification'] 13 | can_export = True 14 | can_view_details = True 15 | can_create = True 16 | can_edit = True 17 | can_delete = True 18 | edit_modal = True 19 | create_modal = True 20 | details_modal = False 21 | 22 | def on_model_change(self, form, model, is_created): 23 | """ 24 | 25 | :param form: 26 | :param hermes_ui.models.configuration.Configuration model: 27 | :param bool is_created: 28 | :return: 29 | """ 30 | if is_created is True: 31 | model.createur = current_user 32 | model.date_creation = datetime.now() 33 | 34 | model.date_modification = datetime.now() 35 | model.responsable_derniere_modification = current_user 36 | 37 | -------------------------------------------------------------------------------- /hermes_ui/adminlte/models.py: -------------------------------------------------------------------------------- 1 | from flask_security import RoleMixin, UserMixin 2 | from hermes_ui.db import db as admin_db 3 | 4 | 5 | roles_users = admin_db.Table( 6 | 'roles_users', 7 | admin_db.Column('user_id', admin_db.Integer(), admin_db.ForeignKey('user.id')), 8 | admin_db.Column('role_id', admin_db.Integer(), admin_db.ForeignKey('role.id')) 9 | ) 10 | 11 | 12 | class Role(admin_db.Model, RoleMixin): 13 | id = admin_db.Column(admin_db.Integer(), primary_key = True) 14 | name = admin_db.Column(admin_db.String(80), unique = True) 15 | description = admin_db.Column(admin_db.String(255)) 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | 21 | class User(admin_db.Model, UserMixin): 22 | id = admin_db.Column(admin_db.Integer, primary_key = True) 23 | first_name = admin_db.Column(admin_db.String(255)) 24 | last_name = admin_db.Column(admin_db.String(255)) 25 | email = admin_db.Column(admin_db.String(255), unique = True, nullable = False) 26 | password = admin_db.Column(admin_db.String(255), nullable = False) 27 | active = admin_db.Column(admin_db.Boolean(), nullable = False) 28 | roles = admin_db.relationship('Role', secondary = roles_users, backref = 'users') 29 | 30 | def __str__(self): 31 | return self.first_name + " " + self.last_name + " <" + self.email + ">" 32 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/modals/details.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% import 'admin/adminlte/forms.html' as forms with context %} 4 | 5 | {% block body %} 6 | 10 | 18 | 27 | {% endblock %} 28 | 29 | {% block tail %} 30 | 31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_button %} 3 | {% include "security/_messages.html" %} 4 | {% block head %} 5 | 10 | {% endblock head %} 11 | {% block page_body %} 12 |
13 | 16 |
17 | 18 |
19 | {{ forgot_password_form.hidden_tag() }} 20 | {{ render_field_with_errors(forgot_password_form.email, 'fa fa-at') }} 21 |
22 |
23 | {{ render_button(forgot_password_form.submit, class="btn btn-primary btn-block btn-flat") }} 24 |
25 |
26 |
27 |
28 |

29 | {{ _( "Connexion" ) }} 30 |

31 |
32 | {% endblock page_body %} -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from hermes_ui.app import app 3 | from ssl import SSLContext, PROTOCOL_TLS, CERT_REQUIRED, Purpose 4 | 5 | if __name__ == '__main__': 6 | """ 7 | Départ du serveur générique WSGI Flask 8 | """ 9 | 10 | context = None 11 | 12 | if app.config.get('HERMES_CERTIFICAT_TLS') and app.config.get('HERMES_CLE_PRIVEE_TLS'): 13 | context = SSLContext(PROTOCOL_TLS) 14 | 15 | context.check_hostname = True 16 | context.set_ciphers( 17 | "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384") 18 | 19 | context.verify_mode = CERT_REQUIRED 20 | context.load_cert_chain(app.config.get('HERMES_CERTIFICAT_TLS'), app.config.get('HERMES_CLE_PRIVEE_TLS')) 21 | 22 | context.load_default_certs(Purpose.SERVER_AUTH) 23 | 24 | if app.config.get('HERMES_CERTIFICAT_CA'): 25 | context.load_verify_locations(app.config.get('HERMES_CERTIFICAT_CA')) 26 | 27 | adhoc_request = app.config.get('HERMES_CERTIFICAT_TLS') is False and app.config.get('HERMES_CLE_PRIVEE_TLS') is False and app.config.get('HERMES_CERTIFICAT_CA') is False 28 | 29 | app.run( 30 | host='0.0.0.0', 31 | port=5000, 32 | threaded=True, 33 | ssl_context=context if not adhoc_request else 'adhoc' 34 | ) 35 | -------------------------------------------------------------------------------- /test/test_action.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hermes.automate import * 3 | 4 | 5 | class FakeSource(Source): 6 | 7 | def titre(self): 8 | return 'FakeSource' 9 | 10 | def corps(self): 11 | return '' 12 | 13 | 14 | class TestAction(unittest.TestCase): 15 | 16 | def test_http_ok(self): 17 | action = RequeteHttpActionNoeud( 18 | "Requête sur httpbin", 19 | "https://httpbin.org/post", 20 | "POST", 21 | { 22 | 'username': 'abc', 23 | 'password': 'xyz' 24 | }, 25 | None, 26 | None, 27 | 200 28 | ) 29 | 30 | self.assertTrue( 31 | action.je_realise(FakeSource('', '')) 32 | ) 33 | 34 | self.assertIn( 35 | 'form', 36 | action.payload 37 | ) 38 | 39 | self.assertEqual( 40 | { 41 | 'username': 'abc', 42 | 'password': 'xyz' 43 | }, 44 | action.payload['form'] 45 | ) 46 | 47 | def test_http_ko(self): 48 | action = RequeteHttpActionNoeud( 49 | "Requête sur httpbin", 50 | "https://httpbin.org/post", 51 | "POST", 52 | { 53 | 'username': 'abc', 54 | 'password': 'xyz' 55 | }, 56 | None, 57 | None, 58 | 201 59 | ) 60 | 61 | self.assertFalse( 62 | action.je_realise(FakeSource('', '')) 63 | ) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_button %} 3 | {% include "security/_messages.html" %} 4 | {% block head %} 5 | 10 | {% endblock head %} 11 | {% block page_body %} 12 |
13 | 16 |
17 | 18 |
19 | {{ change_password_form.hidden_tag() }} 20 | {{ render_field_with_errors(change_password_form.password, 'fa fa-lock') }} 21 | {{ render_field_with_errors(change_password_form.new_password, 'fa fa-lock') }} 22 | {{ render_field_with_errors(change_password_form.new_password_confirm, 'fa fa-lock') }} 23 |
24 |
25 | {{ render_button(change_password_form.submit, class="btn btn-primary btn-block btn-flat") }} 26 |
27 |
28 |
29 |
30 |

31 | Back to the Admin 32 |

33 |
34 | {% endblock page_body %} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu 20.04 as the base image 2 | FROM ubuntu:20.04 3 | # Use an official Python runtime as an image 4 | FROM python:3.8 5 | 6 | #Disable Prompt During Packages Installation 7 | ARG DEBIAN_FRONTEND=noninteractive 8 | 9 | LABEL MAINTAINER Ahmed TAHRI "ahmed.tahri@cloudnursery.dev" 10 | LABEL version ="0.1" 11 | LABEL description="This is a customer docker build for Hermes - https://github.com/Ousret/hermes" 12 | 13 | # Update Current available packages 14 | RUN apt-get update 15 | # Upgrade all installed packages so most recent files are used. 16 | RUN apt-get upgrade -y 17 | 18 | # Lets install some mandatory requirements to grad the rest of the files needed 19 | RUN apt-get -y install curl gnupg wget git 20 | RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - 21 | RUN apt-get -y install nodejs 22 | RUN npm install yarn -g 23 | 24 | RUN pip install certifi pyopenssl 25 | 26 | EXPOSE 5000 27 | 28 | RUN mkdir /python-emails 29 | 30 | WORKDIR /python-emails 31 | 32 | RUN git clone https://github.com/Ousret/python-emails.git . 33 | 34 | RUN python setup.py install 35 | 36 | WORKDIR /app 37 | 38 | COPY ./hermes ./hermes 39 | COPY ./hermes_ui ./hermes_ui 40 | COPY setup.py ./setup.py 41 | COPY setup.cfg ./setup.cfg 42 | COPY wsgi.py ./wsgi.py 43 | 44 | RUN mkdir invitations 45 | 46 | COPY ./configuration.yml /app/configuration.yml 47 | 48 | RUN pip install mysqlclient 49 | 50 | RUN python setup.py install 51 | 52 | WORKDIR /app/hermes_ui 53 | 54 | RUN yarn install 55 | RUN yarn build 56 | 57 | WORKDIR /app 58 | 59 | CMD python wsgi.py 60 | 61 | # This will clean up any un-used apps and any other mess we might have made. 62 | RUN rm -rf /var/lib/apt/lists/* && apt clean 63 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/register_user.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_button %} 3 | {% include "security/_messages.html" %} 4 | {% block head %} 5 | 10 | {% endblock head %} 11 | {% block page_body %} 12 |
13 | 16 |
17 |

Sign up

18 |
19 | {{ register_user_form.hidden_tag() }} 20 | {{ render_field_with_errors(register_user_form.email, 'fa fa-at') }} 21 | {{ render_field_with_errors(register_user_form.password, 'fa fa-lock') }} 22 | {% if register_user_form.password_confirm %} 23 | {{ render_field_with_errors(register_user_form.password_confirm, 'fa fa-lock') }} 24 | {% endif %} 25 |
26 |
27 | {{ render_button(register_user_form.submit, class="btn btn-primary btn-block btn-flat") }} 28 |
29 |
30 |
31 |
32 |

33 | I already have an account 34 |

35 |
36 | {% endblock page_body %} -------------------------------------------------------------------------------- /hermes_ui/models/configuration.py: -------------------------------------------------------------------------------- 1 | from hermes_ui.adminlte.models import User 2 | from hermes_ui.db import db 3 | from flask_login import current_user 4 | from datetime import datetime 5 | 6 | from hermes.session import Session 7 | 8 | 9 | class Configuration(db.Model): 10 | 11 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 12 | designation = db.Column(db.String(255), nullable=False, unique=True) 13 | valeur = db.Column(db.Text(), nullable=False) 14 | 15 | createur_id = db.Column(db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True) 16 | createur = db.relationship(User, primaryjoin="User.id==Configuration.createur_id") 17 | 18 | date_creation = db.Column(db.DateTime(timezone=True), nullable=False) 19 | date_modification = db.Column(db.DateTime(timezone=True), nullable=False) 20 | 21 | responsable_derniere_modification_id = db.Column(db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True) 22 | responsable_derniere_modification = db.relationship(User, primaryjoin="User.id==Configuration.responsable_derniere_modification_id") 23 | 24 | format = db.Column(db.Enum("JSON", 'YAML', 'AUTRE'), nullable=False) 25 | 26 | def __repr__(self): 27 | return ''.format(self.designation) 28 | 29 | def on_model_change(self, form, model, is_created): 30 | """ 31 | 32 | :param form: 33 | :param Configuration model: 34 | :param bool is_created: 35 | :return: 36 | """ 37 | if is_created is True: 38 | model.createur = current_user 39 | model.date_creation = datetime.now() 40 | 41 | model.date_modification = datetime.now() 42 | model.responsable_derniere_modification = current_user 43 | 44 | Session.charger_input(model.designation, model.valeur, model.format) 45 | -------------------------------------------------------------------------------- /hermes_ui/adminlte/admin.py: -------------------------------------------------------------------------------- 1 | from flask_admin._compat import as_unicode 2 | from flask_admin import Admin 3 | from flask_security import SQLAlchemyUserDatastore 4 | from .models import User, Role 5 | from hermes_ui.db import db as admin_db 6 | from .views import AdminsView 7 | from hashlib import sha256 8 | 9 | admins_store = SQLAlchemyUserDatastore(admin_db, User, Role) 10 | 11 | 12 | class AdminLte(Admin): 13 | """ 14 | Collection of the admin views. Also manages menu structure. 15 | """ 16 | 17 | def __init__(self, app = None, name = None, url = None, subdomain = None, index_view = None, 18 | translations_path = None, endpoint = None, static_url_path = None, base_template = None, 19 | category_icon_classes = None, short_name = None, long_name = None, 20 | skin = 'blue'): 21 | super(AdminLte, self).__init__(app, name, url, subdomain, index_view, translations_path, endpoint, 22 | static_url_path, base_template, 'bootstrap3', category_icon_classes) 23 | self.short_name = short_name or name 24 | self.long_name = long_name or name 25 | self.skin = skin 26 | 27 | admin_db.app = app 28 | admin_db.init_app(app) 29 | self.add_view(AdminsView(User, admin_db.session, name = "Utilisateurs", menu_icon_value = 'fa-user-secret')) 30 | 31 | def gravatar_image_url(self, email, default_url, size = 96): 32 | return "https://www.gravatar.com/avatar/"+sha256(email.encode('utf-8')).hexdigest() 33 | 34 | def set_category_icon(self, name, icon_value, icon_type = "fa"): 35 | cat_text = as_unicode(name) 36 | category = self._menu_categories.get(cat_text) 37 | 38 | if category is not None: 39 | category.icon_type = icon_type 40 | category.icon_value = icon_value 41 | -------------------------------------------------------------------------------- /docs/CHAPITRE-3.md: -------------------------------------------------------------------------------- 1 |

Boîtes de messagerie électronique (IMAP)

2 | 3 | ## ✨ Configuration 4 | 5 | Tout le principe d'Hermes est de réagir à une certaine typologie de message électronoqie. 6 | Nous avons alors besoin d'une source de données dans laquelle lire les messages en entrés. 7 | 8 | Le programme permet, clé en main de configurer l'accès à une boite IMAP4 en sachant : 9 | 10 | - Hôte distante (IP ou DNS) 11 | - Nom d'utilisateur pour s'authentifier 12 | - Mot de passe associé à l'utilisateur 13 | - Dossier dans lequel se placer pour lire et analyser les messages, par défaut **INBOX**. 14 | 15 | Pour cela, nous vous invitons à vous rendre dans le menu "Sources de données" puis "Boite aux lettres (IMAP)". 16 | Une fois sur la liste des boîtes, nous vous invitons à cliquer sur "Créer". 17 | 18 | 🔒 Nous vous conseillons de laisser coché "TLS" et "Vérification Certificat" pour plus de sécurité. 19 | ❓ La case **Activation** permet d'autoriser Hermès à inclure cet automate lors de la surveillance de(s) boite(s) IMAP. Inversement pour test(s) uniquement(s). 20 | ⚠️ L'option "Legacy TLS" permet d'essayer de négocier une connexion sur un serveur ayant des protocols déchus. Cette option est déconseillée et risque de ne pas fonctionner selon vos installations locales. (openssl) 21 | 22 | ## Utilisation de variable 23 | 24 | Les champs **Hôte distante**, **Nom d'utilisateur**, **Mot de passe** peuvent contenir des variables au format `{{ ma_variable }}`. 25 | 26 | ## Fournisseurs compatibles 27 | 28 | N'importe quel fournisseur de messagerie est compatible, mais sachez que certain fournisseur exige un niveau 29 | d'authentification plus important que le couple *utilisateur, mot de passe*. 30 | 31 | Pour que GMail soit compatible, il faut d'abord activer l'accès moins sécurisée. (cf. google) 32 | 33 | ## Pour aller plus loin 34 | 35 | - [ ] [Détecter un message électronique](CHAPITRE-4.md) 36 | -------------------------------------------------------------------------------- /docs/GMAIL.md: -------------------------------------------------------------------------------- 1 |

GMail

2 | 3 | Nous allons vous guidez dans la mise en oeuvre de la connexion à votre compte GMail. 4 | 5 | ## Authentification 6 | 7 | Hôte IMAP : `imap.gmail.com` 8 | Hôte SMTP : `smtp.gmail.com` 9 | utilisateur : `mon_compte@gmail.com` 10 | 11 | mot de passe : **Vous ne pouvez pas utiliser votre mot de passe habituel ! Vous devez en créer un spécialement pour hermes.** 12 | 13 | D'abord recherchez "gmail generate app password" sur Google. 14 | 15 | ``` 16 | Create & use App Passwords 17 | Go to your Google Account. 18 | On the left navigation panel, choose Security. 19 | On the "Signing in to Google" panel, choose App Passwords. ... 20 | At the bottom, choose Select app and choose the app you're using. 21 | Choose Select device and choose the device you're using. 22 | Choose Generate. 23 | ``` 24 | 25 | [Générer son mot de passe d'Application](https://support.google.com/accounts/answer/185833?hl=fr) 26 | 27 | ## Configuration GMAIL nécessaire 28 | 29 | Google dispose d'une implémentation IMAP4 modifiée et cela risque de poser un problème lors de la suppression des messages. 30 | Cela vient de la manière dont les instructions `DELETE` et `EXPUNGE` sont interprétées. 31 | 32 | Si vous souhaitez pallier à ce problème de message non supprimable depuis Hermès : 33 | 34 | - Connectez-vous sur votre compte gmail depuis votre navigateur internet. 35 | - Une fois sur votre boite de reception, choississez réglages, en haut à droite de la liste des messages. (Roue crantée puis réglages.) 36 | - Choisir l'onglet 'Boîte de réception' 37 | - Cochez le bouton radio 'Laisser le client mail choisir pour la suppression' 38 | - À votre convenance, choisir comment sera interprété la suppression. (i) suppression immédiate (ii) déplacer dans corbeille. 39 | 40 | Capture d’écran 2020-03-12 à 08 50 32 41 | -------------------------------------------------------------------------------- /test/test_detecteur.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hermes.detecteur import * 3 | from hermes.source import Source 4 | 5 | ma_source = Source( 6 | "#Mesures Relève des températures du mois d'Août le 12/12/2020", 7 | """Réf-091 8 | 9 | Bonjour JOHN DOE ! 10 | 11 | Date de mesure : 12/12/2020 12 | Auteur : Ahmed TAHRI 13 | 14 | Nous avons mesurés à 38 reprises la température de votre ville natale. 15 | 16 | Merci de votre attention. 17 | """ 18 | ) 19 | 20 | mon_detecteur = Detecteur( 21 | "Relève de température" 22 | ) 23 | 24 | mon_detecteur.je_veux( 25 | IdentificateurRechercheInteret( 26 | "Recherche de la référence", 27 | "Réf-" 28 | ) 29 | ) 30 | 31 | mon_detecteur.je_veux( 32 | DateRechercheInteret( 33 | "Recherche date de relève", 34 | "Relève des températures du mois d'Août le" 35 | ) 36 | ) 37 | 38 | mon_detecteur.je_veux( 39 | ExpressionCleRechercheInteret( 40 | "Recherche d'une phrase à l'identique", 41 | "Nous avons mesurés à" 42 | ) 43 | ) 44 | 45 | mon_detecteur.je_veux( 46 | LocalisationExpressionRechercheInteret( 47 | "Recherche du nombre de relève température", 48 | "reprises", 49 | "Nous avons mesurés à" 50 | ) 51 | ) 52 | 53 | mon_detecteur.je_veux( 54 | InformationRechercheInteret( 55 | "Recherche de hashtag", 56 | "Mesures" 57 | ) 58 | ) 59 | 60 | mon_detecteur.je_veux( 61 | CleRechercheInteret( 62 | "Présence de Auteur", 63 | "Auteur" 64 | ) 65 | ) 66 | 67 | mon_detecteur.je_veux( 68 | ExpressionDansCleRechercheInteret( 69 | "Vérifier que Ahmed est auteur", 70 | "Auteur", 71 | "Ahmed" 72 | ) 73 | ) 74 | 75 | 76 | class TestDetecteur(unittest.TestCase): 77 | 78 | def test_detection(self): 79 | 80 | self.assertTrue( 81 | mon_detecteur.lance_toi(ma_source) 82 | ) 83 | 84 | 85 | if __name__ == '__main__': 86 | unittest.main() 87 | -------------------------------------------------------------------------------- /hermes_ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build-dev": "node_modules/.bin/webpack --config webpack.config.js --mode development", 4 | "build": "node_modules/.bin/webpack --config webpack.config.js --mode production" 5 | }, 6 | "dependencies": { 7 | "@babel/core": "7", 8 | "admin-lte": "2.3.8", 9 | "awesomplete": "^1.1.5", 10 | "babel-cli": "^6.26.0", 11 | "babel-core": "^6.26.3", 12 | "babel-loader": "^8.0.6", 13 | "bootstrap": "3", 14 | "clipboard": "^2.0.4", 15 | "css-loader": "^3.2.0", 16 | "cssnano": "^4.1.10", 17 | "datatables.net-bs": "^1.10.20", 18 | "dropzone": "^5.5.1", 19 | "extract-text-webpack-plugin": "^0.8.1", 20 | "fastclick": "^1.0.6", 21 | "file-loader": "^5.1.0", 22 | "font-awesome": "^4.7.0", 23 | "highlight.js": "^10.4.1", 24 | "icheck": "^1.0.2", 25 | "image-webpack-loader": "6.0", 26 | "intro.js": "^2.9.3", 27 | "jquery": "3", 28 | "jquery-sidebar": "^3.3.2", 29 | "jquery-slimscroll": "^1.3.8", 30 | "jquery-ui": "^1.12.1", 31 | "jquery.terminal": "^2.8.0", 32 | "jsoneditor": "^7.1.0", 33 | "jstree": "^3.3.8", 34 | "lodash.template": "^4.5.0", 35 | "manifest-revision-webpack-plugin": "^0.0.5", 36 | "mini-css-extract-plugin": "^0.8.0", 37 | "moment": "^2.24.0", 38 | "optimize-css-assets-webpack-plugin": "^5.0.3", 39 | "postcss-loader": "^3.0.0", 40 | "script-loader": "^0.6.1", 41 | "select2": "^4.0.11", 42 | "serialize-javascript": "^3.1.0", 43 | "style-loader": "^1.0.0", 44 | "sweetalert2": "^8.18.5", 45 | "terser-webpack-plugin": "^2.2.1", 46 | "tunnel-agent": "^0.6.0", 47 | "uglifyjs-webpack-plugin": "^2.2.0", 48 | "url-loader": "^2.2.0", 49 | "webpack": "4", 50 | "webpack-manifest-plugin": "^2.2.0" 51 | }, 52 | "name": "hermes", 53 | "version": "1.0.0", 54 | "main": "index.js", 55 | "author": "Hermès", 56 | "license": "NPOSL-3.0", 57 | "devDependencies": { 58 | "webpack-cli": "^3.3.9" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /hermes_ui/templates/security/login_user.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% from "security/_macros.html" import render_field, render_field_with_errors, render_checkbox_field, render_button %} 3 | {% include "security/_messages.html" %} 4 | {% block head %} 5 | 10 | {% endblock head %} 11 | {% block page_body %} 12 |
13 | 16 | 33 |

34 | {% if security.recoverable %} 35 | {{ _( "Mot de passe perdu" ) }}
36 | {% endif %} 37 | {% if security.registerable %} 38 | {{ _( "Je m'inscris" ) }} 39 | {% endif %} 40 |

41 |
42 | {% endblock page_body %} -------------------------------------------------------------------------------- /hermes_ui/views/automate.py: -------------------------------------------------------------------------------- 1 | from hermes_ui.adminlte.views import BaseAdminView 2 | from flask_login import current_user 3 | from datetime import datetime 4 | 5 | from hermes.i18n import _ 6 | 7 | 8 | class AutomateView(BaseAdminView): 9 | column_editable_list = ['production', 'notifiable'] 10 | column_searchable_list = ['designation', 'production'] 11 | column_exclude_list = ['action_racine', 'date_creation', 'createur', 'responsable_derniere_modification'] 12 | column_details_exclude_list = None 13 | column_filters = ['designation'] 14 | form_excluded_columns = ['actions', 'action_racine', 'createur', 'date_creation', 'date_modification', 'responsable_derniere_modification'] 15 | can_export = True 16 | can_view_details = False 17 | can_create = True 18 | can_edit = True 19 | can_delete = True 20 | edit_modal = True 21 | create_modal = True 22 | details_modal = True 23 | 24 | column_descriptions = { 25 | 'detecteur': _('Associe un détecteur, qui si résolu avec une source permet de lancer votre suite d\'action'), 26 | 'designation': _('Description courte de ce que réalise votre automate, un objectif'), 27 | 'production': _('Si cette case est cochée, votre automate sera executé en production'), 28 | 'priorite': _('Un entier à partir de 0 (Zéro) permettant de priviligier une execution ' 29 | 'd\'automate par rapport à un autre sur une source. De plus la priorité est proche de 0 (Zéro), ' 30 | 'de plus il est prioritaire'), 31 | 'notifiable': _('Active les notifications en cas d\'échec d\'au moins une des actions de votre automate') 32 | } 33 | 34 | def on_model_change(self, form, model, is_created): 35 | """ 36 | :param form: 37 | :param hermes_ui.models.automate.Automate model: 38 | :param bool is_created: 39 | :return: 40 | """ 41 | if is_created is True: 42 | model.createur = current_user 43 | model.date_creation = datetime.now() 44 | 45 | model.date_modification = datetime.now() 46 | model.responsable_derniere_modification = current_user 47 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/layout.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/adminlte/adminlte.html' as adminlte with context %} 2 | 3 | {% macro filter_options(btn_class='dropdown-toggle') %} 4 | {{ adminlte.filter_dropdown_button() }} 5 | {% endmacro %} 6 | 7 | {% macro export_options(btn_class='dropdown-toggle') %} 8 | {{ adminlte.export_dropdown_button() }} 9 | {% endmacro %} 10 | 11 | {# todo: filter_form() #} 12 | {% macro filter_form() %} 13 |
14 | {% if sort_column is not none %} 15 | 16 | {% endif %} 17 | {% if sort_desc %} 18 | 19 | {% endif %} 20 | {% if search %} 21 | 22 | {% endif %} 23 | {% if page_size != default_page_size %} 24 | 25 | {% endif %} 26 |
27 | 28 | {% if active_filters %} 29 | {{ _gettext('Reset Filters') }} 30 | {% endif %} 31 |
32 | 33 |
34 |
35 |
36 | {% endmacro %} 37 | 38 | {% macro search_form(input_class="col-md-2") %} 39 | {{ adminlte.search_form(input_class) }} 40 | {% endmacro %} 41 | 42 | {# todo: page_size_form() #} 43 | {% macro page_size_form(generator, btn_class='dropdown-toggle') %} 44 | 45 | {{ page_size }} {{ _gettext('items') }} 46 | 47 | 52 | {% endmacro %} 53 | -------------------------------------------------------------------------------- /hermes_ui/models/source.py: -------------------------------------------------------------------------------- 1 | from hermes.mail import MailToolbox 2 | from hermes_ui.db import db 3 | from hermes_ui.models import User 4 | 5 | 6 | class BoiteAuxLettresImap(db.Model): 7 | 8 | id = db.Column(db.Integer(), primary_key=True) 9 | designation = db.Column(db.String(255), nullable=False) 10 | 11 | activation = db.Column(db.Boolean(), default=False) 12 | 13 | hote_distante = db.Column(db.String(255), nullable=False) 14 | nom_utilisateur = db.Column(db.String(255), nullable=False) 15 | mot_de_passe = db.Column(db.String(255), nullable=False) 16 | dossier_cible = db.Column(db.String(255), nullable=False, default='INBOX') 17 | 18 | enable_tls = db.Column(db.Boolean(), nullable=False, default=True) 19 | verification_certificat = db.Column(db.Boolean(), nullable=False, default=True) 20 | legacy_tls_support = db.Column(db.Boolean(), nullable=False, default=False) 21 | 22 | createur_id = db.Column(db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True) 23 | createur = db.relationship(User, primaryjoin="User.id==BoiteAuxLettresImap.createur_id") 24 | 25 | date_creation = db.Column(db.DateTime(timezone=True), nullable=False) 26 | date_modification = db.Column(db.DateTime(timezone=True), nullable=False) 27 | 28 | responsable_derniere_modification_id = db.Column(db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True) 29 | responsable_derniere_modification = db.relationship(User, 30 | primaryjoin="User.id==BoiteAuxLettresImap.responsable_derniere_modification_id") 31 | 32 | def get_mailtoolbox(self): 33 | """ 34 | :return: 35 | :rtype: MailToolbox 36 | """ 37 | existant = MailToolbox.fetch_instance(self.hote_distante, self.nom_utilisateur) 38 | if existant is None: 39 | return MailToolbox( 40 | self.hote_distante, 41 | self.nom_utilisateur, 42 | self.mot_de_passe, 43 | dossier_cible=self.dossier_cible, 44 | verify_peer=self.verification_certificat, 45 | use_secure_socket=self.enable_tls, 46 | legacy_secure_protocol=self.legacy_tls_support 47 | ) 48 | return existant 49 | -------------------------------------------------------------------------------- /configuration.dist.yml: -------------------------------------------------------------------------------- 1 | COMMON: &common 2 | SECRET_KEY: bHxU26p6 3 | SECURITY_URL_PREFIX: '/admin' 4 | 5 | SECURITY_PASSWORD_HASH: 'pbkdf2_sha512' 6 | SECURITY_PASSWORD_SALT: 'ATGUOHAELKiubahiughaerGOJAEGj' 7 | 8 | SECURITY_LOGIN_URL: '/connexion/' 9 | SECURITY_LOGOUT_URL: '/deconnexion/' 10 | SECURITY_REGISTER_URL: '/inscription/' 11 | SECURITY_RESET_URL: '/reinitialisation/' 12 | 13 | SECURITY_POST_LOGIN_VIEW: '/admin/' 14 | SECURITY_POST_LOGOUT_VIEW: '/admin/' 15 | SECURITY_POST_REGISTER_VIEW: '/admin/' 16 | SECURITY_POST_RESET_VIEW: '/admin/' 17 | 18 | SECURITY_REGISTERABLE: False 19 | SECURITY_RECOVERABLE: True 20 | SECURITY_CHANGEABLE: True 21 | SECURITY_SEND_REGISTER_EMAIL: False 22 | 23 | BABEL_DEFAULT_LOCALE: 'fr' 24 | BABEL_DEFAULT_TIMEZONE: 'Europe/Paris' 25 | BABEL_TRANSLATION_DIRECTORIES: '../hermes/locale' 26 | 27 | INCIDENT_NOTIFIABLE: ~ 28 | 29 | WEBPACKEXT_MANIFEST_PATH: 'build/manifest.json' 30 | WEBPACKEXT_PROJECT_DISTDIR: './build/' 31 | WEBPACKEXT_PROJECT_DISTURL: './build/' 32 | WEBPACK_ASSETS_URL: './assets/' 33 | 34 | BOOKMARKS: 35 | - 36 | LABEL: YOUR WEBSITE OR SERVICE 37 | ICON: fa-globe 38 | URL: 'http://intranet' 39 | 40 | 41 | DEVELOPMENT: &development 42 | <<: *common 43 | DEBUG: True 44 | SQLALCHEMY_DATABASE_URI: 'sqlite:///hermes.sqlite' 45 | SQLALCHEMY_ECHO: False 46 | SQLALCHEMY_TRACK_MODIFICATIONS: False 47 | SQLALCHEMY_RECORD_QUERIES: True 48 | EMAIL_HOST: 'hote-smtp' 49 | EMAIL_PORT: 587 50 | EMAIL_TIMEOUT: 10 51 | EMAIL_USE_TLS: True 52 | EMAIL_HOST_USER: '' 53 | EMAIL_HOST_PASSWORD: '' 54 | EMAIL_FROM: '' 55 | 56 | 57 | PRODUCTION: &production 58 | <<: *common 59 | SECRET_KEY: MerciDeMeChangerImmediatementAvantPremierLancement 60 | SECURITY_RECOVERABLE: False 61 | SQLALCHEMY_DATABASE_URI: 'mysql://hermes:hermes@mariadb/hermes?charset=utf8' 62 | SQLALCHEMY_ECHO: False 63 | SQLALCHEMY_TRACK_MODIFICATIONS: False 64 | SQLALCHEMY_RECORD_QUERIES: False 65 | EMAIL_HOST: 'hote-smtp' 66 | EMAIL_PORT: 587 67 | EMAIL_TIMEOUT: 10 68 | EMAIL_USE_TLS: True 69 | EMAIL_HOST_USER: 'smtp-utilisateur@hote-smtp' 70 | EMAIL_HOST_PASSWORD: 'secret_smtp' 71 | EMAIL_FROM: 'smtp-utilisateur@hote-smtp' 72 | INCIDENT_NOTIFIABLE: 'destinataire@gmail.com' 73 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% import 'admin/adminlte/forms.html' as forms with context %} 4 | 5 | {% block body %} 6 |
7 |

{{ admin_view.name }}

8 |
9 |
10 |
11 |
12 |
13 |
14 | {% set modal_name = 'View' + ' #' + request.args.get('id') %} 15 | {{ forms.form_header(modal_name, 'fa fa-eye', is_modal=False) }} 16 | {% block details_search %} 17 | {{ forms.form_search() }} 18 | {% endblock %} 19 |
20 |
21 | {% block details_table %} 22 | {{ forms.form_view(details_columns) }} 23 | {% endblock %} 24 |
25 | 41 |
42 |
43 |
44 |
45 | {% endblock %} 46 | 47 | {% block tail %} 48 | {{ super() }} 49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .nox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | *.py,cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | db.sqlite3-journal 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # IPython 76 | profile_default/ 77 | ipython_config.py 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # pipenv 83 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 84 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 85 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 86 | # install all needed dependencies. 87 | #Pipfile.lock 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .dmypy.json 117 | dmypy.json 118 | 119 | # Pyre type checker 120 | .pyre/ 121 | 122 | .idea/ 123 | 124 | node_modules/ 125 | *.sqlite 126 | manifest.json 127 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/AdminLTE-select2.min.css: -------------------------------------------------------------------------------- 1 | .select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none;border:1px solid #3c8dbc}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px} -------------------------------------------------------------------------------- /hermes/source.py: -------------------------------------------------------------------------------- 1 | from hermes.analysis import ExtractionInteret 2 | from hermes.session import Session 3 | from hashlib import sha512 4 | from pickle import dumps 5 | 6 | 7 | class ManipulationSourceException(Exception): 8 | pass 9 | 10 | 11 | class ExtractionSourceException(Exception): 12 | pass 13 | 14 | 15 | class SourceFactory: 16 | 17 | INSTANCES = list() # type: list[SourceFactory] 18 | 19 | def __init__(self, designation): 20 | self._designation = designation 21 | 22 | @property 23 | def designation(self): 24 | return self._designation 25 | 26 | def extraire(self): 27 | """ 28 | 29 | :return: List of extracted Source 30 | :rtype list[Source] 31 | """ 32 | raise NotImplementedError 33 | 34 | def copier(self, source, destination): 35 | raise NotImplementedError 36 | 37 | def deplacer(self, source, destination): 38 | raise NotImplementedError 39 | 40 | def supprimer(self, source): 41 | raise NotImplementedError 42 | 43 | def __repr__(self): 44 | return "".format(self.designation) 45 | 46 | 47 | class Source: 48 | 49 | def __init__(self, titre, corps): 50 | """ 51 | Une source est obligatoirement composée d'un titre et d'un corps 52 | :param str titre: 53 | :param str corps: 54 | """ 55 | self._extraction_interet = ExtractionInteret(titre, corps) # type: ExtractionInteret 56 | 57 | self._session = Session() 58 | self._factory = None # type: SourceFactory 59 | 60 | @property 61 | def factory(self): 62 | return self._factory 63 | 64 | @factory.setter 65 | def factory(self, new_attached_factory): 66 | if isinstance(new_attached_factory, SourceFactory): 67 | self._factory = new_attached_factory 68 | 69 | @property 70 | def raw(self): 71 | raise NotImplementedError 72 | 73 | @property 74 | def session(self): 75 | return self._session 76 | 77 | @property 78 | def extraction_interet(self): 79 | return self._extraction_interet 80 | 81 | @property 82 | def titre(self): 83 | raise NotImplementedError 84 | 85 | @property 86 | def corps(self): 87 | raise NotImplementedError 88 | 89 | @property 90 | def nom_fichier(self): 91 | raise NotImplementedError 92 | 93 | @property 94 | def hash(self): 95 | return sha512(bytes(dumps(self))).hexdigest() 96 | -------------------------------------------------------------------------------- /docs/CRITERES.md: -------------------------------------------------------------------------------- 1 |

Les critères

2 | 3 | Nous développons ici pour chaque type de critère : 4 | 5 | - Une courte description de ce qui est recherché 6 | - Ce qui est capturé (stockable dans une variable) 7 | 8 | ## Identifiant 9 | 10 | La recherche d'identifiant correspond à tout ce qui ressemble à PREFIXE-NUMEROS. 11 | 12 | ``` 13 | Ticket D91827631 : Changement majeur de l'infra PBX-FR 14 | ``` 15 | 16 | **Exemple :** « On recherche un identifiant commençant par la lettre D dans le titre du message » 17 | 18 | **Capture :** D91827631 19 | 20 | Ici le préfixe est `D`. 21 | 22 | ## Recherche d'expression 23 | 24 | Rechercher une expression localisable 25 | 26 | ``` 27 | Bienvenue à Antoine GAUTIER au sein de la ville de Paris 28 | ``` 29 | 30 | **Exemple :** « Je recherche une expression comprise entre, ‘Bienvenue à’... et ...’au sein de la ville de Paris’ dans le corps du message » 31 | 32 | **Capture :** `Antoine GAUTIER` 33 | 34 | ## Date 35 | 36 | Trouver une date peu importe le format de représentation sachant le prefixe. 37 | - RFC 3339 38 | - RFC 2822 39 | - Y-m-d 40 | - d-m-Y 41 | 42 | ``` 43 | Bilan du 10/11/2020 pour le concours d'excellence 44 | ``` 45 | 46 | **Exemple :** « Je recherche une date juxtaposée à l’expression ‘Bilan du‘ dans le titre du message » 47 | 48 | **Capture :** `10/11/2020` 49 | 50 | ## XPath (HTML) 51 | 52 | Trouver un noeud dans un arbre XML soit le corps HTML de votre message. 53 | 54 | ```html 55 | 56 | 57 |
58 | Bonjour ! 59 |
60 | 61 | 62 | ``` 63 | 64 | **Exemple :** « Je souhaite extraire le contenu de la première div ayant la classe .sujet » 65 | 66 | **Capture :** `Bonjour !` 67 | 68 | ## Clé 69 | 70 | **Brève explication :** Le programme Hermès arrive à trouver automatiquement certaine association explicite dans un texte. 71 | Toute expression sous la forme de A -> B (A associé à B). Exemple : « Contact Externe : Ahmed TAHRI » 72 | Dans cet exemple, « Contact Externe » sera la clé auto-découverte. 73 | 74 | ``` 75 | Bonjour Michael, 76 | 77 | Votre ticket de support numéro 761637 est maintenant ouvert. 78 | 79 | Corresp. interne : Dep. RH 80 | Contact Externe : Ahmed TAHRI 81 | 82 | Merci de votre patience. 83 | ``` 84 | 85 | **Exemple :** « Je vérifie que le moteur a trouvé la clé ‘Contact Externe’ dans le message » 86 | 87 | **Capture :** `Ahmed TAHRI` 88 | 89 | ## Expression exacte dans la clé 90 | 91 | Permet de vérifier la présence d'un mot ou d'une suite de mot depuis la valeur associée à une clé auto-découverte par 92 | Hermès. 93 | 94 | **Exemple :** « Je vérifie que la clé ‘Contact Externe’ contient bien » 95 | 96 | **Capture :** `Ahmed TAHRI` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | .idea/ 128 | msg_parser/ 129 | configuration.yml 130 | configurations/*.yml 131 | 132 | node_modules/ 133 | *.sqlite 134 | manifest.json 135 | .DS_Store 136 | -------------------------------------------------------------------------------- /hermes_ui/models/session.py: -------------------------------------------------------------------------------- 1 | from hermes_ui.models.automate import Automate, ActionNoeud 2 | from hermes_ui.models.detecteur import Detecteur, RechercheInteret 3 | from hermes_ui.db import db 4 | 5 | 6 | class AutomateExecution(db.Model): 7 | 8 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True) 9 | 10 | automate_id = db.Column(db.Integer(), db.ForeignKey(Automate.id, ondelete='CASCADE'), nullable=False) 11 | automate = db.relationship(Automate, cascade='all, save-update, delete, merge') 12 | 13 | sujet = db.Column(db.String(255), nullable=False) 14 | corps = db.Column(db.Text(), nullable=False) 15 | 16 | date_creation = db.Column(db.DateTime(timezone=True), nullable=False) 17 | 18 | detecteur_id = db.Column(db.Integer(), db.ForeignKey(Detecteur.id, ondelete='CASCADE'), nullable=False) 19 | detecteur = db.relationship(Detecteur, cascade='all, save-update, merge') 20 | 21 | validation_detecteur = db.Column(db.Boolean(), default=False, nullable=False) 22 | validation_automate = db.Column(db.Boolean(), default=False, nullable=False) 23 | 24 | explications_detecteur = db.Column(db.Text(), nullable=True) 25 | 26 | logs = db.Column(db.Text(), nullable=True) 27 | 28 | date_finalisation = db.Column(db.DateTime(timezone=True), nullable=False) 29 | 30 | actions_noeuds_executions = db.relationship('ActionNoeudExecution') 31 | recherches_interets_executions = db.relationship('RechercheInteretExecution') 32 | 33 | 34 | class ActionNoeudExecution(db.Model): 35 | 36 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True) 37 | 38 | automate_execution_id = db.Column(db.BigInteger(), db.ForeignKey(AutomateExecution.id, ondelete='CASCADE'), nullable=False) 39 | automate_execution = db.relationship(AutomateExecution, cascade='all, save-update, delete, merge') 40 | 41 | action_noeud_id = db.Column(db.Integer(), db.ForeignKey(ActionNoeud.id, ondelete='CASCADE')) 42 | action_noeud = db.relationship(ActionNoeud) 43 | 44 | validation_action_noeud = db.Column(db.Boolean(), default=False, nullable=False) 45 | 46 | args_payload = db.Column(db.Text(), nullable=True) 47 | payload = db.Column(db.Text(), nullable=True) 48 | 49 | 50 | class RechercheInteretExecution(db.Model): 51 | 52 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True) 53 | 54 | automate_execution_id = db.Column(db.BigInteger(), db.ForeignKey(AutomateExecution.id), nullable=False) 55 | automate_execution = db.relationship(AutomateExecution) 56 | 57 | recherche_interet_id = db.Column(db.Integer(), db.ForeignKey(RechercheInteret.id)) 58 | recherche_interet = db.relationship(RechercheInteret) 59 | 60 | validation_recherche_interet = db.Column(db.Boolean(), default=False, nullable=False) 61 | 62 | payload = db.Column(db.Text(), nullable=True) 63 | 64 | 65 | class AutomateExecutionDataTable: 66 | 67 | def __init__(self, executions): 68 | self.data = executions 69 | -------------------------------------------------------------------------------- /docs/CHAPITRE-2.md: -------------------------------------------------------------------------------- 1 |

Création des variables globales

2 | 3 | ## ✨ C'est quoi ? 4 | 5 | Juste avant, nous avons parlé des variables *simplifiées* sous Hermes. Pour mémo, les variables peuvent être issus de : 6 | 7 | - Le résultat d'un critère de recherche 8 | - **Une variable accessible globalement**, depuis le menu "Mes variables globales" 9 | - Le résultat d'une action 10 | 11 | Il est parfois très utile de disposer d'une variable partagée peut-importe l'automate, comme par exemple vos identifiants SMTP pour envoyer un message. 12 | 13 | ## Où ? 14 | 15 | La création de vos variables globales est possible depuis le menu "Mes variables globales". 16 | 17 |

18 | Capture d’écran 2020-01-08 à 13 23 15 19 | 20 | Capture d’écran 2020-01-08 à 13 23 26 21 |

22 | 23 | ## Choix de format 24 | 25 | ### Classique 26 | 27 | La désignation représente le nom de votre future variable. 28 | 29 | Imaginons que vous souhaiteriez conserver le nom d'utilisateur et le mot de passe SMTP. 30 | 31 | Vous allez créer deux variables, l'une `identifiant_smtp` et l'autre `mot_de_passe_smtp`. 32 | Qui seront par la suite accessible par la syntaxe `{{ identifiant_smtp }}`. 33 | 34 | Vous remplirez comme suit : 35 | 36 |

37 | Capture d’écran 2020-01-08 à 13 28 21 38 |

39 | 40 | ### Avancé 41 | 42 | Si vous le souhaitez, vous pouvez exploiter une valeur plus complexe. Vous pouvez insérer dans *Valeur* : 43 | 44 | - Une chaîne JSON 45 | - Une chaîne YAML 46 | 47 | Reprenons notre cas ci-dessus. Au lieu de créer deux variables `identifiant_smtp` et l'autre `mot_de_passe_smtp`, 48 | créons une seule variable `mon_compte_smtp`. 49 | 50 | Pour ce faire nous constituons une chaîne **JSON** tel que : 51 | 52 | ```json 53 | { 54 | "mon_compte_smtp": { 55 | "identifiant": "abcdef@mon-provider.com", 56 | "mot_de_passe": "azerty" 57 | } 58 | } 59 | ``` 60 | 61 | Et donc en remplissant le formulaire de cette manière : 62 | 63 |

64 | Capture d’écran 2020-01-08 à 13 34 03 65 |

66 | 67 | Vous allez pouvoir invoquer `{{ mon_compte_smtp.identifiant }}` et `{{ mon_compte_smtp.mot_de_passe }}`. 68 | 69 | ⚠️ Vous remarquerez que le nom de votre variable n'est plus la **désignation** mais le nom de la clé/index racine. Ceci s'applique dans le cas où le format sélectionné est `JSON` ou `YAML`. 70 | 71 | ## Pour aller plus loin 72 | 73 | - [ ] [Mise en place de votre/vos boîte(s) IMAP](CHAPITRE-3.md) 74 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/model/inline_list_base.html: -------------------------------------------------------------------------------- 1 | {% macro render_inline_fields(field, template, render, check=None) %} 2 |
3 | {# existing inline form fields #} 4 |
5 | {% for subfield in field %} 6 |
7 | {%- if not check or check(subfield) %} 8 | 9 | 10 | {{ field.label.text }} #{{ loop.index }} 11 |
12 | {% if subfield.get_pk and subfield.get_pk() %} 13 | 14 | 16 | {% else %} 17 | 21 | {% endif %} 22 |
23 |
24 |
25 |
26 | {%- endif -%} 27 | {{ render(subfield) }} 28 |
29 | {% endfor %} 30 |
31 | 32 | {# template for new inline form fields #} 33 |
34 | {% filter forceescape %} 35 |
36 | 37 | {{ _gettext('New') }} {{ field.label.text }} 38 |
39 | 42 |
43 |
44 |
45 | {{ render(template) }} 46 |
47 | {% endfilter %} 48 |
49 | {{ _gettext('Add') }} {{ field.label.text }} 51 |
52 | {% endmacro %} 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='hermes', 5 | version='1.0.15', 6 | author='Ahmed TAHRI', 7 | author_email='ahmed.tahri@cloudnursery.dev', 8 | description='Automates programmables à réaction aux échanges électroniques reçus depuis une boîte IMAP4', 9 | license='NPOSL-3.0', 10 | packages=['hermes', 'hermes_ui'], 11 | install_requires=[ 12 | 'Flask==1.1.*', 13 | 'requests_html', 14 | 'python-slugify', 15 | 'requests>=2.23,<3.0', 16 | 'prettytable', 17 | 'imapclient>=2.1.0,<3.0', 18 | 'zeep>=3.4,<4.0', 19 | 'tqdm', 20 | 'emails>=0.6.1,<1.0', 21 | 'flask_security', 22 | 'flask_admin', 23 | 'flask_sqlalchemy==2.4.*', 24 | 'flask_migrate', 25 | 'pyyaml', 26 | 'marshmallow>=3.5.2,<4.0', 27 | 'flask_marshmallow>=0.12,<1.0', 28 | 'marshmallow-sqlalchemy>=0.23,<1.0', 29 | 'python-dateutil', 30 | 'jinja2', 31 | 'flask-emails', 32 | 'ruamel.std.zipfile', 33 | 'ics==0.5', 34 | 'olefile>=0.46,<1.0', 35 | 'html5lib', 36 | 'pandas', 37 | 'flask_babel>=1.0.*', 38 | 'records', 39 | 'flask_babel', 40 | 'unidecode', 41 | 'pandas', 42 | 'records', 43 | 'marshmallow-oneofschema>=2.0.1,<2.1', 44 | 'loguru', 45 | 'Flask-Webpack>=0.1,<1.0', 46 | 'mysql-connector-python', 47 | 'werkzeug==1.0.*', 48 | 'sqlalchemy==1.3.*', 49 | 'flask_webpackext==1.0.*', 50 | 'pyopenssl>=19.1.0', 51 | 'msg_parser>=1.1.0', 52 | 'wtforms', 53 | 'dateparser', 54 | 'kiss-headers>=2.0.4,<3.0', 55 | 'email_validator>=1.1.0' 56 | ], 57 | tests_require=[], 58 | keywords=[], 59 | dependency_links=[ 60 | 'git+https://github.com/Ousret/python-emails.git#egg=emails' 61 | ], 62 | classifiers=[ 63 | 'Development Status :: 5 - Production/Stable', 64 | 'Environment :: Win32 (MS Windows)', 65 | 'Environment :: X11 Applications', 66 | 'Environment :: MacOS X', 67 | 'Intended Audience :: Developers', 68 | 'License :: OSI Approved :: MIT License', 69 | 'Operating System :: OS Independent', 70 | 'Programming Language :: Python', 71 | 'Programming Language :: Python :: 3.6', 72 | 'Programming Language :: Python :: 3.7', 73 | 'Programming Language :: Python :: 3.8', 74 | ], 75 | message_extractors={ 76 | 'hermes': [ 77 | ('**.py', 'python', None), 78 | ('templates/**.html', 'jinja2', None), 79 | ('assets/**', 'ignore', None), 80 | ('static/**', 'ignore', None) 81 | ], 82 | 'hermes_ui': [ 83 | ('**.py', 'python', None), 84 | ('templates/**.html', 'jinja2', None), 85 | ('assets/**', 'ignore', None), 86 | ('static/**', 'ignore', None) 87 | ], 88 | }, 89 | 90 | ) 91 | -------------------------------------------------------------------------------- /hermes_ui/assets/styles/AdminLTE-select2.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Plugin: Select2 3 | * --------------- 4 | */ 5 | .select2-container--default.select2-container--focus, 6 | .select2-selection.select2-container--focus, 7 | .select2-container--default:focus, 8 | .select2-selection:focus, 9 | .select2-container--default:active, 10 | .select2-selection:active { 11 | outline: none; 12 | } 13 | .select2-container--default .select2-selection--single, 14 | .select2-selection .select2-selection--single { 15 | border: 1px solid #d2d6de; 16 | border-radius: 0; 17 | padding: 6px 12px; 18 | height: 34px; 19 | } 20 | .select2-container--default.select2-container--open { 21 | border-color: #3c8dbc; 22 | } 23 | .select2-dropdown { 24 | border: 1px solid #d2d6de; 25 | border-radius: 0; 26 | } 27 | .select2-container--default .select2-results__option--highlighted[aria-selected] { 28 | background-color: #3c8dbc; 29 | color: white; 30 | } 31 | .select2-results__option { 32 | padding: 6px 12px; 33 | user-select: none; 34 | -webkit-user-select: none; 35 | } 36 | .select2-container .select2-selection--single .select2-selection__rendered { 37 | padding-left: 0; 38 | padding-right: 0; 39 | height: auto; 40 | margin-top: -4px; 41 | } 42 | .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { 43 | padding-right: 6px; 44 | padding-left: 20px; 45 | } 46 | .select2-container--default .select2-selection--single .select2-selection__arrow { 47 | height: 28px; 48 | right: 3px; 49 | } 50 | .select2-container--default .select2-selection--single .select2-selection__arrow b { 51 | margin-top: 0; 52 | } 53 | .select2-dropdown .select2-search__field, 54 | .select2-search--inline .select2-search__field { 55 | border: 1px solid #d2d6de; 56 | } 57 | .select2-dropdown .select2-search__field:focus, 58 | .select2-search--inline .select2-search__field:focus { 59 | outline: none; 60 | border: 1px solid #3c8dbc; 61 | } 62 | .select2-container--default .select2-results__option[aria-disabled=true] { 63 | color: #999; 64 | } 65 | .select2-container--default .select2-results__option[aria-selected=true] { 66 | background-color: #ddd; 67 | } 68 | .select2-container--default .select2-results__option[aria-selected=true], 69 | .select2-container--default .select2-results__option[aria-selected=true]:hover { 70 | color: #444; 71 | } 72 | .select2-container--default .select2-selection--multiple { 73 | border: 1px solid #d2d6de; 74 | border-radius: 0; 75 | } 76 | .select2-container--default .select2-selection--multiple:focus { 77 | border-color: #3c8dbc; 78 | } 79 | .select2-container--default.select2-container--focus .select2-selection--multiple { 80 | border-color: #d2d6de; 81 | } 82 | .select2-container--default .select2-selection--multiple .select2-selection__choice { 83 | background-color: #3c8dbc; 84 | border-color: #367fa9; 85 | padding: 1px 10px; 86 | color: #fff; 87 | } 88 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { 89 | margin-right: 5px; 90 | color: rgba(255, 255, 255, 0.7); 91 | } 92 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { 93 | color: #fff; 94 | } 95 | .select2-container .select2-selection--single .select2-selection__rendered { 96 | padding-right: 10px; 97 | } 98 | -------------------------------------------------------------------------------- /hermes_ui/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /hermes_ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | let webpack = require('webpack'); 2 | let config = require('./assets/config'); 3 | let ManifestPlugin = require('webpack-manifest-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | let path = require('path'); 8 | 9 | module.exports = { 10 | context: path.join(__dirname, config.build.context), 11 | entry: { 12 | app: "./scripts/app.js", 13 | app_help: './scripts/app_help.js', 14 | app_hermes: './scripts/app_hermes.js' 15 | }, 16 | output: { 17 | path: path.join(__dirname, config.build.assetsPath), 18 | filename: 'js/[name].[chunkhash].js', 19 | publicPath: path.join(__dirname, config.build.assetsURL) 20 | }, 21 | optimization: { 22 | minimizer: [new TerserPlugin()], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | { 30 | loader: MiniCssExtractPlugin.loader, 31 | options: { 32 | publicPath: '../', 33 | hmr: process.env.NODE_ENV === 'development', 34 | }, 35 | }, 36 | //'style-loader', 37 | 'css-loader' 38 | ], 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | exclude: /node_modules/ 44 | }, 45 | { 46 | test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/, 47 | loader: 'url-loader', 48 | options: { 49 | limit: 10000, 50 | name: 'fonts/[name].[hash:7].[ext]', 51 | publicPath: '../build/', 52 | } 53 | }, 54 | { 55 | test: /\.(png|jpg|gif)$/i, 56 | use: [ 57 | { 58 | loader: 'url-loader', 59 | options: { 60 | limit: 8192, 61 | }, 62 | }, 63 | ], 64 | }, 65 | ] 66 | }, 67 | plugins: [ 68 | new ManifestPlugin({ 69 | fileName: 'manifest.json', 70 | stripSrc: true, 71 | publicPath: config.build.assetsURL 72 | }), 73 | new MiniCssExtractPlugin( 74 | { 75 | // Options similar to the same options in webpackOptions.output 76 | // all options are optional 77 | filename: '[name].css', 78 | chunkFilename: '[id].css', 79 | ignoreOrder: false, // Enable to remove warnings about conflicting order 80 | 81 | } 82 | ), 83 | new OptimizeCssAssetsPlugin({ 84 | assetNameRegExp: /\.css$/g, 85 | cssProcessor: require('cssnano'), 86 | cssProcessorPluginOptions: { 87 | preset: ['default', {discardComments: {removeAll: true}}], 88 | }, 89 | canPrint: true 90 | }), 91 | // new webpack.ProvidePlugin({ 92 | // jQuery: 'jquery/src/jquery', 93 | // $: 'jquery/src/jquery', 94 | // jquery: 'jquery/src/jquery', 95 | // 'window.jQuery': 'jquery/src/jquery' 96 | // }) 97 | ] 98 | }; -------------------------------------------------------------------------------- /test/test_automate.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hermes.automate import * 3 | from hermes.detecteur import * 4 | 5 | 6 | class FakeSource(Source): 7 | 8 | def __init__(self, titre, corps): 9 | super().__init__(titre, corps) 10 | self._titre = titre 11 | self._corps = corps 12 | 13 | def titre(self): 14 | return self._titre 15 | 16 | def corps(self): 17 | return self._corps 18 | 19 | 20 | ma_source = FakeSource( 21 | "#Mesures Relève des températures du mois d'Août le 12/12/2020", 22 | """Réf-091 23 | 24 | Bonjour JOHN DOE ! 25 | 26 | Date de mesure : 12/12/2020 27 | Auteur : Ahmed TAHRI 28 | 29 | Nous avons mesurés à 38 reprises la température de votre ville natale. 30 | 31 | Merci de votre attention. 32 | """ 33 | ) 34 | 35 | mon_detecteur = Detecteur( 36 | "Relève de température" 37 | ) 38 | 39 | mon_detecteur.je_veux( 40 | IdentificateurRechercheInteret( 41 | "Recherche de la référence", 42 | "Réf-", 43 | friendly_name='reference_releve' 44 | ) 45 | ) 46 | 47 | mon_detecteur.je_veux( 48 | DateRechercheInteret( 49 | "Recherche date de relève", 50 | "Relève des températures du mois d'Août le", 51 | friendly_name='date_releve' 52 | ) 53 | ) 54 | 55 | mon_detecteur.je_veux( 56 | ExpressionCleRechercheInteret( 57 | "Recherche d'une phrase à l'identique", 58 | "Nous avons mesurés à" 59 | ) 60 | ) 61 | 62 | mon_detecteur.je_veux( 63 | LocalisationExpressionRechercheInteret( 64 | "Recherche du nombre de relève température", 65 | "reprises", 66 | "Nous avons mesurés à", 67 | friendly_name='nombre_releve' 68 | ) 69 | ) 70 | 71 | mon_detecteur.je_veux( 72 | InformationRechercheInteret( 73 | "Recherche de hashtag", 74 | "Mesures" 75 | ) 76 | ) 77 | 78 | mon_detecteur.je_veux( 79 | CleRechercheInteret( 80 | "Présence de Auteur", 81 | "Auteur" 82 | ) 83 | ) 84 | 85 | mon_detecteur.je_veux( 86 | ExpressionDansCleRechercheInteret( 87 | "Vérifier que Ahmed est auteur", 88 | "Auteur", 89 | "Ahmed" 90 | ) 91 | ) 92 | 93 | action_a = RequeteHttpActionNoeud( 94 | "Requête sur httpbin", 95 | "https://httpbin.org/post", 96 | "POST", 97 | { 98 | 'nombre_releve': '{{ nombre_releve }}', 99 | 'date_releve': '{{ date_releve }}', 100 | 'id': '{{ reference_releve }}' 101 | }, 102 | None, 103 | None, 104 | 200, 105 | friendly_name='reponse_webservice_httpbin' 106 | ) 107 | 108 | action_b = ComparaisonVariableActionNoeud( 109 | "Vérifier la cohérence réponse du website", 110 | "{{ reponse_webservice_httpbin.form.id }}", 111 | '==', 112 | '{{ reference_releve }}', 113 | None 114 | ) 115 | 116 | 117 | class TestAutomate(unittest.TestCase): 118 | def test_automate_basic(self): 119 | mon_automate = Automate( 120 | "Réaction à la reception des mesures de température", 121 | mon_detecteur 122 | ) 123 | 124 | mon_automate.action_racine = action_a 125 | action_a.je_realise_en_cas_reussite(action_b) 126 | 127 | self.assertTrue( 128 | mon_automate.lance_toi( 129 | ma_source 130 | ) 131 | ) 132 | 133 | self.assertEqual( 134 | 2, 135 | len(mon_automate.actions_lancees) 136 | ) 137 | 138 | 139 | if __name__ == '__main__': 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ahmed.tahri@cloudnursery.dev. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /hermes_ui/incident.py: -------------------------------------------------------------------------------- 1 | from hermes.automate import EnvoyerMessageSmtpActionNoeud 2 | from hermes.source import Source 3 | from hermes.logger import logger, mem_handler 4 | 5 | from .models import Automate 6 | 7 | 8 | class SourceLogger(Source): 9 | 10 | def __init__(self): 11 | super().__init__('', '') 12 | 13 | self._destinataire = 'admin@localhost' 14 | self._raw_content = '\n'.join([str(el.msg) for el in mem_handler.buffer]).encode('utf-8') 15 | 16 | @property 17 | def raw(self): 18 | return self._raw_content 19 | 20 | @property 21 | def nom_fichier(self): 22 | return 'interoperabilite.log' 23 | 24 | @property 25 | def destinataire(self): 26 | return self._destinataire 27 | 28 | @destinataire.setter 29 | def destinataire(self, nouveau_destinataire): 30 | self._destinataire = nouveau_destinataire 31 | 32 | @property 33 | def titre(self): 34 | return 'Traces de votre interopérabilité' 35 | 36 | 37 | class NotificationIncident: 38 | 39 | EMAIL_HOST = None 40 | EMAIL_PORT = None 41 | EMAIL_TIMEOUT = None 42 | EMAIL_USE_TLS = None 43 | EMAIL_HOST_USER = None 44 | EMAIL_HOST_PASSWORD = None 45 | EMAIL_FROM = None 46 | 47 | EMAIL_TO_DEFAULT = None 48 | 49 | @staticmethod 50 | def init_app(app): 51 | """ 52 | :param flask.Flask app: 53 | """ 54 | NotificationIncident.EMAIL_HOST = app.config.get('EMAIL_HOST', 'localhost') 55 | NotificationIncident.EMAIL_PORT = app.config.get('EMAIL_PORT', 25) 56 | NotificationIncident.EMAIL_TIMEOUT = app.config.get('EMAIL_TIMEOUT', 10) 57 | NotificationIncident.EMAIL_USE_TLS = app.config.get('EMAIL_USE_TLS', False) 58 | NotificationIncident.EMAIL_HOST_USER = app.config.get('EMAIL_HOST_USER', None) 59 | NotificationIncident.EMAIL_HOST_PASSWORD = app.config.get('EMAIL_HOST_PASSWORD', None) 60 | NotificationIncident.EMAIL_FROM = app.config.get('EMAIL_FROM', None) 61 | 62 | NotificationIncident.EMAIL_TO_DEFAULT = app.config.get('INCIDENT_NOTIFIABLE', None) 63 | 64 | @staticmethod 65 | def prevenir(automate, source, titre, description): 66 | """ 67 | :param Automate automate: 68 | :param Source source: 69 | :param str titre: 70 | :param str description: 71 | :return: 72 | """ 73 | 74 | if NotificationIncident.EMAIL_HOST is None or NotificationIncident.EMAIL_PORT is None or NotificationIncident.EMAIL_USE_TLS is None: 75 | return False 76 | 77 | ma_fausse_source = SourceLogger() 78 | ma_fausse_source.destinataire = NotificationIncident.EMAIL_FROM 79 | 80 | if automate is None: 81 | return False 82 | 83 | mon_action = EnvoyerMessageSmtpActionNoeud( 84 | "Envoyer une notification d'erreur au(x) responsable(s) de l'automate", 85 | str(NotificationIncident.EMAIL_TO_DEFAULT) + ((',' + automate.responsable_derniere_modification.email) if automate.responsable_derniere_modification is not None else ''), 86 | titre, 87 | description, 88 | hote_smtp=NotificationIncident.EMAIL_HOST, 89 | port_smtp=NotificationIncident.EMAIL_PORT, 90 | nom_utilisateur=NotificationIncident.EMAIL_HOST_USER, 91 | mot_de_passe=NotificationIncident.EMAIL_HOST_PASSWORD, 92 | enable_tls=NotificationIncident.EMAIL_USE_TLS, 93 | pj_source=True, 94 | source_pj_complementaire=source, 95 | force_keep_template=True 96 | ) 97 | 98 | return mon_action.je_realise(ma_fausse_source) 99 | -------------------------------------------------------------------------------- /test/test_critere.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hermes.source import Source 3 | from hermes.detecteur import * 4 | 5 | 6 | ma_source = Source( 7 | "#Mesures Relève des températures du mois d'Août le 12/12/2020", 8 | """Réf-091 9 | 10 | Bonjour JOHN DOE ! 11 | 12 | Date de mesure : 12/12/2020 13 | Auteur : Ahmed TAHRI 14 | 15 | Nous avons mesurés à 38 reprises la température de votre ville natale. 16 | 17 | Merci de votre attention. 18 | """ 19 | ) 20 | 21 | 22 | class TestCriteres(unittest.TestCase): 23 | 24 | def test_identifiant(self): 25 | 26 | critere = IdentificateurRechercheInteret( 27 | "Recherche de la référence", 28 | "Réf-" 29 | ) 30 | 31 | self.assertTrue( 32 | critere.tester_sur(ma_source.extraction_interet) 33 | ) 34 | 35 | self.assertEqual( 36 | critere.value, 37 | "Réf-091" 38 | ) 39 | 40 | def test_date(self): 41 | 42 | critere = DateRechercheInteret( 43 | "Recherche date de relève", 44 | "Relève des températures du mois d'Août le" 45 | ) 46 | 47 | self.assertTrue( 48 | critere.tester_sur(ma_source.extraction_interet) 49 | ) 50 | 51 | self.assertEqual( 52 | critere.value, 53 | ' 12/12/2020' 54 | ) 55 | 56 | def test_expression_exacte(self): 57 | 58 | critere = ExpressionCleRechercheInteret( 59 | "Recherche d'une phrase à l'identique", 60 | "Nous avons mesurés à" 61 | ) 62 | 63 | self.assertTrue( 64 | critere.tester_sur(ma_source.extraction_interet) 65 | ) 66 | 67 | self.assertEqual( 68 | critere.value, 69 | True 70 | ) 71 | 72 | def test_localisation_expression(self): 73 | 74 | critere = LocalisationExpressionRechercheInteret( 75 | "Recherche du nombre de relève température", 76 | "reprises", 77 | "Nous avons mesurés à" 78 | ) 79 | 80 | self.assertTrue( 81 | critere.tester_sur(ma_source.extraction_interet) 82 | ) 83 | 84 | self.assertEqual( 85 | critere.value, 86 | '38' 87 | ) 88 | 89 | def test_information(self): 90 | 91 | critere = InformationRechercheInteret( 92 | "Recherche de hashtag", 93 | "Mesures" 94 | ) 95 | 96 | self.assertTrue( 97 | critere.tester_sur(ma_source.extraction_interet) 98 | ) 99 | 100 | self.assertEqual( 101 | critere.value, 102 | "Mesures" 103 | ) 104 | 105 | def test_cle_recherche(self): 106 | 107 | critere = CleRechercheInteret( 108 | "Présence de Auteur", 109 | "Auteur" 110 | ) 111 | 112 | self.assertTrue( 113 | critere.tester_sur(ma_source.extraction_interet) 114 | ) 115 | 116 | self.assertEqual( 117 | critere.value, 118 | "Ahmed TAHRI" 119 | ) 120 | 121 | def test_expression_exacte_dans_cle(self): 122 | 123 | critere = ExpressionDansCleRechercheInteret( 124 | "Vérifier que Ahmed est auteur", 125 | "Auteur", 126 | "Ahmed" 127 | ) 128 | 129 | self.assertTrue( 130 | critere.tester_sur(ma_source.extraction_interet) 131 | ) 132 | 133 | self.assertEqual( 134 | critere.value, 135 | "Ahmed" 136 | ) 137 | 138 | 139 | if __name__ == '__main__': 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /docs/CHAPITRE-7.md: -------------------------------------------------------------------------------- 1 |

Tester & Débugger un Automate

2 | 3 | **Pré-requis:** Avoir mis en place au moins un détecteur, la description associée d'un automate ainsi que des actions prêtes. 4 | 5 | ## ✨ Phase finale de conception 6 | 7 | Lors de la création d'un **détecteur** nous avions vu qu'il est possible de le tester au fur et à mesure de la conception. 8 | Il en va de même pour la création des actions d'un automate. 9 | 10 | ### Mode de test et de prodution 11 | 12 | Dans la zone "Choisir Automate" et aussi dans la boîte de dialogue création "Description d'un Automate", 13 | il y a une option "Production". 14 | 15 | Cette checkbox, décochée, permet d'empêcher le moteur de surveillance continue de votre boîte IMAP d'exécuter votre automate. 16 | 17 | Pendant la phase de conception il est recommandé de laisser votre automate en mode test. Donc checkbox décochée. 18 | 19 | ### Lancer l'automate seul 20 | 21 | Depuis la page "Éditeur d'Automate", selectionnez votre automate depuis la zone "Choisir Automate". 22 | Puis une fois dans cet état. 23 | 24 |

25 | Capture d’écran 2020-01-09 à 11 21 41 26 |

27 | 28 | Cliquez sur "Tester Automate", puis confirmer le démarrage. 29 | 30 | ⚠️ Une limitation empêche de pouvoir conduire un test autrement que depuis votre boîte IMAP. 31 | Ce qui signifie que vous devez vous assurer que : 32 | 33 | - Votre message type est disponible dans le dossier dans lequel Hermes ira lire les messages 34 | - La boucle de surveillance des messages est suspendue 35 | - Votre automate est en mode test 36 | 37 | Une fois les critères réunis, vous observerez le résultat en temps réel depuis la zone console. 38 | 39 |

40 | Capture d’écran 2020-01-09 à 15 18 04 41 |

42 | 43 | ### Résultat d'un automate 44 | 45 | Un automate termine par une réussite si la dernière action de l'arbre se termine correctement. 46 | 47 | ## 📊 Historique des lancements 48 | 49 | Hermes permet de consulter les 50 derniers lancements que ce soit en mode production ou de test depuis la zone 50 | "Historique des exécutions". 51 | 52 | ### 😞 Les erreurs critiques 53 | 54 | Une erreur critique est qqch qui ne se rattrape pas et qui empêche l'automate d'aboutir. Par ex. une variable non résolue. 55 | 56 | ⚠️ Les automates qui se solde par une erreur critique n'apparaissent pas dans l'historique. Néanmoins un message électronique est 57 | envoyé à : 58 | 59 | - Adresse de messagerie `INCIDENT_NOTIFIABLE` parametrée dans **configuration.yml** 60 | - Dernier éditeur de l'automate 61 | - Créateur de l'automate 62 | 63 | Ce rapport contient autant d'information que possible pour assister à la résolution. 64 | 65 | ### Debug 66 | 67 | Pour chaque rapport d'execution existe une ligne dans le tableau "Historique des exécutions". 68 | Ce tableau s'actualise lui aussi avec une latence de plus ou moins cinq secondes. 69 | 70 | Chaque ligne offre un récapitulatif succint de l'exécution. 71 | 72 | ![hermes_logs](https://user-images.githubusercontent.com/9326700/72078007-8f8ecc80-32f8-11ea-82b5-a803cd2e706e.jpg) 73 | 74 | Pour consulter les détails de chaque exécution, il est possible de cliquer sur le bouton de la colonne "Info". 75 | 76 | Vous observerez ansi un assistant similaire à celui de la création d'une action. 77 | 78 | **Revoir comment s'est déroulé la détection** 79 | ![hermes_logs_2](https://user-images.githubusercontent.com/9326700/72078006-8f8ecc80-32f8-11ea-9238-17f700b6e4c8.jpg) 80 | **Voir comment s'est exécutée une action et en connaître la réponse** 81 | ![hermes_logs_3](https://user-images.githubusercontent.com/9326700/72078015-90bff980-32f8-11ea-9d84-e5ce4417ba4a.jpg) 82 | 83 | ⚠️ Les caches sur les images ne représentent pas la réalité, et sont ici pour protéger la confidentialité de mon environnement de production. 84 | 85 | -------------------------------------------------------------------------------- /hermes_ui/adminlte/views.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib import sqla 2 | from flask_security import current_user 3 | from flask import url_for, redirect, request, abort 4 | from flask_admin import menu 5 | from flask_security.utils import hash_password 6 | from flask_security.forms import EqualTo, unique_user_email 7 | from wtforms import fields, validators 8 | from wtforms.fields import html5 9 | 10 | 11 | class FaLink(menu.MenuLink): 12 | 13 | def __init__(self, name, url = None, endpoint = None, category = None, class_name = None, icon_type = "fa", 14 | icon_value = None, target = None): 15 | super(FaLink, self).__init__(name, url, endpoint, category, class_name, icon_type, icon_value, target) 16 | 17 | 18 | class FaModelView(sqla.ModelView): 19 | 20 | def __init__(self, model, session, name = None, category = None, endpoint = None, url = None, static_folder = None, 21 | menu_class_name = None, menu_icon_type = "fa", menu_icon_value = None): 22 | super(FaModelView, self).__init__(model, session, name, category, endpoint, url, static_folder, menu_class_name, 23 | menu_icon_type, menu_icon_value) 24 | 25 | 26 | class BaseAdminView(FaModelView): 27 | required_role = 'admin' 28 | can_export = True 29 | can_view_details = True 30 | can_create = True 31 | can_edit = True 32 | can_delete = True 33 | edit_modal = True 34 | create_modal = True 35 | details_modal = True 36 | 37 | def is_accessible(self): 38 | if not current_user.is_active or not current_user.is_authenticated: 39 | return False 40 | 41 | if current_user.has_role(self.required_role): 42 | return True 43 | 44 | return False 45 | 46 | def _handle_view(self, name, **kwargs): 47 | if not self.is_accessible(): 48 | if current_user.is_authenticated: 49 | abort(403) 50 | else: 51 | return redirect(url_for('security.login', next = request.url)) 52 | 53 | 54 | class AdminsView(BaseAdminView): 55 | required_role = 'superadmin' 56 | 57 | column_display_all_relations = True 58 | column_editable_list = ['email', 'first_name', 'last_name'] 59 | column_searchable_list = ['roles.name', 'email', 'first_name', 'last_name'] 60 | column_exclude_list = ['password'] 61 | column_details_exclude_list = ['password'] 62 | form_excluded_columns = ['password'] 63 | column_filters = ['email', 'first_name', 'last_name'] 64 | can_export = True 65 | can_view_details = True 66 | can_create = True 67 | can_edit = True 68 | can_delete = True 69 | edit_modal = True 70 | create_modal = True 71 | details_modal = True 72 | 73 | def on_model_change(self, form, model, is_created): 74 | """ 75 | :param form: 76 | :param hermes_ui.adminlte.models.User model: 77 | :param is_created: 78 | :return: 79 | """ 80 | model.password = hash_password(form.password.data) 81 | 82 | def get_create_form(self): 83 | CreateForm = super().get_create_form() 84 | 85 | CreateForm.email = html5.EmailField( 86 | 'Email', 87 | validators=[ 88 | validators.DataRequired(), 89 | validators.Email(), 90 | unique_user_email, 91 | ], 92 | ) 93 | CreateForm.password = fields.PasswordField( 94 | 'Password', 95 | validators=[ 96 | validators.DataRequired(), 97 | ], 98 | ) 99 | 100 | CreateForm.confirm_password = fields.PasswordField( 101 | 'Confirm Password', 102 | validators=[ 103 | validators.DataRequired(), 104 | EqualTo('password', message='RETYPE_PASSWORD_MISMATCH'), 105 | ], 106 | ) 107 | 108 | CreateForm.field_order = ( 109 | 'email', 'first_name', 'last_name', 110 | 'password', 'confirm_password', 'roles', 'active') 111 | 112 | return CreateForm 113 | -------------------------------------------------------------------------------- /hermes_ui/templates/admin/adminlte/forms.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/lib.html' as lib with context %} 2 | 3 | {% macro form_header(header, icon=None, is_modal=False) %} 4 | {% if is_modal %} 5 | 8 | 9 | {% else %} 10 |

{% if icon %} {% endif %}{{ header }}

11 | {% endif %} 12 | {% endmacro %} 13 | 14 | {% macro form_body(form, form_opts=None) %} 15 | {% if form.hidden_tag is defined %} 16 | {{ form.hidden_tag() }} 17 | {% else %} 18 | {% if csrf_token %} 19 | 20 | {% endif %} 21 | {% for f in form if f.widget.input_type == 'hidden' %} 22 | {{ f }} 23 | {% endfor %} 24 | {% endif %} 25 | 26 | {% if form_opts and form_opts.form_rules %} 27 | {% for r in form_opts.form_rules %} 28 | {{ r(form, form_opts=form_opts) }} 29 | {% endfor %} 30 | {% else %} 31 | {% for f in form if f.widget.input_type != 'hidden' %} 32 | {% if form_opts %} 33 | {% set kwargs = form_opts.widget_args.get(f.short_name, {}) %} 34 | {% else %} 35 | {% set kwargs = {} %} 36 | {% endif %} 37 | {{ lib.render_field(form, f, kwargs) }} 38 | {% endfor %} 39 | {% endif %} 40 | {% endmacro %} 41 | 42 | {% macro form_view(details_columns) %} 43 | 44 | {% for c, name in details_columns %} 45 | 46 | 49 | 52 | 53 | {% endfor %} 54 |
47 | {{ name }} 48 | 50 | {{ get_value(model, c) }} 51 |
55 | {% endmacro %} 56 | 57 | {% macro form_search(search_class='pull-right col-md-4', search_style='') %} 58 |
59 |
60 |
61 | {{ _gettext('Filter') }} 62 |
63 | 64 |
65 |
66 | {% endmacro %} 67 | 68 | {% macro form_footer(cancel_url, has_more=False, is_modal=False) %} 69 | 81 |
82 | {% if has_more and admin_view.can_create %} 83 | 85 | {% endif %} 86 | 87 |
88 | {% endmacro %} 89 | 90 | {% macro form_tag(form=None, action=None) %} 91 |
93 | {{ caller() }} 94 |
95 | {% endmacro %} 96 | 97 | {% macro form(header, icon, form, form_opts, cancel_url, action=None, has_more=False, is_modal=False) %} 98 | {% call form_tag(action=action) %} 99 | {% if is_modal %}{% set type='modal' %}{% else %}{% set type='box' %}{% endif %} 100 |
{{ form_header(header, icon, is_modal) }}
101 |
{{ form_body(form, form_opts) }}
102 | 103 | {% endcall %} 104 | {% endmacro %} -------------------------------------------------------------------------------- /hermes_ui/templates/admin/layout.html: -------------------------------------------------------------------------------- 1 | {% macro menu_icon(item) -%} 2 | {% set icon_type = item.get_icon_type() %} 3 | {%- if icon_type %} 4 | {% set icon_value = item.get_icon_value() %} 5 | {% if icon_type == 'glyph' %} 6 | 7 | {% elif icon_type == 'fa' %} 8 | 9 | {% elif icon_type == 'image' %} 10 | menu image 11 | {% elif icon_type == 'image-url' %} 12 | menu image 13 | {% endif %} 14 | {% else %} 15 | {%- if item.name == "Home" %} 16 | 17 | {% else %} 18 | 19 | {% endif %} 20 | {% endif %} 21 | {%- endmacro %} 22 | 23 | {% macro menu(menu_root=None) %} 24 | {% if menu_root is none %}{% set menu_root = admin_view.admin.menu() %}{% endif %} 25 | {%- for item in menu_root %} 26 | {%- if item.is_category() -%} 27 | {% set children = item.get_children() %} 28 | {%- if children %} 29 | {% set class_name = item.get_class_name() %} 30 | {%- if item.is_active(admin_view) %} 31 |
  • 32 | {% else -%} 33 |
  • 34 | {%- endif %} 35 | 36 | {{ menu_icon(item) }} 37 | {{ item.name }} 38 | 39 | 40 |
      41 | {%- for child in children -%} 42 | {% set class_name = child.get_class_name() %} 43 | {%- if child.is_active(admin_view) %} 44 |
    • 45 | {% else %} 46 | 47 | {%- endif %} 48 | 49 | {{ menu_icon(child) }}{{ child.name }} 50 |
    • 51 | {%- endfor %} 52 |
    53 |
  • 54 | {% endif %} 55 | {%- else %} 56 | {%- if item.is_accessible() and item.is_visible() -%} 57 | {% set class_name = item.get_class_name() %} 58 | {%- if item.is_active(admin_view) %} 59 |
  • 60 | {%- else %} 61 | 62 | {%- endif %} 63 | 64 | {{ menu_icon(item) }}{{ item.name }} 65 |
  • 66 | {%- endif -%} 67 | {% endif -%} 68 | {% endfor %} 69 | {% endmacro %} 70 | 71 | {% macro menu_links(links=None) %} 72 | {% if links is none %}{% set links = admin_view.admin.menu_links() %}{% endif %} 73 | {% for item in links %} 74 | {% if item.is_accessible() and item.is_visible() %} 75 |
  • 76 | {{ menu_icon(item) }}{{ item.name }} 77 |
  • 78 | {% endif %} 79 | {% endfor %} 80 | {% endmacro %} 81 | 82 | {% macro messages() %} 83 | {% with messages = get_flashed_messages(with_categories=True) %} 84 | {% if messages %} 85 | {% for category, m in messages %} 86 | {% if category %} 87 | {# alert-error changed to alert-danger in bootstrap 3, mapping is for backwards compatibility #} 88 | {% set mapping = {'message': 'info', 'error': 'danger'} %} 89 |
    90 | {% else %} 91 |
    92 | {% endif %} 93 | 94 | {{ m }} 95 |
    96 | {% endfor %} 97 | {% endif %} 98 | {% endwith %} 99 | {% endmacro %} 100 | -------------------------------------------------------------------------------- /hermes_ui/marshmallow/legacy.py: -------------------------------------------------------------------------------- 1 | from flask_marshmallow import Marshmallow 2 | import flask_marshmallow.fields 3 | from marshmallow_oneofschema import OneOfSchema 4 | from hermes_ui.models import * 5 | 6 | 7 | ma = Marshmallow() 8 | 9 | 10 | class ActionNoeudLegacySchema(ma.SQLAlchemyAutoSchema): 11 | class Meta: 12 | model = ActionNoeud 13 | exclude = ('id', 'createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'mapped_class_child', 'friendly_name') 14 | load_instance = True 15 | 16 | action_reussite = flask_marshmallow.fields.fields.Nested('ActionNoeudLegacyPolySchema', allow_none=True, required=False) 17 | action_echec = flask_marshmallow.fields.fields.Nested('ActionNoeudLegacyPolySchema', allow_none=True, required=False) 18 | 19 | variable = flask_marshmallow.fields.fields.String(attribute='friendly_name', allow_none=True, required=False) 20 | 21 | 22 | for my_class in ActionNoeud.__subclasses__(): 23 | t_ = str(my_class).split("'")[-2].split('.')[-1] 24 | if 'ExecutionAutomate' not in t_: 25 | exec( 26 | """class {class_name}LegacySchema(ActionNoeudLegacySchema): 27 | class Meta: 28 | model = {class_name} 29 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'mapped_class_child', 'id', 'friendly_name') 30 | load_instance = True""".format(class_name=str(my_class).split("'")[-2].split('.')[-1]) 31 | ) 32 | else: 33 | exec( 34 | """class {class_name}LegacySchema(ActionNoeudLegacySchema): 35 | class Meta: 36 | model = {class_name} 37 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'mapped_class_child', 'id', 'friendly_name', 'automate') 38 | load_instance = True 39 | automate = flask_marshmallow.fields.fields.Nested('AutomateLegacySchema', many=False)""".format( 40 | class_name=str(my_class).split("'")[-2].split('.')[-1]) 41 | ) 42 | 43 | 44 | class ActionNoeudLegacyPolySchema(OneOfSchema): 45 | 46 | type_field = "type" 47 | type_schemas = dict([(str(cl_type).split("'")[-2].split('.')[-1].replace('LegacySchema', ''), cl_type) for cl_type in ActionNoeudLegacySchema.__subclasses__()]) 48 | 49 | 50 | class RechercheInteretLegacySchema(ma.SQLAlchemyAutoSchema): 51 | class Meta: 52 | model = RechercheInteret 53 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'friendly_name') 54 | load_instance = True 55 | 56 | variable = flask_marshmallow.fields.fields.String(attribute='friendly_name', allow_none=True, required=False) 57 | 58 | 59 | for my_class in RechercheInteret.__subclasses__(): 60 | t_ = str(my_class).split("'")[-2].split('.')[-1] 61 | if 'OperationLogique' not in t_: 62 | exec( 63 | """class {class_name}LegacySchema(RechercheInteretLegacySchema): 64 | class Meta: 65 | model = {class_name} 66 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'id', 'detecteurs', 'mapped_class_child', 'friendly_name') 67 | load_instance = True""".format(class_name=str(my_class).split("'")[-2].split('.')[-1]) 68 | ) 69 | else: 70 | exec( 71 | """class {class_name}LegacySchema(RechercheInteretLegacySchema): 72 | class Meta: 73 | model = {class_name} 74 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'id', 'detecteurs', 'mapped_class_child', 'sous_regles', 'friendly_name') 75 | load_instance = True 76 | sous_criteres = flask_marshmallow.fields.fields.Nested('RechercheInteretLegacyPolySchema', many=True, attribute="sous_regles")""".format( 77 | class_name=str(my_class).split("'")[-2].split('.')[-1]) 78 | ) 79 | 80 | 81 | class RechercheInteretLegacyPolySchema(OneOfSchema): 82 | 83 | type_field = "type" 84 | type_schemas = dict([(str(cl_type).split("'")[-2].split('.')[-1].replace('LegacySchema', ''), cl_type) for cl_type in RechercheInteretLegacySchema.__subclasses__()]) 85 | 86 | 87 | class DetecteurLegacySchema(ma.SQLAlchemyAutoSchema): 88 | 89 | class Meta: 90 | model = Detecteur 91 | exclude = ('id', 'createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'automates', 'regles') 92 | load_instance = True 93 | 94 | criteres = flask_marshmallow.fields.fields.Nested( 95 | RechercheInteretLegacyPolySchema, many=True, attribute="regles" 96 | ) 97 | 98 | 99 | class AutomateLegacySchema(ma.SQLAlchemyAutoSchema): 100 | class Meta: 101 | model = Automate 102 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification', 'actions', 'id', 'detecteur', 'priorite') 103 | load_instance = True 104 | 105 | regle = flask_marshmallow.fields.fields.Nested(DetecteurLegacySchema, attribute='detecteur') 106 | action_racine = flask_marshmallow.fields.fields.Nested(ActionNoeudLegacyPolySchema, allow_none=True, required=False) 107 | 108 | rang = flask_marshmallow.fields.fields.Integer(attribute='priorite') 109 | -------------------------------------------------------------------------------- /hermes_ui/marshmallow/front.py: -------------------------------------------------------------------------------- 1 | from flask_marshmallow import Marshmallow 2 | import flask_marshmallow.fields 3 | from marshmallow_oneofschema import OneOfSchema 4 | from hermes_ui.models import * 5 | 6 | 7 | ma = Marshmallow() 8 | 9 | 10 | class UserSchema(ma.SQLAlchemyAutoSchema): 11 | class Meta: 12 | model = User 13 | exclude = ('password',) 14 | load_instance = True 15 | 16 | 17 | class ActionNoeudSchema(ma.SQLAlchemyAutoSchema): 18 | class Meta: 19 | model = ActionNoeud 20 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 21 | load_instance = True 22 | 23 | action_reussite = flask_marshmallow.fields.fields.Nested('ActionNoeudPolySchema') 24 | action_echec = flask_marshmallow.fields.fields.Nested('ActionNoeudPolySchema') 25 | 26 | 27 | for my_class in ActionNoeud.__subclasses__(): 28 | exec( 29 | """class {class_name}Schema(ActionNoeudSchema): 30 | class Meta: 31 | model = {class_name} 32 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 33 | load_instance = True""".format(class_name=str(my_class).split("'")[-2].split('.')[-1]) 34 | ) 35 | 36 | 37 | class ActionNoeudPolySchema(OneOfSchema): 38 | 39 | type_field = "type" 40 | type_schemas = dict([(str(cl_type).split("'")[-2].split('.')[-1].replace('Schema', ''), cl_type) for cl_type in ActionNoeudSchema.__subclasses__()]) 41 | 42 | 43 | class RechercheInteretSchema(ma.SQLAlchemyAutoSchema): 44 | class Meta: 45 | model = RechercheInteret 46 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 47 | load_instance = True 48 | 49 | createur = flask_marshmallow.fields.fields.Nested(UserSchema) 50 | responsable_derniere_modification = flask_marshmallow.fields.fields.Nested(UserSchema) 51 | 52 | 53 | for my_class in RechercheInteret.__subclasses__(): 54 | exec( 55 | """class {class_name}Schema(RechercheInteretSchema): 56 | class Meta: 57 | model = {class_name} 58 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 59 | load_instance = True""".format(class_name=str(my_class).split("'")[-2].split('.')[-1]) 60 | ) 61 | 62 | 63 | class RechercheInteretPolySchema(OneOfSchema): 64 | 65 | type_field = "type" 66 | type_schemas = dict([(str(cl_type).split("'")[-2].split('.')[-1].replace('Schema', ''), cl_type) for cl_type in RechercheInteretSchema.__subclasses__()]) 67 | 68 | 69 | class DetecteurSchema(ma.SQLAlchemyAutoSchema): 70 | 71 | class Meta: 72 | model = Detecteur 73 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 74 | load_instance = True 75 | 76 | createur = flask_marshmallow.fields.fields.Nested(UserSchema) 77 | responsable_derniere_modification = flask_marshmallow.fields.fields.Nested(UserSchema) 78 | 79 | regles = flask_marshmallow.fields.fields.Nested( 80 | RechercheInteretPolySchema, many=True 81 | ) 82 | 83 | 84 | class AutomateSchema(ma.SQLAlchemyAutoSchema): 85 | class Meta: 86 | model = Automate 87 | exclude = ('createur', 'responsable_derniere_modification', 'date_creation', 'date_modification') 88 | load_instance = True 89 | 90 | detecteur = flask_marshmallow.fields.fields.Nested(DetecteurSchema) 91 | action_racine = flask_marshmallow.fields.fields.Nested(ActionNoeudPolySchema) 92 | actions = flask_marshmallow.fields.fields.List(flask_marshmallow.fields.fields.Nested(ActionNoeudPolySchema)) 93 | 94 | createur = flask_marshmallow.fields.fields.Nested(UserSchema) 95 | responsable_derniere_modification = flask_marshmallow.fields.fields.Nested(UserSchema) 96 | 97 | 98 | class ActionNoeudExecutionSchema(ma.SQLAlchemyAutoSchema): 99 | class Meta: 100 | model = ActionNoeudExecution 101 | load_instance = True 102 | 103 | action_noeud = flask_marshmallow.fields.fields.Nested(ActionNoeudPolySchema) 104 | 105 | 106 | class RechercheInteretExecutionSchema(ma.SQLAlchemyAutoSchema): 107 | class Meta: 108 | model = RechercheInteretExecution 109 | load_instance = True 110 | 111 | recherche_interet = flask_marshmallow.fields.fields.Nested(RechercheInteretSchema) 112 | 113 | 114 | class AutomateExecutionSchema(ma.SQLAlchemyAutoSchema): 115 | 116 | class Meta: 117 | model = AutomateExecution 118 | load_instance = True 119 | 120 | automate = flask_marshmallow.fields.fields.Nested(AutomateSchema) 121 | detecteur = flask_marshmallow.fields.fields.Nested(DetecteurSchema) 122 | 123 | actions_noeuds_executions = flask_marshmallow.fields.fields.List( 124 | flask_marshmallow.fields.fields.Nested(ActionNoeudExecutionSchema) 125 | ) 126 | 127 | recherches_interets_executions = flask_marshmallow.fields.fields.List( 128 | flask_marshmallow.fields.fields.Nested(RechercheInteretExecutionSchema) 129 | ) 130 | 131 | 132 | class AutomateExecutionDataTableSchema(ma.Schema): 133 | 134 | class Meta: 135 | model = AutomateExecutionDataTable 136 | load_instance = True 137 | 138 | data = flask_marshmallow.fields.fields.List( 139 | flask_marshmallow.fields.fields.Nested(AutomateExecutionSchema) 140 | ) 141 | -------------------------------------------------------------------------------- /hermes_ui/assets/scripts/Compoments/jquery.livequery.js: -------------------------------------------------------------------------------- 1 | /*! jquery.livequery - v1.3.6 - 2013-08-26 2 | * Copyright (c) 3 | * (c) 2010, Brandon Aaron (http://brandonaaron.net) 4 | * (c) 2012 - 2013, Alexander Zaytsev (http://hazzik.ru/en) 5 | * Dual licensed under the MIT (MIT_LICENSE.txt) 6 | * and GPL Version 2 (GPL_LICENSE.txt) licenses. 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | define(['jquery'], factory); 11 | } else if (typeof exports === 'object') { 12 | factory(require('jquery')); 13 | } else { 14 | factory(jQuery); 15 | } 16 | }(function ($, undefined) { 17 | 18 | function _match(me, query, fn, fn2) { 19 | return me.selector == query.selector && 20 | me.context == query.context && 21 | (!fn || fn.$lqguid == query.fn.$lqguid) && 22 | (!fn2 || fn2.$lqguid == query.fn2.$lqguid); 23 | } 24 | 25 | $.extend($.fn, { 26 | livequery: function(fn, fn2) { 27 | var me = this, q; 28 | 29 | // See if Live Query already exists 30 | $.each( $jQlq.queries, function(i, query) { 31 | if ( _match(me, query, fn, fn2) ) 32 | // Found the query, exit the each loop 33 | return (q = query) && false; 34 | }); 35 | 36 | // Create new Live Query if it wasn't found 37 | q = q || new $jQlq(me.selector, me.context, fn, fn2); 38 | 39 | // Make sure it is running 40 | q.stopped = false; 41 | 42 | // Run it immediately for the first time 43 | q.run(); 44 | 45 | // Contnue the chain 46 | return me; 47 | }, 48 | 49 | expire: function(fn, fn2) { 50 | var me = this; 51 | 52 | // Find the Live Query based on arguments and stop it 53 | $.each( $jQlq.queries, function(i, query) { 54 | if ( _match(me, query, fn, fn2) && !me.stopped) 55 | $jQlq.stop(query.id); 56 | }); 57 | 58 | // Continue the chain 59 | return me; 60 | } 61 | }); 62 | 63 | var $jQlq = $.livequery = function(selector, context, fn, fn2) { 64 | var me = this; 65 | 66 | me.selector = selector; 67 | me.context = context; 68 | me.fn = fn; 69 | me.fn2 = fn2; 70 | me.elements = $([]); 71 | me.stopped = false; 72 | 73 | // The id is the index of the Live Query in $.livequiery.queries 74 | me.id = $jQlq.queries.push(me)-1; 75 | 76 | // Mark the functions for matching later on 77 | fn.$lqguid = fn.$lqguid || $jQlq.guid++; 78 | if (fn2) fn2.$lqguid = fn2.$lqguid || $jQlq.guid++; 79 | 80 | // Return the Live Query 81 | return me; 82 | }; 83 | 84 | $jQlq.prototype = { 85 | stop: function() { 86 | var me = this; 87 | // Short-circuit if stopped 88 | if ( me.stopped ) return; 89 | 90 | if (me.fn2) 91 | // Call the second function for all matched elements 92 | me.elements.each(me.fn2); 93 | 94 | // Clear out matched elements 95 | me.elements = $([]); 96 | 97 | // Stop the Live Query from running until restarted 98 | me.stopped = true; 99 | }, 100 | 101 | run: function() { 102 | var me = this; 103 | // Short-circuit if stopped 104 | if ( me.stopped ) return; 105 | 106 | var oEls = me.elements, 107 | els = $(me.selector, me.context), 108 | newEls = els.not(oEls), 109 | delEls = oEls.not(els); 110 | 111 | // Set elements to the latest set of matched elements 112 | me.elements = els; 113 | 114 | // Call the first function for newly matched elements 115 | newEls.each(me.fn); 116 | 117 | // Call the second function for elements no longer matched 118 | if ( me.fn2 ) 119 | delEls.each(me.fn2); 120 | } 121 | }; 122 | 123 | $.extend($jQlq, { 124 | guid: 0, 125 | queries: [], 126 | queue: [], 127 | running: false, 128 | timeout: null, 129 | registered: [], 130 | 131 | checkQueue: function() { 132 | if ( $jQlq.running && $jQlq.queue.length ) { 133 | var length = $jQlq.queue.length; 134 | // Run each Live Query currently in the queue 135 | while ( length-- ) 136 | $jQlq.queries[ $jQlq.queue.shift() ].run(); 137 | } 138 | }, 139 | 140 | pause: function() { 141 | // Don't run anymore Live Queries until restarted 142 | $jQlq.running = false; 143 | }, 144 | 145 | play: function() { 146 | // Restart Live Queries 147 | $jQlq.running = true; 148 | // Request a run of the Live Queries 149 | $jQlq.run(); 150 | }, 151 | 152 | registerPlugin: function() { 153 | $.each( arguments, function(i,n) { 154 | // Short-circuit if the method doesn't exist 155 | if (!$.fn[n] || $.inArray(n, $jQlq.registered) > 0) return; 156 | 157 | // Save a reference to the original method 158 | var old = $.fn[n]; 159 | 160 | // Create a new method 161 | $.fn[n] = function() { 162 | // Call the original method 163 | var r = old.apply(this, arguments); 164 | 165 | // Request a run of the Live Queries 166 | $jQlq.run(); 167 | 168 | // Return the original methods result 169 | return r; 170 | }; 171 | 172 | $jQlq.registered.push(n); 173 | }); 174 | }, 175 | 176 | run: function(id) { 177 | if (id !== undefined) { 178 | // Put the particular Live Query in the queue if it doesn't already exist 179 | if ( $.inArray(id, $jQlq.queue) < 0 ) 180 | $jQlq.queue.push( id ); 181 | } 182 | else 183 | // Put each Live Query in the queue if it doesn't already exist 184 | $.each( $jQlq.queries, function(id) { 185 | if ( $.inArray(id, $jQlq.queue) < 0 ) 186 | $jQlq.queue.push( id ); 187 | }); 188 | 189 | // Clear timeout if it already exists 190 | if ($jQlq.timeout) clearTimeout($jQlq.timeout); 191 | // Create a timeout to check the queue and actually run the Live Queries 192 | $jQlq.timeout = setTimeout($jQlq.checkQueue, 20); 193 | }, 194 | 195 | stop: function(id) { 196 | if (id !== undefined) 197 | // Stop are particular Live Query 198 | $jQlq.queries[ id ].stop(); 199 | else 200 | // Stop all Live Queries 201 | $.each( $jQlq.queries, $jQlq.prototype.stop); 202 | } 203 | }); 204 | 205 | // Register core DOM manipulation methods 206 | $jQlq.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html', 'prop', 'removeProp'); 207 | 208 | // Run Live Queries when the Document is ready 209 | $(function() { $jQlq.play(); }); 210 | 211 | })); 212 | -------------------------------------------------------------------------------- /hermes_ui/assets/scripts/app.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | global.$ = $; /* Quick fix pour jQuery et plugin ext. */ 4 | global.jQuery = $; 5 | 6 | require('icheck/icheck'); 7 | require('fastclick'); 8 | require('bootstrap'); 9 | require('bootstrap/js/tooltip'); 10 | require('jquery-slimscroll'); 11 | require('select2'); 12 | 13 | const hljs = require('highlight.js'); 14 | let Dropzone = require('dropzone'); 15 | const Swal = require('sweetalert2'); 16 | global.moment = require('moment'); 17 | 18 | require('./Compoments/jquery.a-tools'); 19 | require('./Compoments/jquery.asuggest'); 20 | 21 | //require('awesomplete'); 22 | require('admin-lte/dist/js/app'); 23 | 24 | require('bootstrap/dist/css/bootstrap.css'); 25 | require('admin-lte/dist/css/AdminLTE.css'); 26 | require('admin-lte/dist/css/skins/skin-green-light.css'); 27 | require('icheck/skins/square/purple.png'); 28 | require('font-awesome/css/font-awesome.css'); 29 | require('select2/dist/css/select2.css'); 30 | require('awesomplete/awesomplete.css'); 31 | require('highlight.js/styles/atelier-lakeside-dark.css'); 32 | require('dropzone/dist/dropzone.css'); 33 | 34 | require('../styles/hermes-surcharge.css'); 35 | 36 | const AppInterfaceInteroperabilite = require('./Compoments/hermes_ui'); 37 | 38 | Dropzone.autoDiscover = false; 39 | 40 | $.fn.sidebar = function(options) { 41 | 42 | var self = this; 43 | if (self.length > 1) { 44 | return self.each(function () { 45 | $(this).sidebar(options); 46 | }); 47 | } 48 | 49 | // Width, height 50 | var width = self.outerWidth(); 51 | var height = self.outerHeight(); 52 | 53 | // Defaults 54 | var settings = $.extend({ 55 | 56 | // Animation speed 57 | speed: 200, 58 | 59 | // Side: left|right|top|bottom 60 | side: "left", 61 | 62 | // Is closed 63 | isClosed: false, 64 | 65 | // Should I close the sidebar? 66 | close: true 67 | 68 | }, options); 69 | 70 | /*! 71 | * Opens the sidebar 72 | * $([jQuery selector]).trigger("sidebar:open"); 73 | * */ 74 | self.on("sidebar:open", function(ev, data) { 75 | var properties = {}; 76 | properties[settings.side] = 0; 77 | settings.isClosed = null; 78 | self.stop().animate(properties, $.extend({}, settings, data).speed, function() { 79 | settings.isClosed = false; 80 | self.trigger("sidebar:opened"); 81 | }); 82 | }); 83 | 84 | 85 | /*! 86 | * Closes the sidebar 87 | * $("[jQuery selector]).trigger("sidebar:close"); 88 | * */ 89 | self.on("sidebar:close", function(ev, data) { 90 | var properties = {}; 91 | if (settings.side === "left" || settings.side === "right") { 92 | properties[settings.side] = -self.outerWidth(); 93 | } else { 94 | properties[settings.side] = -self.outerHeight(); 95 | } 96 | settings.isClosed = null; 97 | self.stop().animate(properties, $.extend({}, settings, data).speed, function() { 98 | settings.isClosed = true; 99 | self.trigger("sidebar:closed"); 100 | }); 101 | }); 102 | 103 | /*! 104 | * Toggles the sidebar 105 | * $("[jQuery selector]).trigger("sidebar:toggle"); 106 | * */ 107 | self.on("sidebar:toggle", function(ev, data) { 108 | if (settings.isClosed) { 109 | self.trigger("sidebar:open", [data]); 110 | } else { 111 | self.trigger("sidebar:close", [data]); 112 | } 113 | }); 114 | 115 | function closeWithNoAnimation() { 116 | self.trigger("sidebar:close", [{ 117 | speed: 0 118 | }]); 119 | } 120 | 121 | // Close the sidebar 122 | if (!settings.isClosed && settings.close) { 123 | closeWithNoAnimation(); 124 | } 125 | 126 | $(window).on("resize", function () { 127 | if (!settings.isClosed) { return; } 128 | closeWithNoAnimation(); 129 | }); 130 | 131 | self.data("sidebar", settings); 132 | 133 | return self; 134 | }; 135 | 136 | $(function () { 137 | 138 | // Crappy way of not running this outside of app 139 | if ($('.login-box').length > 0) {return; } 140 | 141 | $(".sidebarh.right").sidebar({side: "right"}); 142 | 143 | $('#btn-analyse-manuelle-detecteur').click(AppInterfaceInteroperabilite.assistant_simulation_detecteur); 144 | $('#btn-analyse-manuelle-raw').click(AppInterfaceInteroperabilite.assistant_simulation_extraction_interet); 145 | 146 | AppInterfaceInteroperabilite.recuperation_saisie_assistee().then(AppInterfaceInteroperabilite.assistant_saisie_assistee); 147 | 148 | let $chatbox = $('.chatbox'), 149 | $chatboxTitle = $('.chatbox__title'), 150 | $chatboxTitleClose = $('.chatbox__title__close'); 151 | 152 | $chatboxTitle.on('click', function() { 153 | $chatbox.toggleClass('chatbox--tray'); 154 | }); 155 | 156 | $chatboxTitleClose.on('click', function(e) { 157 | e.stopPropagation(); 158 | $chatbox.addClass('chatbox--closed'); 159 | }); 160 | 161 | $chatbox.on('transitionend', function() { 162 | if ($chatbox.hasClass('chatbox--closed')) $chatbox.remove(); 163 | }); 164 | 165 | var myDropzone = new Dropzone( 166 | "#detecteur-fichier-dropzone-floatbox", 167 | { 168 | url: "/admin/rest/simulation/detecteur/fichier", 169 | uploadMultiple: false, 170 | dictDefaultMessage: 'Déposez un fichier MSG ou EML dans cette zone', 171 | 172 | complete: function(a) { 173 | let rs_template = a.xhr.response; 174 | Swal.fire( 175 | { 176 | title: 'Analyse de message', 177 | type: 'info', 178 | html: rs_template, 179 | showCloseButton: true, 180 | showCancelButton: false 181 | } 182 | ); 183 | hljs.initHighlightingOnLoad(); 184 | this.removeAllFiles(true); 185 | } 186 | }, 187 | 188 | ); 189 | 190 | Dropzone.options.detecteurFichierDropzoneFloatbox = { 191 | paramName: "file", // The name that will be used to transfer the file 192 | maxFilesize: 5, // MB 193 | }; 194 | 195 | }); -------------------------------------------------------------------------------- /test/test_session.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hermes.session import Session 3 | 4 | 5 | class TestSession(unittest.TestCase): 6 | 7 | SESSION_LOCALE = None # type: Session 8 | 9 | @classmethod 10 | def setUpClass(cls): 11 | 12 | TestSession.SESSION_LOCALE = Session() 13 | 14 | Session.UNIVERSELLE.sauver('licorne', 'mdp_ultra_secret') 15 | Session.UNIVERSELLE.sauver('montagne', 'utilisateur_ldap') 16 | 17 | Session.UNIVERSELLE.sauver('mm', 'licorne') 18 | 19 | Session.UNIVERSELLE.sauver('mmm', 'hello.0') 20 | 21 | Session.UNIVERSELLE.sauver('http_requete_0', {'status': 200, 'json_data': {}, 'hello': [0, 1, 2, 'b']}) 22 | 23 | TestSession.SESSION_LOCALE.sauver('hello_world', 'you are most welcome') 24 | 25 | Session.UNIVERSELLE.sauver('identifiant_itop', 'ITOP-C-000941') 26 | 27 | Session.UNIVERSELLE.sauver('test_boolean', True) 28 | Session.UNIVERSELLE.sauver('test_boolean_2', False) 29 | 30 | Session.UNIVERSELLE.sauver('anniversaire', '1994-02-06') 31 | 32 | TestSession.SESSION_LOCALE.sauver( 33 | 'ma_liste', 34 | [ 35 | { 36 | 'a': 'n', 37 | 'b': 'p' 38 | }, 39 | { 40 | 'a': 'yu', 41 | 'b': 'po' 42 | }, 43 | ] 44 | ) 45 | 46 | TestSession.SESSION_LOCALE.sauver( 47 | 'requete_itop_0', 48 | { 49 | 'objects': { 50 | 'NormalChange::1515': { 51 | 'fields': 52 | { 53 | 'caller_mail': 'toto@zero.fr' 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | 60 | TestSession.SESSION_LOCALE.sauver('Class', 'NormalChange') 61 | TestSession.SESSION_LOCALE.sauver('RefITOP', '1515') 62 | 63 | def test_remplacement_obj_vers_str(self): 64 | 65 | self.assertEqual( 66 | TestSession.SESSION_LOCALE.retranscrire('{{ ma_liste }}'), 67 | '[{"a": "n", "b": "p"}, {"a": "yu", "b": "po"}]' 68 | ) 69 | 70 | def test_boolean(self): 71 | 72 | self.assertEqual( 73 | TestSession.SESSION_LOCALE.retranscrire('{{ test_boolean }}'), 74 | 'True' 75 | ) 76 | 77 | self.assertEqual( 78 | TestSession.SESSION_LOCALE.retranscrire('{{ test_boolean_2 }}'), 79 | 'False' 80 | ) 81 | 82 | def test_remplacement_nested(self): 83 | 84 | self.assertEqual( 85 | TestSession.SESSION_LOCALE.retranscrire("{{ mm }}"), 86 | "licorne" 87 | ) 88 | 89 | self.assertEqual( 90 | TestSession.SESSION_LOCALE.retranscrire("{{ {{ mm }} }}"), 91 | 'mdp_ultra_secret' 92 | ) 93 | 94 | self.assertEqual( 95 | TestSession.SESSION_LOCALE.retranscrire("{{ http_requete_0.{{ mmm }} }}"), 96 | '0' 97 | ) 98 | 99 | self.assertEqual( 100 | TestSession.SESSION_LOCALE.retranscrire( 101 | '{{ requete_itop_0.objects.{{ Class }}::{{ RefITOP }}.fields.caller_mail }}'), 102 | 'toto@zero.fr' 103 | ) 104 | 105 | def test_remplacement_simple(self): 106 | 107 | self.assertEqual( 108 | "J'aime les mdp_ultra_secret ! Et aussi les utilisateur_ldap !!", 109 | TestSession.SESSION_LOCALE.retranscrire( 110 | "J'aime les {{ licorne }} ! Et aussi les {{montagne}} !!" 111 | ) 112 | ) 113 | 114 | def test_remplacement_sous_niveau(self): 115 | 116 | self.assertEqual( 117 | "Je souhaite arriver au status 200 sachant la lettre b", 118 | TestSession.SESSION_LOCALE.retranscrire("Je souhaite arriver au status {{ http_requete_0.status }} sachant la lettre {{ http_requete_0.hello.3 }}") 119 | ) 120 | 121 | self.assertEqual( 122 | "Je souhaite arriver au status 200", 123 | TestSession.SESSION_LOCALE.retranscrire("Je souhaite arriver au status {{ http_requete_0.status }}") 124 | ) 125 | 126 | def test_remplacement_dict(self): 127 | 128 | self.assertEqual( 129 | { 130 | 'auth_user': 'mdp_ultra_secret', 131 | 'auth_pass': 'utilisateur_ldap', 132 | 'json_data': '0' 133 | }, 134 | TestSession.SESSION_LOCALE.retranscrire( 135 | { 136 | 'auth_user': '{{licorne}}', 137 | 'auth_pass': '{{montagne}}', 138 | 'json_data': '{{http_requete_0.hello.0}}' 139 | } 140 | ) 141 | ) 142 | 143 | def test_filtre(self): 144 | 145 | self.assertEqual( 146 | TestSession.SESSION_LOCALE.retranscrire( 147 | '{{identifiant_itop|int}}' 148 | ), 149 | '941' 150 | ) 151 | 152 | self.assertEqual( 153 | TestSession.SESSION_LOCALE.retranscrire( 154 | '{{identifiant_itop|int|remplissageSixZero}}' 155 | ), 156 | '000941' 157 | ) 158 | 159 | self.assertEqual( 160 | TestSession.SESSION_LOCALE.retranscrire( 161 | '{{anniversaire|dateAjouterUnJour}}' 162 | ), 163 | '1994-02-07' 164 | ) 165 | 166 | self.assertEqual( 167 | TestSession.SESSION_LOCALE.retranscrire( 168 | '{{anniversaire|dateRetirerUnJour}}' 169 | ), 170 | '1994-02-05' 171 | ) 172 | 173 | self.assertEqual( 174 | TestSession.SESSION_LOCALE.retranscrire( 175 | '{{anniversaire|dateAjouterUnJour|dateFormatFrance}}' 176 | ), 177 | '07/02/1994' 178 | ) 179 | 180 | def test_key_error(self): 181 | 182 | with self.assertRaises(KeyError): 183 | TestSession.SESSION_LOCALE.retranscrire('{{ cle_inexistante }}') 184 | 185 | with self.assertRaises(KeyError): 186 | Session.UNIVERSELLE.retranscrire('{{ cle_n_existe_toujours_pas }}') 187 | 188 | with self.assertRaises(KeyError): 189 | TestSession.SESSION_LOCALE.retranscrire('{{ Class.hello.a }}') 190 | 191 | def test_key_dict_int(self): 192 | 193 | self.assertEqual( 194 | TestSession.SESSION_LOCALE.retranscrire('{{ http_requete_0.0 }}'), 195 | TestSession.SESSION_LOCALE.retranscrire('{{ http_requete_0.status }}') 196 | ) 197 | 198 | 199 | if __name__ == '__main__': 200 | unittest.main() 201 | -------------------------------------------------------------------------------- /hermes_ui/assets/scripts/app_help.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | let create_helper_callout = (titre, corps) => { 4 | return ` 5 |
    6 |

    ${titre}

    7 |

    8 | ${corps} 9 |

    10 |
    11 | `; 12 | }; 13 | 14 | $(function () { 15 | 16 | // Crappy way of not running this outside of app 17 | if ($('.login-box').length > 0) {return; } 18 | 19 | let current_admin_page = $('.content-header h1').html(), 20 | header_section = $('section.content-header'), 21 | content_section = $('section.content'); 22 | 23 | if (current_admin_page.startsWith('Clé')) 24 | { 25 | content_section.prepend( 26 | create_helper_callout( 27 | "Comprendre ce qu'est une Clé", 28 | "
      " + 29 | "
    • Une Clé est découverte automatiquement par le moteur
    • " + 30 | "
    • Vous pouvez les découvrir avec l'outil 'Analyse de message' en bas à droite de l'écran
    • " + 31 | "
    • La valeur associée a cette clé peut être stockée pour être exploitée
    • " + 32 | "
    " 33 | ) 34 | ); 35 | header_section.append("Permet de vérifier l'existe d'une Clé dans l'analyse préliminaire de votre source"); 36 | } 37 | else if(current_admin_page.startsWith('Identifiant')) 38 | { 39 | header_section.append("Permet de vérifier la présence d'un identifiant au format numérique dans votre source"); 40 | } 41 | else if(current_admin_page.startsWith('Recherche d\'expression')) 42 | { 43 | header_section.append("Permet d'extraire un ou des mot(s) sachant au moins soit la partie immédiatement à droite et/ou immédiatement à gauche"); 44 | } 45 | else if(current_admin_page.startsWith('Détecteur')) 46 | { 47 | header_section.append("Décrire à l'aide d'une collection de règles un type de source, ou comment identifier une source en tant que"); 48 | 49 | content_section.prepend( 50 | create_helper_callout( 51 | "Comprendre ce qu'est un Détecteur", 52 | "
      " + 53 | "
    • Un Détecteur est une définition, elle permet d'identifier une source
    • " + 54 | "
    • Cette définition ce précise avec un ensemble de règles, celles-ci sont éditables/créables depuis le sous menu Règles de détection
    • " + 55 | "
    • Finalement, votre détecteur sera utile pour déclencher les actions d'un Automate
    • " + 56 | "
    • De plus un détecteur comprendra de règle, de plus le taux de faux positif sera faible
    • " + 57 | "
    " 58 | ) 59 | ); 60 | } 61 | else if(current_admin_page.startsWith('Expression exacte')) 62 | { 63 | header_section.append("Retrouver une expression, une phrase, une suite de mots, dans votre source"); 64 | 65 | content_section.prepend( 66 | create_helper_callout( 67 | "Remarques sur la recherche d'expression exacte", 68 | "
      " + 69 | "
    • La recherche ne sera pas sensible à la case
    • " + 70 | "
    • Les accents ne sont pas un critère d'égalité, eg. é = e, à = a
    • " + 71 | "
    " 72 | ) 73 | ); 74 | } 75 | else if(current_admin_page.startsWith('Mes variables globales')) 76 | { 77 | header_section.append("Stocke des variables globales disponibles à tout les automates"); 78 | 79 | content_section.prepend( 80 | create_helper_callout( 81 | "Remarques sur le stockage de variable globale", 82 | "
      " + 83 | "
    • Il est possible de stocker des informations structurées au format JSON ou YAML
    • " + 84 | "
    • Les variables créées sont disponibles depuis le volet de droite accessible par le bouton en bas à droite
    • " + 85 | "
    • Pour un stockage simple sans JSON ou YAML, choissisez AUTRE dans le choix format
    • " + 86 | "
    • Pour mémo, les variables sont accessible en les écrivants de la forme suivante: {{ ma_variable }}
    • " + 87 | "
    " 88 | ) 89 | ); 90 | } 91 | else if(current_admin_page.startsWith('Description des Automates')) 92 | { 93 | header_section.append("Précise le cadre d'un automate"); 94 | 95 | content_section.prepend( 96 | create_helper_callout( 97 | "Comprendre ce qu'est une Description d'Automate", 98 | "
      " + 99 | "
    • Vous pouvez ici associer un Automate avec un Détecteur
    • " + 100 | "
    • Cette section décrit le comportement de lancement d'un Automate
    • " + 101 | "
    • Les Actions lancées sont éditables depuis la page principale de l'application
    • " + 102 | "
    " 103 | ) 104 | ); 105 | } 106 | else if(current_admin_page.startsWith('Date')) 107 | { 108 | header_section.append("Recherche une date au format français ou anglais ou RFC 3339 ou RFC 2822"); 109 | } 110 | else if(current_admin_page.startsWith('Opération sur critères')) 111 | { 112 | header_section.append("Ce critère permet de combiner UN ou PLUSIEUR autre critères et y appliquer un opérateur"); 113 | 114 | content_section.prepend( 115 | create_helper_callout( 116 | "Remarque sur Opération sur Critère(s)", 117 | "
      " + 118 | "
    • Vous pouvez utiliser un critère Opération sur Critère(s) en sous-critère sauf lui-même
    • " + 119 | "
    " 120 | ) 121 | ); 122 | 123 | content_section.prepend( 124 | create_helper_callout( 125 | "Comprendre ce qu'est une Opération sur Critère(s)", 126 | "
      " + 127 | "
    • Ce critère permet de combiner l'existance d'autres critères
    • " + 128 | "
    • Ce critère doit contenir au moins UN sous critère
    • " + 129 | "
    • Les opérations possibles sont les suivantes : AND, OR, NOT, XOR

    • " + 130 | "
    • AND : L'ensemble des sous-critères doivent être validés
    • " + 131 | "
    • OR : Au moins UN des sous-critères doit être validé
    • " + 132 | "
    • NOT : AUCUN des sous-critères ne doit être validé
    • " + 133 | "
    • XOR : Uniquement UN SEUL des sous-critères doit être validé
    • " + 134 | "
    " 135 | ) 136 | ); 137 | } 138 | else if(current_admin_page.startsWith('Information balisée')) 139 | { 140 | header_section.append("Recherche d'information entre balise [ MON_INFO ] ou de hashtag #MON_INFO"); 141 | } 142 | 143 | }); -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 |

    Programmable IMAP4 controllers for humans 👋

    2 | 3 |

    4 | Travis-CI Build Badge 5 | Codacy Badge 6 | 7 | License: NPOSL-3.0 8 | 9 | 10 | 11 | 12 |

    13 | 14 | > Hermès is a pagan god in Greek mythology - messenger of the gods. 15 | 16 | ![hermes](https://user-images.githubusercontent.com/9326700/71805247-0eb8a200-3066-11ea-90a8-a58477ce5e8f.jpg) 17 | 18 | The names and logos of `iTop` and `Microsoft Exchange` are displayed only as samples. 19 | Any IMAP provider or service works with Hermes, just as `iTop` is only one of the services you can use to send HTTP requests. Hermes is not affiliated with Combodo (iTop) or Microsoft (Exchange). 20 | 21 | ## Contributions 22 | 23 | Please ⭐ this project if you found it useful. Even better, contribute by : 24 | - Reporting issues and problems 25 | - Submitting a fix with a pull request 26 | - Requesting features to benefit everyone 27 | 28 | ## 🍰 Why Hermes ? 29 | 30 | This project was created with a specific use case in mind, which brought up the possibilities of a more open and generic use case. 31 | A company may face this problem : 32 | 33 | **How do we manage the interoperability of services with n-tiers, based only on electronic exchanges?** 34 | 35 | A company was currently using the ITSM iTop program and the Incoming Mail (Mailbox Scanner) functionalities. 36 | The official description of iTop is the following : `This extension runs in the background to scan the defined mail inbox(es) and either create or update tickets based on the content of the incoming emails.` 37 | 38 | With the old solution (Incoming Mail): 39 | 40 | 1) Limited and restricted message identification 41 | 2) Forced to create IMAP files for *n* operations 42 | 3) Scanner actions are limited to basic operations 43 | 44 | They found themselves extremely limited by Incoming Mail's functionalities. 45 | 46 | Hermes offers a complete solution, building on what iTop cannot provide. 47 | 48 | ## ✨ Installation 49 | 50 | Hermes is easily installed and executed in two ways. Requirements: 51 | 52 | - A usable IMAP / SMTP account 53 | - Your choice of a Linux / Unix / Windows environment 54 | 55 | Whatever your preferred method, start by running : 56 | 57 | ```shell 58 | cd $HOME 59 | git clone https://github.com/Ousret/hermes.git 60 | cd ./hermes 61 | cp configuration.dist.yml configuration.yml 62 | ``` 63 | 64 | First, modify the configuration with your preferred text editor: `nano`, `vim`, etc.. 65 | 66 | ```shell 67 | nano configuration.yml 68 | ``` 69 | 70 | ```yaml 71 | PRODUCTION: &production 72 | <<: *common 73 | SECRET_KEY: PleaseChangeThisStringBeforeDeployment # Replace with a long randomly generated string 74 | # *-* smtp configuration *-* used to send error reports 75 | EMAIL_HOST: 'smtp-host' 76 | EMAIL_PORT: 587 77 | EMAIL_TIMEOUT: 10 78 | EMAIL_USE_TLS: True 79 | EMAIL_HOST_USER: 'smtp-user@smtp-host' 80 | EMAIL_HOST_PASSWORD: 'secret_smtp' 81 | EMAIL_FROM: 'smtp-user@smtp-host' 82 | INCIDENT_NOTIFIABLE: 'destination@gmail.com' # Replace with the email to send error reports to 83 | ``` 84 | 85 | ### Method 1 : WITH Docker 86 | 87 | If you've already installed `docker` and `docker-compose` on your machine, you can simply run : 88 | 89 | ```shell 90 | docker-compose up 91 | ``` 92 | 93 | ### Method 2 : WITHOUT Docker 94 | 95 | Requirements : `python3`, `pip`, `nodejs`, `npm`. Optional : `mariadb-server` and `mariadb-client`. 96 | 97 | These commands may require superuser privileges. (Installing the `yarn` utility) 98 | ```bash 99 | npm install yarn -g 100 | ``` 101 | 102 | ```shell 103 | pip install certifi pyopenssl --user 104 | 105 | python setup.py install --user 106 | cd ./hermes_ui 107 | yarn install 108 | yarn build 109 | cd .. 110 | ``` 111 | 112 | The second method requires a database implementation. If you're using `mariadb`, connect and create a `hermes` database. 113 | 114 | ```sql 115 | CREATE DATABASE hermes; 116 | ``` 117 | 118 | If you don't have `mariadb` installed, you can opt for a lightweight `sqlite` implementation. 119 | 120 | In the `configuration.yml` file, change the following parameter : 121 | 122 | ```yaml 123 | PRODUCTION: &production 124 | <<: *common 125 | SQLALCHEMY_DATABASE_URI: 'mysql://user:mdp@127.0.0.1/hermes' 126 | ``` 127 | 128 | If you don't want to use `mariadb`, replace it with : 129 | 130 | ```yaml 131 | PRODUCTION: &production 132 | <<: *common 133 | SQLALCHEMY_DATABASE_URI: 'sqlite:///hermes.sqlite' 134 | ``` 135 | 136 | Finally, launch `wsgi.py`. 137 | 138 | ```shell 139 | python wsgi.py 140 | ``` 141 | 142 | ### AFTER Method 1 or 2 143 | 144 | Navigate to the following address : [http://127.0.0.1:5000](http://127.0.0.1:5000) 145 | The default user is `hermes@localhost` and the password is `admin`. 146 | It's a good idea to change these after the first connection. 147 | 148 |

    149 | Capture d’écran 2020-01-10 à 15 59 14 150 |

    151 | 152 | ## ⚡ How does it work ? 153 | 154 | ![hermes-principes](https://user-images.githubusercontent.com/9326700/71805268-2001ae80-3066-11ea-9e8e-386044ddd621.gif) 155 | 156 | Essentially, 157 | 158 | An electronic **message** is received -> we use a **series of criteria** from a **detector** to find the nature of the message while preserving evaluation results -> **A series of actions** defined by the designer will be linked in a binary tree -> each action results in a **success** or a **failure** and takes the appropriate following action. 159 | 160 | ## 👤 Documentation 161 | 162 | This section is a guide to getting started with Hermes quickly. 163 | 164 | - [ ] [Understanding simplified variables with Hermes](docs/CHAPITRE-1.md) 165 | - [ ] [Write / save global variables](docs/CHAPITRE-2.md) 166 | - [ ] [Configure your IMAP box(es)](docs/CHAPITRE-3.md) 167 | - [ ] [Detecting an email message](docs/CHAPITRE-4.md) 168 | - [ ] [Creating a controller in response to a message detection](docs/CHAPITRE-5.md) 169 | - [ ] [Implement an action sequence](docs/CHAPITRE-6.md) 170 | - [ ] [Test and debug the controller](docs/CHAPITRE-7.md) 171 | 172 | Going further : 173 | 174 | - [ ] [Detection criteria](docs/CRITERES.md) 175 | - [ ] [The actions](docs/ACTIONS.md) 176 | - [ ] [Gmail](docs/GMAIL.md) 177 | 178 | ## 🚧 Maintenance 179 | 180 | This program is still in its development stages. 181 | Hermes is stable and available for production and deployment. This project can be improved - ideas for significant refactors are being considered. 182 | 183 | A GitHub Project is open with all the tasks to be carried out to make Hermes even more incredible! 184 | 185 | For now, I'm focusing on bugs and security maintenance, and I re-read and approve all contributions. 186 | 187 | ## ⬆️ Upgrade 188 | 189 | Hermes may require updates. To do so, run the `upgrade.sh` script. 190 | 191 | ```shell 192 | ./upgrade.sh 193 | ``` 194 | 195 | ## 📝 License 196 | 197 | **Commercial exploitation is strictly prohibited, however, internal use is authorized.** 198 | 199 | Released under "Non-Profit Open Software License 3.0 (NPOSL-3.0)" 200 | 201 | ## Contributor(s) : 202 | 203 | - Ahmed TAHRI @Ousret, Developer and maintainer 204 | - Didier JEAN ROBERT @SadarSSI, Initial conception and feature brainstormer 205 | - Denis GUILLOTEAU @Dsniss, Initial conception, tester, validator. -------------------------------------------------------------------------------- /docs/CHAPITRE-6.md: -------------------------------------------------------------------------------- 1 |

    Éditer les actions d'un Automate

    2 | 3 | **Pré-requis:** Avoir mis en place au moins un détecteur et la description associée d'un automate. 4 | 5 | ## ✨ Éditeur d'un automate 6 | 7 | Une fois la description de votre automate effectuée, nous vous invitons à revenir sur le menu "Éditeur d'Automate". 8 | 9 | Nous vous invitons à choisir votre automate correspondant depuis la zone "Choisir Automate". 10 | 11 |

    12 | Capture d’écran 2020-01-09 à 10 59 26 13 |

    14 | 15 | N'hésitez pas à regarder le volet des "variables" disponibles en appuyant sur le bouton en bas à droite. 16 | Nous constatons que nos deux variables du détecteur sont disponibles. C'est bon signe. 17 | 18 | ## 👁️ Comprendre l'interface d'édition des Automates 19 | 20 | Un assistant visuel vous permet de faire le tour de l'interface étape par étape. Nous vous invitons, 21 | au moins une fois à cliquer sur "Guide de l'interface". 22 | 23 |

    24 | Capture d’écran 2020-01-09 à 11 07 57 25 |

    26 | 27 | ## ✍️ Actions & Scénario 28 | 29 | Hermes remplace vos traitements répétitifs en vous permettant de créer une suite d'actions. 30 | 31 | Cette suite d'actions s'organise en arbre binaire, chaque action possède deux issues, l'une en cas de **réussite**, l'autre en cas **d'échec**. 32 | 33 | Vous pouvez modifier votre arbre d'actions avec les boutons suivants : 34 | 35 | - Nouvelle Action 36 | - Supprimer Action 37 | - Modifier Action 38 | - Remplacer Action 39 | 40 | ## Actions 41 | 42 | Les actions disponibles sur étagère sont les suivantes : 43 | 44 | | Type d'Action | Description | 45 | |------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| 46 | | RequeteSqlActionNoeud | Effectuer une requête de type SQL sur un serveur SGDB tel que Oracle, MySQL, PosgreSQL, Microsoft SQL Serveur et MariaDB. | 47 | | RequeteSoapActionNoeud | Effectuer une requête de type SOAP Webservice. | 48 | | RequeteHttpActionNoeud | Effectuer une requête de type HTTP sur un serveur distant. | 49 | | EnvoyerMessageSmtpActionNoeud | Ecrire un message électronique vers n- tiers via un serveur SMTP. | 50 | | TransfertSmtpActionNoeud | Transférer le message électronique d'origine vers n-tiers via un serveur SMTP. | 51 | | ConstructionInteretActionNoeud | Construire une variable intermédiaire. | 52 | | ConstructionChaineCaractereSurListeActionNoeud | Fabriquer une chaîne de caractère à partir d'une liste identifiable. | 53 | | InvitationEvenementActionNoeud | Emettre ou mettre à jour une invitation à un évènement par message électronique. | 54 | | VerifierSiVariableVraiActionNoeud | Vérifie si une variable est Vrai. | 55 | | ComparaisonVariableActionNoeud | Effectue une comparaison entre deux variables de votre choix, nombres, dates, etc.. | 56 | | DeplacerMailSourceActionNoeud | Déplacer le message électronique d'origine dans un autre dossier. | 57 | | CopierMailSourceActionNoeud | Copier le message électronique d'origine dans un autre dossier. | 58 | | SupprimerMailSourceActionNoeud | Supprimer le message électronique d'origine | 59 | | TransformationListeVersDictionnaireActionNoeud | Création d'une variable intermédiaire sachant une liste [{'cle_a': 'val_a', 'cle_b': 'val_b'}] vers {'val_a': 'val_b'}. | 60 | | ItopRequeteCoreGetActionNoeud | Effectuer une requête sur iTop avec l'opération core/get REST JSON | 61 | | ItopRequeteCoreCreateActionNoeud | Effectuer une requête sur iTop avec l'opération core/create REST JSON | 62 | | ItopRequeteCoreUpdateActionNoeud | Effectuer une requête sur iTop avec l'opération core/update REST JSON | 63 | | ItopRequeteCoreApplyStimulusActionNoeud | Effectuer une requête sur iTop avec l'opération core/apply_stimulus REST JSON | 64 | | ItopRequeteCoreDeleteActionNoeud | Effectuer une requête sur iTop avec l'opération core/delete REST JSON | 65 | | ExecutionAutomateActionNoeud | Exécute un autre Automate (routine ou plugin) | 66 | 67 | Chaque action nécessite de remplir n argument(s). 68 | Les arguments communs sont les suivants : 69 | 70 | - designation (Courte description de votre action) 71 | - friendly_name (Précise dans quel nom de variable le résultat doit être stocké) 72 | 73 | ### Scénario fictif 74 | 75 | Pour traiter nos factures iCloud, nous allons employer le scénario suivant : 76 | 77 | - Si le montant de la facture est inférieur à 1.00 EUR on supprime le message immédiatement 78 | - Sinon on effectue une requête http sur un serveur distant pour l'informer de la facture 79 | - Dans le cas ou facture >= 1.00 EUR on la conserve dans le dossier IMAP iCloud 80 | 81 | Pour cela nous allons utiliser les actions de la manière suivante : 82 | 83 | - ComparaisonVariableActionNoeud 84 | - RequeteHttpActionNoeud 85 | - DeplacerMailSourceActionNoeud 86 | - SupprimerMailSourceActionNoeud 87 | 88 | ### Création d'une action 89 | 90 | Pour créer une nouvelle action, nous vous invitons à cliquer sur "Nouvelle Action". 91 | 92 | 1) Choisir le type d'action. 93 | 2) (Optionnel) choisir l'action parente et la branche fils, échec ou réussite. 94 | 3) Renseigner les arguments de l'assistant. 95 | 96 | Captures, dans l'ordre. 97 | 98 |

    99 | Capture d’écran 2020-01-09 à 11 14 53 100 | Capture d’écran 2020-01-09 à 11 17 29 101 | Capture d’écran 2020-01-09 à 11 17 35 102 |

    103 | 104 | ⚠️ Les deux dernières captures n'apparaissent que lorsque la première action a déjà été créée. L'action racine n'a pas de parent et elle s'exécute obligatoirement. 105 | 106 | ### Arguments d'une action 107 | 108 | Chaque action nécessite n argument(s) obligatoire(s) et n optionnel(s). Chaque argument peut contenir des variables. 109 | 110 |

    111 | Capture d’écran 2020-01-09 à 11 15 46 112 |

    113 | 114 | Un support de completion automatique est disponible dans la mesure du raisonable. 115 | 116 | ⚠️ Aucun support de marche arrière n'est disponible. Ceci est une limitation de la bibliothèque sweetalert2. 117 | L'assistant de création des actions ne permet pas de revenir à une étape antérieur. 118 | 119 | ### L'arbre des actions 120 | 121 |

    122 | Capture d’écran 2020-01-09 à 11 21 41 123 |

    124 | 125 | Une fois vos actions mises en place. L'éditeur vous proposera une représentation visuelle de votre arbre d'action. 126 | 127 | ### Modifier l'arbre 128 | 129 | Vous pouvez insérer dans l'arbre, supprimer et remplacer. 130 | Sachez qu'Hermes tentera de rééquilibrer l'arbre en priviligiant toujours la branche de **réussite**. 131 | 132 | ## Pour aller plus loin 133 | 134 | - [ ] [Test et debug d'un automate](CHAPITRE-7.md) 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    Automates programmables IMAP4 pour les humains 👋

    2 | 3 |

    4 | Travis-CI Build Badge 5 | Codacy Badge 6 | 7 | License: NPOSL-3.0 8 | 9 | 10 | 11 | 12 |

    13 | 14 | > Hermès est une divinité issue de la mythologie grecque. Messager des dieux. 15 | 16 | ![hermes](https://user-images.githubusercontent.com/9326700/71805247-0eb8a200-3066-11ea-90a8-a58477ce5e8f.jpg) 17 | 18 | Les noms et logos `iTop` et `Microsoft Exchange` sont affichés à titre d'exemple uniquement. 19 | N'importe quel service IMAP fonctionne avec Hermes. De même qu'iTop est UN des services sur lequel vous pouvez émettre des requêtes. Hermes n'est pas affilié à Combodo (iTop) ni à Microsoft (Exchange). 20 | 21 | ## Contributions 22 | 23 | Merci d'offrir une ⭐ à ce projet s'il vous a été utile. Encore mieux, participez en : 24 | - Signalant un problème 25 | - Proposant un correctif via le système de pull request 26 | - Proposant des fonctionnalités/idées utiles à tous 27 | 28 | ## 🍰 Quel besoin ? 29 | 30 | Ce projet est né d'un besoin spécifique qui a laissé entrevoir la possibilité d'un cas bien plus ouvert et générique. 31 | Une entreprise peut-être confrontée à cette problématique : 32 | 33 | **Comment gérer une interopérabilité des services avec n-tiers en se basant uniquement sur les échanges électroniques ?** 34 | 35 | L'origine est qu'une entreprise utilisant le programme ITSM iTop et l'Incoming Mail (Scanner de boîte mail). 36 | La description officielle du module iTop est la suivante : `This extension runs in the background to scan the defined mail inbox(es) and either create or update tickets based on the content of the incoming emails.` 37 | 38 | Avec l'ancienne solution (Incoming Mail): 39 | 40 | 1) Identification d'un message très limitée et restreinte 41 | 2) Obligation de créer des dossiers IMAP pour n opération(s) 42 | 3) Les actions du scanner sont limitées à des simples opérations 43 | 44 | Ils se sont retrouvés extrêment limitée par l'Incoming Mail. 45 | 46 | Hermes offre une solution complète à ce qu'iTop ne peux pas fournir. 47 | 48 | ## ✨ Installation 49 | 50 | Le projet Hermes s'installe et s'execute très facilement de deux manières. À condition d'avoir : 51 | 52 | - Un compte IMAP et SMTP utilisable 53 | - Environnement Linux, Unix ou Windows au choix 54 | 55 | Quelque soit votre méthode préférée, commencez par : 56 | 57 | ```shell 58 | cd $HOME 59 | git clone https://github.com/Ousret/hermes.git 60 | cd ./hermes 61 | cp configuration.dist.yml configuration.yml 62 | ``` 63 | 64 | Modifions d'abord la configuration à l'aide de votre éditeur préféré, `nano`, `vim`, etc.. 65 | 66 | ```shell 67 | nano configuration.yml 68 | ``` 69 | 70 | ```yaml 71 | PRODUCTION: &production 72 | <<: *common 73 | SECRET_KEY: MerciDeMeChangerImmediatementAvantPremierLancement # Remplacer par une longue chaîne de caractère aléatoire 74 | # *-* configuration smtp *-* à utiliser pour envoyer les rapports d'erreurs 75 | EMAIL_HOST: 'hote-smtp' 76 | EMAIL_PORT: 587 77 | EMAIL_TIMEOUT: 10 78 | EMAIL_USE_TLS: True 79 | EMAIL_HOST_USER: 'smtp-utilisateur@hote-smtp' 80 | EMAIL_HOST_PASSWORD: 'secret_smtp' 81 | EMAIL_FROM: 'smtp-utilisateur@hote-smtp' 82 | INCIDENT_NOTIFIABLE: 'destinataire@gmail.com' # Remplacer par l'adresse email à laquelle transmettre un rapport d'erreur 83 | ``` 84 | 85 | ### Méthode 1 : AVEC Docker 86 | 87 | En ayant déjà installé `docker` et `docker-compose` sur votre machine, vous n'avez plus qu'à lancer : 88 | 89 | ```shell 90 | docker-compose up 91 | ``` 92 | 93 | ### Méthode 2 : SANS Docker 94 | 95 | Les pré-requis sont les suivants : `python3`, `pip`, `nodejs`, `npm`. Optionnellement `mariadb-server` et `mariadb-client`. 96 | 97 | Il est possible que cette commande nécessite les droits super-utilisateur. (Installation de l'utilitaire `yarn`) 98 | ```bash 99 | npm install yarn -g 100 | ``` 101 | 102 | ```shell 103 | pip install certifi pyopenssl --user 104 | 105 | python setup.py install --user 106 | cd ./hermes_ui 107 | yarn install 108 | yarn build 109 | cd .. 110 | ``` 111 | 112 | La seconde méthode nécessite de mettre en oeuvre une base de données. Si vous êtes sous `mariadb`, connectez-vous et créez une base de données `hermes`. 113 | 114 | ```sql 115 | CREATE DATABASE hermes; 116 | ``` 117 | 118 | Si vous n'avez pas `mariadb`, vous pouvez opter pour un système léger `sqlite` qui ne nécessite rien de plus. 119 | 120 | Dans le fichier `configuration.yml`, modifiez le paramètre suivant : 121 | 122 | ```yaml 123 | PRODUCTION: &production 124 | <<: *common 125 | SQLALCHEMY_DATABASE_URI: 'mysql://utilisateur:mdp@127.0.0.1/hermes' 126 | ``` 127 | 128 | Si vous ne souhaitez pas mettre en place `mariadb`, remplacez par : 129 | 130 | ```yaml 131 | PRODUCTION: &production 132 | <<: *common 133 | SQLALCHEMY_DATABASE_URI: 'sqlite:///hermes.sqlite' 134 | ``` 135 | 136 | Pour finir lancer le programme `wsgi.py`. 137 | 138 | ```shell 139 | python wsgi.py 140 | ``` 141 | 142 | ### APRÈS Méthode 1 OU 2 143 | 144 | Ouvrir le navigateur à l'adresse suivante : [http://127.0.0.1:5000](http://127.0.0.1:5000) 145 | L'utilisateur par défaut est `hermes@localhost` et le mot de passe associé est `admin`. 146 | Il est bien entendu sage de le modifier rapidement après la 1ere connexion. 147 | 148 |

    149 | Capture d’écran 2020-01-10 à 15 59 14 150 |

    151 | 152 | ## ⚡ Comment ça marche ? 153 | 154 | ![hermes-principes](https://user-images.githubusercontent.com/9326700/71805268-2001ae80-3066-11ea-9e8e-386044ddd621.gif) 155 | 156 | En bref, 157 | 158 | Un **message** électronique est reçu, nous arrivons, grâce à une suite **de critères** (depuis **un détecteur**) à définir la nature du message tout en conservant les résultats de l'évaluation 159 | des critères. Ensuite **une suite d'actions** déterminées par le concepteur s'enchainera en arbre binaire, chaque action se solde par **une réussite** ou **un échec** et prend la branche correspondante 160 | pour exécuter l'action suivante. 161 | 162 | ## 👤 Documentations 163 | 164 | Cette section vous propose de prendre en main rapidement Hermes. 165 | 166 | - [ ] [Comprendre le mécanisme des variables simplifiées sous Hermes](docs/CHAPITRE-1.md) 167 | - [ ] [Écrire et enregistrer vos variables partagées / globales](docs/CHAPITRE-2.md) 168 | - [ ] [Configurer votre/vos boîte(s) IMAP](docs/CHAPITRE-3.md) 169 | - [ ] [Détecter un message électronique](docs/CHAPITRE-4.md) 170 | - [ ] [Créer un automate en réaction à une détection de message électronique](docs/CHAPITRE-5.md) 171 | - [ ] [Mettre en oeuvre une suite d'action à appliquer après la détection](docs/CHAPITRE-6.md) 172 | - [ ] [Test et debug d'un automate](docs/CHAPITRE-7.md) 173 | 174 | Pour aller encore plus loin : 175 | 176 | - [ ] [Les critères de détection](docs/CRITERES.md) 177 | - [ ] [Les actions](docs/ACTIONS.md) 178 | - [ ] [GMail](docs/GMAIL.md) 179 | 180 | ## 🚧 Maintenance 181 | 182 | Ce programme n'est qu'à ses balbutiements. 183 | Hermès est stable et disponible pour la production. Ce projet peut être amélioré, des idées d'évolutions significatives sont à l'étude. 184 | 185 | Un projet Github est ouvert avec l'ensemble des idées / tâches à réaliser pour rendre ce projet incroyable. 186 | 187 | Pour le moment, j'adresse la maintenance concernant les bugs et la sécurité et je relis et j'approuve les contributions soumises. 188 | 189 | ## ⬆️ Mise à niveau 190 | 191 | Hermès peut être sujet à une mise à jour. Pour ce faire il est possible d'utiliser le script `upgrade.sh`. 192 | 193 | ```shell 194 | ./upgrade.sh 195 | ``` 196 | 197 | ## 📝 Droits 198 | 199 | **L'exploitation commerciale est strictement interdite tandis que l'usage interne professionnel est autorisée.** 200 | 201 | Publication sous "Non-Profit Open Software License 3.0 (NPOSL-3.0)" 202 | 203 | ## Contributeur(s) : 204 | 205 | - Ahmed TAHRI @Ousret, Développeur et mainteneur 206 | - Didier JEAN ROBERT @SadarSSI, Conception et expression de besoins 207 | - Denis GUILLOTEAU @Dsniss, Aide à la conception, expression de besoins, testeur, valideur 208 | -------------------------------------------------------------------------------- /hermes_ui/assets/scripts/Compoments/jquery.asuggest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery textarea suggest plugin 3 | * 4 | * Copyright (c) 2009-2010 Roman Imankulov 5 | * 6 | * Dual licensed under the MIT and GPL licenses: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.gnu.org/licenses/gpl.html 9 | * 10 | * Requires: 11 | * - jQuery (tested with 1.3.x and 1.4.x) 12 | * - jquery.a-tools >= 1.4.1 (http://plugins.jquery.com/project/a-tools) 13 | */ 14 | 15 | /*globals jQuery,document */ 16 | 17 | (function ($) { 18 | // workaround for Opera browser 19 | if (navigator.userAgent.match(/opera/i)) { 20 | $(document).keypress(function (e) { 21 | if ($.asuggestFocused) { 22 | $.asuggestFocused.focus(); 23 | $.asuggestFocused = null; 24 | e.preventDefault(); 25 | e.stopPropagation(); 26 | } 27 | }); 28 | } 29 | 30 | $.asuggestKeys = { 31 | UNKNOWN: 0, 32 | SHIFT: 16, 33 | CTRL: 17, 34 | ALT: 18, 35 | LEFT: 37, 36 | UP: 38, 37 | RIGHT: 39, 38 | DOWN: 40, 39 | DEL: 46, 40 | TAB: 9, 41 | RETURN: 13, 42 | ESC: 27, 43 | COMMA: 188, 44 | PAGEUP: 33, 45 | PAGEDOWN: 34, 46 | BACKSPACE: 8, 47 | SPACE: 32 48 | }; 49 | $.asuggestFocused = null; 50 | 51 | $.fn.asuggest = function (suggests, options) { 52 | return this.each(function () { 53 | $.makeSuggest(this, suggests, options); 54 | }); 55 | }; 56 | 57 | $.fn.asuggest.defaults = { 58 | 'delimiters': '\n ', 59 | 'minChunkSize': 1, 60 | 'cycleOnTab': true, 61 | 'autoComplete': true, 62 | 'endingSymbols': ' ', 63 | 'stopSuggestionKeys': [$.asuggestKeys.RETURN, $.asuggestKeys.SPACE], 64 | 'ignoreCase': false 65 | }; 66 | 67 | /* Make suggest: 68 | * 69 | * create and return jQuery object on the top of DOM object 70 | * and store suggests as part of this object 71 | * 72 | * @param area: HTML DOM element to add suggests to 73 | * @param suggests: The array of suggest strings 74 | * @param options: The options object 75 | */ 76 | $.makeSuggest = function (area, suggests, options) { 77 | options = $.extend({}, $.fn.asuggest.defaults, options); 78 | 79 | var KEY = $.asuggestKeys, 80 | $area = $(area); 81 | $area.suggests = suggests; 82 | $area.options = options; 83 | 84 | /* Internal method: get the chunk of text before the cursor */ 85 | $area.getChunk = function () { 86 | var delimiters = this.options.delimiters.split(''), // array of chars 87 | textBeforeCursor = this.val().substr(0, this.getSelection().start), 88 | indexOfDelimiter = -1, 89 | i, 90 | d, 91 | idx; 92 | for (i = 0; i < delimiters.length; i++) { 93 | d = delimiters[i]; 94 | idx = textBeforeCursor.lastIndexOf(d); 95 | if (idx > indexOfDelimiter) { 96 | indexOfDelimiter = idx; 97 | } 98 | } 99 | if (indexOfDelimiter < 0) { 100 | return textBeforeCursor; 101 | } else { 102 | return textBeforeCursor.substr(indexOfDelimiter + 1); 103 | } 104 | }; 105 | 106 | /* Internal method: get completion. 107 | * If performCycle is true then analyze getChunk() and and getSelection() 108 | */ 109 | $area.getCompletion = function (performCycle) { 110 | var text = this.getChunk(), 111 | selectionText = this.getSelection().text, 112 | suggests = this.suggests, 113 | foundAlreadySelectedValue = false, 114 | firstMatchedValue = null, 115 | i, 116 | suggest; 117 | // search the variant 118 | for (i = 0; i < suggests.length; i++) { 119 | suggest = suggests[i]; 120 | if ($area.options.ignoreCase) { 121 | suggest = suggest.toLowerCase(); 122 | text = text.toLowerCase(); 123 | } 124 | // some variant is found 125 | if (suggest.indexOf(text) === 0) { 126 | if (performCycle) { 127 | if (text + selectionText === suggest) { 128 | foundAlreadySelectedValue = true; 129 | } else if (foundAlreadySelectedValue) { 130 | return suggest.substr(text.length); 131 | } else if (firstMatchedValue === null) { 132 | firstMatchedValue = suggest; 133 | } 134 | } else { 135 | return suggest.substr(text.length); 136 | } 137 | } 138 | } 139 | if (performCycle && firstMatchedValue) { 140 | return firstMatchedValue.substr(text.length); 141 | } else { 142 | return null; 143 | } 144 | }; 145 | 146 | $area.updateSelection = function (completion) { 147 | if (completion) { 148 | var _selectionStart = $area.getSelection().start, 149 | _selectionEnd = _selectionStart + completion.length; 150 | if ($area.getSelection().text === "") { 151 | if ($area.val().length === _selectionStart) { // Weird IE workaround, I really have no idea why it works 152 | $area.setCaretPos(_selectionStart + 10000); 153 | } 154 | $area.insertAtCaretPos(completion); 155 | } else { 156 | $area.replaceSelection(completion); 157 | } 158 | $area.setSelection(_selectionStart, _selectionEnd); 159 | } 160 | }; 161 | 162 | $area.unbind('keydown.asuggest').bind('keydown.asuggest', function (e) { 163 | if (e.keyCode === KEY.TAB) { 164 | if ($area.options.cycleOnTab) { 165 | var chunk = $area.getChunk(); 166 | if (chunk.length >= $area.options.minChunkSize) { 167 | $area.updateSelection($area.getCompletion(true)); 168 | } 169 | e.preventDefault(); 170 | e.stopPropagation(); 171 | $area.focus(); 172 | $.asuggestFocused = this; 173 | return false; 174 | } 175 | } 176 | // Check for conditions to stop suggestion 177 | if ($area.getSelection().length && 178 | $.inArray(e.keyCode, $area.options.stopSuggestionKeys) !== -1) { 179 | // apply suggestion. Clean up selection and insert a space 180 | var _selectionEnd = $area.getSelection().end + 181 | $area.options.endingSymbols.length; 182 | var _text = $area.getSelection().text + 183 | $area.options.endingSymbols; 184 | $area.replaceSelection(_text); 185 | $area.setSelection(_selectionEnd, _selectionEnd); 186 | e.preventDefault(); 187 | e.stopPropagation(); 188 | this.focus(); 189 | $.asuggestFocused = this; 190 | return false; 191 | } 192 | }); 193 | 194 | $area.unbind('keyup.asuggest').bind('keyup.asuggest', function (e) { 195 | var hasSpecialKeys = e.altKey || e.metaKey || e.ctrlKey, 196 | hasSpecialKeysOrShift = hasSpecialKeys || e.shiftKey; 197 | switch (e.keyCode) { 198 | case KEY.UNKNOWN: // Special key released 199 | case KEY.SHIFT: 200 | case KEY.CTRL: 201 | case KEY.ALT: 202 | case KEY.RETURN: // we don't want to suggest when RETURN key has pressed (another IE workaround) 203 | break; 204 | case KEY.TAB: 205 | if (!hasSpecialKeysOrShift && $area.options.cycleOnTab) { 206 | break; 207 | } 208 | case KEY.ESC: 209 | case KEY.BACKSPACE: 210 | case KEY.DEL: 211 | case KEY.UP: 212 | case KEY.DOWN: 213 | case KEY.LEFT: 214 | case KEY.RIGHT: 215 | if (!hasSpecialKeysOrShift && $area.options.autoComplete) { 216 | $area.replaceSelection(""); 217 | } 218 | break; 219 | default: 220 | if (!hasSpecialKeys && $area.options.autoComplete) { 221 | var chunk = $area.getChunk(); 222 | if (chunk.length >= $area.options.minChunkSize) { 223 | $area.updateSelection($area.getCompletion(false)); 224 | } 225 | } 226 | break; 227 | } 228 | }); 229 | return $area; 230 | }; 231 | }(jQuery)); 232 | -------------------------------------------------------------------------------- /docs/CHAPITRE-1.md: -------------------------------------------------------------------------------- 1 |

    Comprendre le fonctionnement des variables sous d'Hermes

    2 | 3 | ## ✨ La génèse 4 | 5 | Hermes dispose d'un moteur de variables (simplifié). 6 | La syntaxe d'appel des variables est similaire à celle du moteur de template Twig. 7 | 8 | Faire appel à une variable de la manière suivante : `{{ ma_variable }}`. 9 | Une variable commence systèmatiquement par `{{` et se termine par `}}`. 10 | Les espaces ne sont pas obligatoires. 11 | 12 | ## Où ? 13 | 14 | Vous êtes autorisés à utiliser les variables dans vos actions et dans la description de vos boîtes IMAP. 15 | Il n'est pas possible d'utiliser les variables dans les paramètres des critères de détection. 16 | 17 | Les variables disponibles sont accessible par un volet caché à droite. 18 | 19 | ![hermes-variables](https://user-images.githubusercontent.com/9326700/71878521-1d698c80-312c-11ea-9516-e828498c79b0.gif) 20 | 21 | Le bouton en bas à droite vous permet de le faire apparaître et disparaître à votre guise. 22 | 23 | Trois sections sont visibles : 24 | 25 | - Les variables locales sont produites par un automate, ses actions ainsi que les critères de selection d'un message. 26 | - Les variables globales sont produites par vos entrés depuis le menu "Mes variables globales". 27 | - Les filtres permettent d'agir sur une variable, pour plus d'information, ci-dessous. 28 | 29 | ## Comment ? 30 | 31 | Vous produisez des variables de *TROIS* manières : 32 | 33 | - Le résultat d'un critère de recherche 34 | - Une variable accessible globalement, depuis le menu "Mes variables globales" 35 | - Le résultat d'une action 36 | 37 | Vous pouvez exploiter les variables seulement : 38 | 39 | - Dans les arguments d'une action 40 | 41 | ## Les filtres 42 | 43 | Pouvoir stocker et réutiliser de l'information c'est bien, pouvoir la transformer c'est mieux. 44 | 45 | | Filtre | Description | Avant | Après | 46 | |----------------------|---------------------------------------------------------------------------------------------------------------|--------------------|--------------| 47 | | escapeQuote | Sécurise une chaîne de caractère pour une insertion dans un JSON. Traite par ex. les doubles chevrons. | Je"suis" | Je\\"suis\\" | 48 | | keys | Liste les clés d'un dictionnaire associatif | {A: 0, B: 1, C: 2} | [A, B, C] | 49 | | int | Conserve UNIQUEMENT les chiffres d'une chaîne de caractère et convertir en entier | ITOP-T-00541 | 541 | 50 | | float | Conserve UNIQUEMENT les chiffres et caractère '.' et ',' et convertir en float | 1,22 € | 1.22 | 51 | | lower | Chaque caractère se transforme en minuscule s'il y a lieu | ITOP-T-00541 | itop-t-00541 | 52 | | upper | Chaque caractère se transforme en majuscule s'il y a lieu | je suis | JE SUIS | 53 | | strip | Retire les espaces d'une chaîne de caractères | je suis | jesuis | 54 | | capitalize | Première lettre en majuscule uniquement | je Suis | Je suis | 55 | | dateAjouterUnJour | Prends une date US et y additionne une seule journée | 2020/01/01 | 2020/01/02 | 56 | | dateAjouterUnMois | Prends une date US et y additionne un mois | 2020/01/01 | 2020/02/01 | 57 | | dateAjouterUneAnnee | Prends une date US et y additionne une année | 2020/01/01 | 2021/01/01 | 58 | | dateRetirerUnJour | Prends une date US et y retire une journée | 2020/01/01 | 2019/12/31 | 59 | | dateRetirerUnMois | Prends une date US et y retire un mois | 2020/01/01 | 2019/12/01 | 60 | | dateRetirerUneAnnee | Prends une date US et y retire une année | 2020/01/01 | 2019/01/01 | 61 | | dateFormatFrance | Prends une date et passe du format US à FR Y-m-d à d-m-Y | 2020/01/01 | 01/01/2020 | 62 | | dateFormatUS | Prends une date et passe du format FR à US d-m-Y à Y-m-d | 01/01/2020 | 2020/01/01 | 63 | | dateProchainLundi | Prends une date FR et remplace cette date par la date du prochain Lundi SI cette date n'est pas déjà un Lundi | | | 64 | | dateProchainMardi | Prends une date FR et remplace cette date par la date du prochain Mardi SI cette date n'est pas déjà un Mardi | | | 65 | | dateProchainMercredi | // | | | 66 | | dateProchainJeudi | // | | | 67 | | dateProchainVendredi | // | | | 68 | | dateProchainSamedi | // | | | 69 | | dateProchainDimanche | // | | | 70 | | slug | Transforme une chaîne de caractères en slug. URL-Safe String. | J'étais là | j-etais-la | 71 | | alNum | Conserve les caractères alphanumériques d'une chaîne | [##BONJOUR1] | BONJOUR1 | 72 | | alpha | Conserve les caractères alpha d'une chaîne | [##BONJOUR1] | BONJOUR | 73 | | remplissage**Zero | Rajoute des zéros en début de chaîne. Remplacer « *** » par « Un,Deux, Trois, Quatre, Cinq, etc.. » | | | 74 | 75 | Imaginons que la variable `{{ ma_variable }}` contienne la valeur `ITOP-T-00541`. Pour en extraire la partie numérique j'applique 76 | le filtre `int` sur celle-ci. 77 | 78 | ``` 79 | {{ ma_variable|int }} 80 | ``` 81 | 82 | `{{ ma_variable }}` est remplacée par `ITOP-T-00541` tandis que `{{ ma_variable|int }}` est remplacée par `541`. 83 | 84 | ## Les différents types 85 | 86 | Les variables sous Hermes ne sont qu'un *proxy* vers les variables nativements accessibles sous Python. 87 | 88 | Ce qui signifie qu'une variable peut contenir un `int`, `str`, `float` mais aussi un `list` et `dict` ! 89 | 90 | `{{ ma_variable }}` peut contenir le `dict` suivant : 91 | 92 | ```json 93 | { 94 | "nom_utilisateur": "john_doe", 95 | "mot_de_passe": "azerty" 96 | } 97 | ``` 98 | 99 | Pour acceder à `nom_utilisateur` --> `{{ ma_variable.nom_utilisateur }}`. 100 | 101 | Pour accéder à un niveau plus bas nous séparons les `étages` par un POINT. 102 | Il est également possible d'accéder à `nom_utilisateur` de la manière suivante `{{ ma_variable.0 }}`. 103 | 104 | `{{ ma_variable }}` peut contenir le `list` suivant : 105 | 106 | ```json 107 | [ 108 | 'A', 109 | 'B', 110 | 'C' 111 | ] 112 | ``` 113 | 114 | Pour acceder à la lettre `C`, on écrit `{{ ma_variable.2 }}`. Les index de liste commence à ZÉRO. 115 | 116 | ## Les variables imbriquées 117 | 118 | Pour les plus aguéris, sachez que vous pouvez invoquer une variable dans une variable. Sans limite. 119 | 120 | Imaginons que la variable `{{ ma_variable }}` contienne : 121 | 122 | ```json 123 | { 124 | "tickets": { 125 | "561": { 126 | "A": 1, 127 | "B": 2 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | Nous souhaitons obtenir la valeur associée à `A`, soit `1`. Nous écrirons, naturellement, alors `{{ ma_variable.tickets.561.A }}`. 134 | Néanmoins, partons du principe que nous sachons pas à l'avance que nous souhaitons passer par le niveau `561`. 135 | 136 | Disons que si un de vos critères a réussi à capturer `561` dans la variable `mon_numero_de_ticket`. 137 | 138 | Nous pouvons écrire : `{{ ma_variable.tickets.{{ mon_numero_de_ticket }}.A }}`, sachant qu'il sera traduit par `{{ ma_variable.tickets.561.A }}` puis `1`. 139 | Génial, non ? 140 | 141 | Maintenant si `{{ mon_numero_de_ticket }}` contient `Ticket 561` à la place de `561`, vous n'avez qu'à appliquer le filtre `|int` sur cette variable tel que : 142 | `{{ ma_variable.tickets.{{ mon_numero_de_ticket|int }}.A }}` 143 | 144 | ## Pour aller plus loin 145 | 146 | - [ ] [Écrire et enregistrer vos variables partagées / globales](CHAPITRE-2.md) 147 | --------------------------------------------------------------------------------