diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2dbd64e99..e6a5156c9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.4.16 +current_version = 2.4.17 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P.*)-(?P\d+))? diff --git a/README.md b/README.md index 04514642c..0cfc40deb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Incident Response Investigation System
- Current Version v2.4.16 + Current Version v2.4.17
Online Demonstration

@@ -52,7 +52,7 @@ git clone https://github.com/dfir-iris/iris-web.git cd iris-web # Checkout to the last tagged version -git checkout v2.4.16 +git checkout v2.4.17 # Copy the environment file cp .env.model .env diff --git a/docker-compose.yml b/docker-compose.yml index b9b24a3f1..ce5bbf263 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,25 +25,25 @@ services: extends: file: docker-compose.base.yml service: db - image: ${DB_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_db}:${DB_IMAGE_TAG:-v2.4.16} + image: ${DB_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_db}:${DB_IMAGE_TAG:-v2.4.17} app: extends: file: docker-compose.base.yml service: app - image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.16} + image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.17} worker: extends: file: docker-compose.base.yml service: worker - image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.16} + image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.17} nginx: extends: file: docker-compose.base.yml service: nginx - image: ${NGINX_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_nginx}:${NGINX_IMAGE_TAG:-v2.4.16} + image: ${NGINX_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_nginx}:${NGINX_IMAGE_TAG:-v2.4.17} volumes: iris-downloads: diff --git a/source/app/blueprints/alerts/alerts_routes.py b/source/app/blueprints/alerts/alerts_routes.py index 5876f6e1d..8740aabe1 100644 --- a/source/app/blueprints/alerts/alerts_routes.py +++ b/source/app/blueprints/alerts/alerts_routes.py @@ -110,31 +110,45 @@ def alerts_list_route() -> Response: except ValueError: return response_error('Invalid alert ioc') - filtered_data = get_filtered_alerts( - start_date=request.args.get('creation_start_date'), - end_date=request.args.get('creation_end_date'), - source_start_date=request.args.get('source_start_date'), - source_end_date=request.args.get('source_end_date'), - source_reference=request.args.get('source_reference'), - title=request.args.get('alert_title'), - description=request.args.get('alert_description'), - status=request.args.get('alert_status_id', type=int), - severity=request.args.get('alert_severity_id', type=int), - owner=request.args.get('alert_owner_id', type=int), - source=request.args.get('alert_source'), - tags=request.args.get('alert_tags'), - classification=request.args.get('alert_classification_id', type=int), - client=request.args.get('alert_customer_id'), - case_id=request.args.get('case_id', type=int), - alert_ids=alert_ids, - page=page, - per_page=per_page, - sort=request.args.get('sort'), - assets=alert_assets, - iocs=alert_iocs, - resolution_status=request.args.get('alert_resolution_id', type=int), - current_user_id=current_user.id - ) + fields_str = request.args.get('fields') + if fields_str: + # Split into a list + fields = [field.strip() for field in fields_str.split(',') if field.strip()] + else: + fields = None + + try: + filtered_data = get_filtered_alerts( + start_date=request.args.get('creation_start_date'), + end_date=request.args.get('creation_end_date'), + source_start_date=request.args.get('source_start_date'), + source_end_date=request.args.get('source_end_date'), + source_reference=request.args.get('source_reference'), + title=request.args.get('alert_title'), + description=request.args.get('alert_description'), + status=request.args.get('alert_status_id', type=int), + severity=request.args.get('alert_severity_id', type=int), + owner=request.args.get('alert_owner_id', type=int), + source=request.args.get('alert_source'), + tags=request.args.get('alert_tags'), + classification=request.args.get('alert_classification_id', type=int), + client=request.args.get('alert_customer_id'), + case_id=request.args.get('case_id', type=int), + alert_ids=alert_ids, + page=page, + per_page=per_page, + sort=request.args.get('sort', 'desc', type=str), + custom_conditions=request.args.get('custom_conditions'), + assets=alert_assets, + iocs=alert_iocs, + resolution_status=request.args.get('alert_resolution_id', type=int), + current_user_id=current_user.id, + fields=fields + ) + + except Exception as e: + app.app.logger.exception(e) + return response_error(str(e)) if filtered_data is None: return response_error('Filtering error') @@ -434,6 +448,9 @@ def alerts_batch_update_route() -> Response: if not user_has_client_access(current_user.id, alert.alert_customer_id): return response_error('User not entitled to update alerts for the client', status=403) + if getattr(alert, 'alert_owner_id') is None: + updates['alert_owner_id'] = current_user.id + if data.get('alert_owner_id') == "-1" or data.get('alert_owner_id') == -1: updates['alert_owner_id'] = None @@ -531,7 +548,7 @@ def alerts_delete_route(alert_id) -> Response: delete_similar_alert_cache(alert_id=alert_id) # Delete the similarity entries - delete_related_alerts_cache(alert_id=alert_id) + delete_related_alerts_cache([alert_id]) # Delete the alert from the database db.session.delete(alert) diff --git a/source/app/blueprints/alerts/templates/alerts.html b/source/app/blueprints/alerts/templates/alerts.html index a7c28c1ac..b2532cf7c 100644 --- a/source/app/blueprints/alerts/templates/alerts.html +++ b/source/app/blueprints/alerts/templates/alerts.html @@ -179,6 +179,50 @@ +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+

Here are two sample custom conditions:

+

+[{
+  "field": "alert_severity_id",
+  "operator": "in",
+  "value": [1, 2]
+},
+{
+  "field": "alert_title",
+  "operator": "like",
+  "value": "phishing"
+}]
+

+[{
+  "field": "severity.severity_name",
+  "operator": "like",
+  "value": "Critical"
+}]
+
+

Copy one of these conditions and paste it into the "Custom Conditions" field above, then adjust the values as needed.

+
+
diff --git a/source/app/blueprints/dashboard/dashboard_routes.py b/source/app/blueprints/dashboard/dashboard_routes.py index 87a5ae4d6..9f45c927a 100644 --- a/source/app/blueprints/dashboard/dashboard_routes.py +++ b/source/app/blueprints/dashboard/dashboard_routes.py @@ -58,6 +58,9 @@ from oic.oauth2.exception import GrantError +log = app.logger + + # CONTENT ------------------------------------------------ dashboard_blueprint = Blueprint( 'index', @@ -93,9 +96,12 @@ def logout(): ctx_less=True, display_in_ui=False ) + except Exception as e: + log.error(f"Error logging out: {e}") + log.warning(f'Will continue to local logout') - track_activity("user '{}' has been logged-out".format(current_user.user), ctx_less=True, display_in_ui=False) logout_user() + track_activity("user '{}' has been logged-out".format(current_user.user), ctx_less=True, display_in_ui=False) session.clear() return redirect(not_authenticated_redirection_url('/')) diff --git a/source/app/blueprints/login/login_routes.py b/source/app/blueprints/login/login_routes.py index 1deaef42a..51c14c31f 100644 --- a/source/app/blueprints/login/login_routes.py +++ b/source/app/blueprints/login/login_routes.py @@ -218,13 +218,15 @@ def oidc_authorise(): email_field = app.config.get("OIDC_MAPPING_EMAIL") username_field = app.config.get("OIDC_MAPPING_USERNAME") - user_login = access_token_resp['id_token'].get(email_field) or access_token_resp['id_token'].get(username_field) + user_login = access_token_resp['id_token'].get(username_field) or access_token_resp['id_token'].get(email_field) user_name = access_token_resp['id_token'].get(email_field) or access_token_resp['id_token'].get(username_field) user = get_user(user_login, 'user') if not user: - if app.config.get("AUTHENTICATION_CREATE_USER_IF_NOT_EXISTS") is False: + log.warning(f"OIDC user {user_login} not found in database") + if app.config.get("AUTHENTICATION_CREATE_USER_IF_NOT_EXIST") is False: + log.warning(f"Authentication is set to not create user if not exists") track_activity( f"OIDC user {user_login} not found in database", ctx_less=True, @@ -232,6 +234,7 @@ def oidc_authorise(): ) return response_error("User not found in IRIS", 404) + log.info(f"Creating OIDC user {user_login} in database") track_activity( f"Creating OIDC user {user_login} in database", ctx_less=True, diff --git a/source/app/blueprints/manage/manage_users.py b/source/app/blueprints/manage/manage_users.py index 3d48cf361..681e0287f 100644 --- a/source/app/blueprints/manage/manage_users.py +++ b/source/app/blueprints/manage/manage_users.py @@ -543,7 +543,8 @@ def view_delete_user(cur_id): track_activity(message="deleted user ID {}".format(cur_id), ctx_less=True) return response_success("Deleted user ID {}".format(cur_id)) - except Exception: + except Exception as e: + print(e) db.session.rollback() track_activity(message="tried to delete active user ID {}".format(cur_id), ctx_less=True) return response_error("Cannot delete active user") diff --git a/source/app/business/cases.py b/source/app/business/cases.py index fe41d1386..58a8b65c3 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -79,7 +79,7 @@ def create(request_json): case = _load(request_data) case.owner_id = current_user.id - case.severity_id = 4 + if not case.severity_id: case.severity_id = 4 if case_template_id and len(case_template_id) > 0: case = case_template_pre_modifier(case, case_template_id) diff --git a/source/app/configuration.py b/source/app/configuration.py index 3585052fd..306fdea9a 100644 --- a/source/app/configuration.py +++ b/source/app/configuration.py @@ -206,7 +206,8 @@ class AuthenticationType(Enum): authentication_type = os.environ.get('IRIS_AUTHENTICATION_TYPE', config.get('IRIS', 'AUTHENTICATION_TYPE', fallback="local")) -authentication_create_user_if_not_exists = config.load('IRIS', 'AUTHENTICATION_CREATE_USER_IF_NOT_EXIST') +authentication_create_user_if_not_exists = config.load('IRIS', 'AUTHENTICATION_CREATE_USER_IF_NOT_EXIST', + fallback="False") tls_root_ca = os.environ.get('TLS_ROOT_CA', config.get('IRIS', 'TLS_ROOT_CA', fallback=None)) @@ -263,7 +264,7 @@ class CeleryConfig: # --------- APP --------- class Config: # Handled by bumpversion - IRIS_VERSION = "v2.4.16" # DO NOT EDIT THIS LINE MANUALLY + IRIS_VERSION = "v2.4.17" # DO NOT EDIT THIS LINE MANUALLY if os.environ.get('IRIS_DEMO_VERSION') is not None and os.environ.get('IRIS_DEMO_VERSION') != 'None': IRIS_VERSION = os.environ.get('IRIS_DEMO_VERSION') diff --git a/source/app/datamgmt/alerts/alerts_db.py b/source/app/datamgmt/alerts/alerts_db.py index 8d3745adb..6d84d747c 100644 --- a/source/app/datamgmt/alerts/alerts_db.py +++ b/source/app/datamgmt/alerts/alerts_db.py @@ -22,7 +22,7 @@ from flask_login import current_user from functools import reduce from operator import and_ -from sqlalchemy import desc, asc, func, tuple_, or_ +from sqlalchemy import desc, asc, func, tuple_, or_, not_ from sqlalchemy.orm import aliased, make_transient, selectinload from typing import List, Tuple, Dict @@ -36,15 +36,41 @@ from app.datamgmt.manage.manage_case_templates_db import get_case_template_by_id, \ case_template_post_modifier from app.datamgmt.states import update_timeline_state +from app.iris_engine.access_control.utils import ac_current_user_has_permission from app.iris_engine.utils.common import parse_bf_date_format from app.models import Cases, EventCategory, Tags, AssetsType, Comments, CaseAssets, alert_assets_association, \ - alert_iocs_association, Ioc, IocLink + alert_iocs_association, Ioc, IocLink, Client from app.models.alerts import Alert, AlertStatus, AlertCaseAssociation, SimilarAlertsCache, AlertResolutionStatus, \ - AlertSimilarity + AlertSimilarity, Severity +from app.models.authorization import Permissions, User from app.schema.marshables import EventSchema, AlertSchema from app.util import add_obj_history_entry +relationship_model_map = { + 'owner': User, + 'severity': Severity, + 'status': AlertStatus, + 'customer': Client, + 'resolution_status': AlertResolutionStatus, + 'cases': Cases, + 'comments': Comments, + 'assets': CaseAssets, + 'iocs': Ioc +} + +RESTRICTED_USER_FIELDS = { + 'password', + 'mfa_secrets', + 'webauthn_credentials', + 'api_key', + 'external_id', + 'ctx_case', + 'ctx_human_case', + 'is_service_account' +} + + def db_list_all_alerts(): """ List all alerts in the database @@ -52,12 +78,59 @@ def db_list_all_alerts(): return db.session.query(Alert).all() +def build_condition(column, operator, value): + if hasattr(column, 'property') and hasattr(column.property, 'local_columns'): + # It's a relationship attribute + fk_cols = list(column.property.local_columns) + if operator in ['in', 'not_in']: + if len(fk_cols) == 1: + # Use the single FK column for the condition + fk_col = fk_cols[0] + if operator == 'in': + return fk_col.in_(value) + else: + return ~fk_col.in_(value) + else: + raise NotImplementedError( + "in_() on a relationship with multiple FK columns not supported. Specify a direct column.") + else: + raise ValueError( + "Non-in operators on relationships require specifying a related model column, e.g., owner.id or assets.asset_name.") + + # If we get here, 'column' should be an actual column, not a relationship. + if operator == 'not': + return column != value + elif operator == 'in': + return column.in_(value) + elif operator == 'not_in': + return ~column.in_(value) + elif operator == 'eq': + return column == value + elif operator == 'like': + return column.ilike(f"%{value}%") + else: + raise ValueError(f"Unsupported operator: {operator}") + + +def combine_conditions(conditions, logical_operator): + if len(conditions) > 1: + if logical_operator == 'or': + return or_(*conditions) + elif logical_operator == 'not': + return not_(and_(*conditions)) + else: # Default to 'and' + return and_(*conditions) + elif conditions: + return conditions[0] + else: + return None + + def get_filtered_alerts( start_date: str = None, end_date: str = None, source_start_date: str = None, source_end_date: str = None, - source_reference: str = None, title: str = None, description: str = None, status: int = None, @@ -71,19 +144,27 @@ def get_filtered_alerts( alert_ids: List[int] = None, assets: List[str] = None, iocs: List[str] = None, - resolution_status: int = None, + resolution_status: List[int] = None, + logical_operator: str = 'and', # Logical operator: 'and', 'or', 'not' page: int = 1, per_page: int = 10, sort: str = 'desc', - current_user_id: int = None -): + current_user_id: int = None, + source_reference=None, + custom_conditions: List[dict] = None, + fields: List[str] = None): """ Get a list of alerts that match the given filter conditions + args: + start_date (datetime): The start date of the alert creation time + end_date (datetime): The end date of the alert creation time + ... + fields (List[str]): The list of fields to include in the output + returns: - dict: A dictionary containing the total count, alerts, and pagination information + dict: Dictionary with pagination info and list of serialized alerts """ - # Build the filter conditions conditions = [] if start_date is not None and end_date is not None: @@ -111,7 +192,10 @@ def get_filtered_alerts( conditions.append(Alert.alert_severity_id == severity) if resolution_status is not None: - conditions.append(Alert.alert_resolution_status_id == resolution_status) + if isinstance(resolution_status, list): + conditions.append(not_(Alert.alert_resolution_status_id.in_(resolution_status))) + else: + conditions.append(Alert.alert_resolution_status_id == resolution_status) if source_reference is not None: conditions.append(Alert.alert_source_ref.like(f'%{source_reference}%')) @@ -149,28 +233,98 @@ def get_filtered_alerts( if isinstance(iocs, list): conditions.append(Alert.iocs.any(Ioc.ioc_value.in_(iocs))) - if current_user_id is not None: + if current_user_id is not None and not ac_current_user_has_permission(Permissions.server_administrator): clients_filters = get_user_clients_id(current_user_id) if clients_filters is not None: conditions.append(Alert.alert_customer_id.in_(clients_filters)) - if len(conditions) > 1: - conditions = [reduce(and_, conditions)] + query = db.session.query( + Alert + ).options( + selectinload(Alert.severity), + selectinload(Alert.status), + selectinload(Alert.customer), + selectinload(Alert.cases), + selectinload(Alert.iocs), + selectinload(Alert.assets) + ) + + # Apply custom conditions if provided + if custom_conditions: + if isinstance(custom_conditions, str): + try: + custom_conditions = json.loads(custom_conditions) + except: + app.app.logger.exception(f"Error parsing custom_conditions: {custom_conditions}") + return + + # Keep track of which relationships we've already joined + joined_relationships = set() + + for custom_condition in custom_conditions: + field_path = custom_condition['field'] + operator = custom_condition['operator'] + value = custom_condition['value'] + + # Check if we need to handle a related field + if '.' in field_path: + relationship_name, related_field_name = field_path.split('.', 1) + + # Ensure the relationship name is known + if relationship_name not in relationship_model_map: + raise ValueError(f"Unknown relationship: {relationship_name}") + + if related_field_name in RESTRICTED_USER_FIELDS: + app.logger.error(f"Access to the field '{related_field_name}' is restricted.") + app.logger.error(f"Suspicious behavior detected for user {current_user.id} - {current_user.user}.") + continue + + related_model = relationship_model_map[relationship_name] + + # Join the relationship if not already joined + if relationship_name not in joined_relationships: + query = query.join(getattr(Alert, relationship_name)) + joined_relationships.add(relationship_name) + + related_field = getattr(related_model, related_field_name, None) + if related_field is None: + raise ValueError( + f"Field '{related_field_name}' not found in related model '{related_model.__name__}'") + + # Build the condition + condition = build_condition(related_field, operator, value) + conditions.append(condition) + else: + # Field belongs to Alert model + field = getattr(Alert, field_path, None) + if field is None: + raise ValueError(f"Field '{field_path}' not found in Alert model") + + condition = build_condition(field, operator, value) + conditions.append(condition) + + # Combine conditions + combined_conditions = combine_conditions(conditions, logical_operator) order_func = desc if sort == "desc" else asc - alert_schema = AlertSchema() + # If fields are provided, use them in the schema + if fields: + try: + alert_schema = AlertSchema(only=fields) + except Exception as e: + app.app.logger.exception(f"Error selecting fields in AlertSchema: {str(e)}") + alert_schema = AlertSchema() + else: + alert_schema = AlertSchema() try: # Query the alerts using the filter conditions - filtered_alerts = db.session.query( - Alert - ).filter( - *conditions - ).options( - selectinload(Alert.severity), selectinload(Alert.status), selectinload(Alert.customer), selectinload(Alert.cases), - selectinload(Alert.iocs), selectinload(Alert.assets) - ).order_by( + + if combined_conditions is not None: + query = query.filter(combined_conditions) + + filtered_alerts = query.order_by( order_func(Alert.alert_source_event_time) ).paginate(page=page, per_page=per_page, error_out=False) @@ -839,7 +993,7 @@ def delete_similar_alert_cache(alert_id): db.session.commit() -def delete_related_alerts_cache(alert_id): +def delete_related_alert_cache(alert_id): """ Delete the related alerts cache @@ -871,6 +1025,25 @@ def delete_similar_alerts_cache(alert_ids: List[int]): db.session.commit() +def delete_related_alerts_cache(alert_ids: List[int]): + """ + Delete the related alerts cache + + args: + alert_ids (List(int)): The ID of the alert + + returns: + None + """ + AlertSimilarity.query.filter( + or_( + AlertSimilarity.alert_id.in_(alert_ids), + AlertSimilarity.similar_alert_id.in_(alert_ids) + ) + ).delete() + db.session.commit() + + def get_related_alerts(customer_id, assets, iocs, details=False): """ Check if an alert is related to another alert @@ -1303,6 +1476,8 @@ def delete_alerts(alert_ids: List[int]) -> tuple[bool, str]: delete_similar_alerts_cache(alert_ids) + delete_related_alerts_cache(alert_ids) + remove_alerts_from_assets_by_ids(alert_ids) remove_alerts_from_iocs_by_ids(alert_ids) remove_case_alerts_by_ids(alert_ids) diff --git a/source/app/datamgmt/manage/manage_users_db.py b/source/app/datamgmt/manage/manage_users_db.py index 726c54f50..266f753d0 100644 --- a/source/app/datamgmt/manage/manage_users_db.py +++ b/source/app/datamgmt/manage/manage_users_db.py @@ -31,7 +31,7 @@ from app.iris_engine.access_control.utils import ac_get_detailed_effective_permissions_from_groups from app.iris_engine.access_control.utils import ac_remove_case_access_from_user from app.iris_engine.access_control.utils import ac_set_case_access_for_user -from app.models import Cases, Client +from app.models import Cases, Client, UserActivity from app.models.authorization import CaseAccessLevel, UserClient from app.models.authorization import Group from app.models.authorization import Organisation @@ -699,6 +699,10 @@ def update_user(user: User, name: str = None, email: str = None, password: str = def delete_user(user_id): + # Migrate the user activity to a shadow user + + UserActivity.query.filter(UserActivity.user_id == user_id).update({UserActivity.user_id: None}) + UserCaseAccess.query.filter(UserCaseAccess.user_id == user_id).delete() UserOrganisation.query.filter(UserOrganisation.user_id == user_id).delete() UserGroup.query.filter(UserGroup.user_id == user_id).delete() diff --git a/source/app/models/cases.py b/source/app/models/cases.py index cb253b99b..329f569cd 100644 --- a/source/app/models/cases.py +++ b/source/app/models/cases.py @@ -94,7 +94,8 @@ def __init__(self, user=None, custom_attributes=None, classification_id=None, - state_id=None + state_id=None, + severity_id=None ): self.name = name[:200] if name else None, self.soc_id = soc_id, @@ -111,7 +112,8 @@ def __init__(self, self.case_uuid = uuid.uuid4() self.status_id = 0 self.classification_id = classification_id - self.state_id = state_id + self.state_id = state_id, + self.severity_id = severity_id def save(self): """ diff --git a/source/app/schema/marshables.py b/source/app/schema/marshables.py index 94eb41190..e14a29210 100644 --- a/source/app/schema/marshables.py +++ b/source/app/schema/marshables.py @@ -1542,6 +1542,7 @@ class CaseSchema(ma.SQLAlchemyAutoSchema): initial_date: Optional[datetime.datetime] = auto_field('initial_date', required=False) classification_id: Optional[int] = auto_field('classification_id', required=False, allow_none=True) reviewer_id: Optional[int] = auto_field('reviewer_id', required=False, allow_none=True) + severity_id: Optional[int] = auto_field('severity_id', required=False, allow_none=True) class Meta: model = Cases diff --git a/source/app/static/assets/js/iris/alerts.js b/source/app/static/assets/js/iris/alerts.js index 02c2db2de..4ae5bb92e 100644 --- a/source/app/static/assets/js/iris/alerts.js +++ b/source/app/static/assets/js/iris/alerts.js @@ -1,4 +1,5 @@ let sortOrder ; +let editor = null; function objectToQueryString(obj) { return Object.keys(obj) @@ -811,7 +812,11 @@ function addTagFilter(this_object) { function getFiltersFromUrl() { const formData = new FormData($('#alertFilterForm')[0]); - return Object.fromEntries(formData.entries()); + const filters = Object.fromEntries(formData.entries()); + + filters.custom_conditions = editor.getValue(); + + return filters; } function alertResolutionToARC(resolution, alert_id) { @@ -1341,11 +1346,17 @@ async function updateAlerts(page, per_page, filters = {}, paging=false){ filters = getFiltersFromUrl(); } + filters.custom_conditions = editor.getValue(); + const alertsContainer = $('.alerts-container'); alertsContainer.html('

Retrieving alerts...

'); const filterString = objectToQueryString(filters); - const data = await fetchAlerts(page, per_page, filterString, sortOrder); + const data = await fetchAlerts(page, per_page, filterString, sortOrder).catch((error) => { + notify_error('Failed to fetch alerts'); + alertsContainer.html('

Oops error loading the alerts - Check logs

'); + console.error(error); + }); if (!notify_auto_api(data, true)) { return; @@ -1484,6 +1495,8 @@ function refreshAlerts(){ const formData = new FormData($('#alertFilterForm')[0]); const filters = Object.fromEntries(formData.entries()); + filters.custom_conditions = editor.getValue(); + updateAlerts(page_number, per_page, filters) .then(() => { notify_success('Refreshed'); @@ -1546,6 +1559,7 @@ $('#resetFilters').on('click', function () { } }); + editor.setValue("", 1); // Reset the saved filters dropdown resetSavedFilters(null); @@ -1793,6 +1807,8 @@ $('#saveFilterButton').on('click', function () { const filterDescription = $('#filterDescription').val(); const filterIsPrivate = $('#filterIsPrivate').prop('checked'); + filterData.custom_conditions = editor.getValue(); + if (!filterName) return; const url = '/filters/add'; @@ -1926,6 +1942,12 @@ function setFormValuesFromUrl() { queryParams.forEach((value, key) => { const input = form.find(`[name="${key}"]`); + if (key === 'custom_conditions') { + // If there's a custom_conditions param, load it into the ACE editor + editor.setValue(value, 1); // 1 = move cursor to start + return; + } + if (input.length > 0) { if (input.prop('type') === 'checkbox') { input.prop('checked', value in ['true', 'y', 'yes', '1', 'on']); @@ -2110,6 +2132,75 @@ $(document).ready(function () { }); } + + editor = ace.edit('custom_conditions'); + if ($("#custom_conditions").attr("data-theme") != "dark") { + editor.setTheme("ace/theme/tomorrow"); + } else { + editor.setTheme("ace/theme/iris_night"); + } + editor.session.setMode("ace/mode/json"); + editor.renderer.setShowGutter(true); + editor.setOption("showLineNumbers", true); + editor.setOption("showPrintMargin", false); + editor.setOption("displayIndentGuides", true); + editor.setOption("maxLines", "Infinity"); + editor.setOption("minLines", "2"); + editor.setOption("autoScrollEditorIntoView", true); + editor.session.setUseWrapMode(true); + editor.setOption("indentedSoftWrap", false); + editor.renderer.setScrollMargin(8, 5) + editor.setOption("enableBasicAutocompletion", true); + + editor.setOption("enableBasicAutocompletion", true); + editor.setOption("enableLiveAutocompletion", true); + + // Use the langTools from ACE for autocompletion + let langTools = ace.require("ace/ext/language_tools"); + + // Define a custom completer + let customCompleter = { + getCompletions: function(editor, session, pos, prefix, callback) { + const completions = [ + { caption: '"field": "alert_title"', value: '"field": "alert_title"', meta: "field" }, + { caption: '"field": "alert_description"', value: '"field": "alert_description"', meta: "field" }, + { caption: '"field": "alert_source"', value: '"field": "alert_source"', meta: "field" }, + { caption: '"field": "alert_tags"', value: '"field": "alert_tags"', meta: "field" }, + { caption: '"field": "alert_status_id"', value: '"field": "alert_status_id"', meta: "field" }, + { caption: '"field": "alert_severity_id"', value: '"field": "alert_severity_id"', meta: "field" }, + { caption: '"field": "alert_classification_id"', value: '"field": "alert_classification_id"', meta: "field" }, + { caption: '"field": "alert_customer_id"', value: '"field": "alert_customer_id"', meta: "field" }, + { caption: '"field": "source_start_date"', value: '"field": "source_start_date"', meta: "field" }, + { caption: '"field": "source_end_date"', value: '"field": "source_end_date"', meta: "field" }, + { caption: '"field": "creation_start_date"', value: '"field": "creation_start_date"', meta: "field" }, + { caption: '"field": "creation_end_date"', value: '"field": "creation_end_date"', meta: "field" }, + { caption: '"field": "alert_assets"', value: '"field": "alert_assets"', meta: "field" }, + { caption: '"field": "alert_iocs"', value: '"field": "alert_iocs"', meta: "field" }, + { caption: '"field": "alert_ids"', value: '"field": "alert_ids"', meta: "field" }, + { caption: '"field": "source_reference"', value: '"field": "source_reference"', meta: "field" }, + { caption: '"field": "case_id"', value: '"field": "case_id"', meta: "field" }, + { caption: '"field": "alert_owner_id"', value: '"field": "alert_owner_id"', meta: "field" }, + { caption: '"field": "alert_resolution_id"', value: '"field": "alert_resolution_id"', meta: "field" }, + { caption: '"operator": "in"', value: '"operator": "in"', meta: "operator" }, + { caption: '"operator": "not_in"', value: '"operator": "not_in"', meta: "operator" }, + { caption: '"operator": "eq"', value: '"operator": "eq"', meta: "operator" }, + { caption: '"operator": "like"', value: '"operator": "like"', meta: "operator" }, + { caption: '"value": [1]', value: '"value": [1]', meta: "value" } + ]; + + // Filter the completions based on the current prefix if desired + let filtered = completions; + if (prefix) { + filtered = completions.filter(item => item.caption.toLowerCase().includes(prefix.toLowerCase())); + } + + callback(null, filtered); + } + }; + + // Add the custom completer to ACE + langTools.addCompleter(customCompleter); + fetchSavedFilters() .then(() => { setFormValuesFromUrl(); diff --git a/source/app/static/assets/js/iris/case.notes.js b/source/app/static/assets/js/iris/case.notes.js index 481a138f6..7bc3f19cf 100644 --- a/source/app/static/assets/js/iris/case.notes.js +++ b/source/app/static/assets/js/iris/case.notes.js @@ -432,7 +432,7 @@ async function note_detail(id) { $('.note').removeClass('note-highlight'); $('#note-' + id).addClass('note-highlight'); - $('#object_comments_number').text(data.data.comments.length); + $('#object_comments_number').text(data.data.comments.length > 0 ? data.data.comments.length: ''); $('#content_last_saved_by').text(''); $('#content_typing').text(''); $('#last_saved').removeClass('btn-danger').addClass('btn-success'); diff --git a/source/requirements.txt b/source/requirements.txt index 8a70664dd..b4209c480 100644 --- a/source/requirements.txt +++ b/source/requirements.txt @@ -11,7 +11,7 @@ Flask-Caching==1.10.1 marshmallow==3.20.1 marshmallow-sqlalchemy==0.30.0 gunicorn==20.1.0 -psycopg2-binary==2.9.1 +psycopg2-binary==2.9.10 pyunpack==0.2.2 packaging==21.3 requests==2.31.0