Compare commits

..

No commits in common. "99f9751e22c7ba14012ac1c106d1f50ab6b49e33" and "a21549f58762d7258f3495851ddfa9d1ac43dbb8" have entirely different histories.

5 changed files with 45 additions and 124 deletions

View file

@ -2,6 +2,7 @@
import functools import functools
import time import time
from collections import defaultdict
from pathlib import Path from pathlib import Path
import json import json
import re import re
@ -9,7 +10,7 @@ from typing import Union
import jq import jq
import db import db
from inject_evaluator import eval_data_filtering, eval_query_mirror, eval_query_search from inject_evaluator import eval_data_filtering, eval_query_mirror
import misp_api import misp_api
import config import config
from config import logger from config import logger
@ -311,9 +312,9 @@ def get_progress():
return progress return progress
async def check_inject(user_id: int, inject: dict, data: dict, context: dict) -> bool: def check_inject(user_id: int, inject: dict, data: dict, context: dict) -> bool:
for inject_evaluation in inject['inject_evaluation']: for inject_evaluation in inject['inject_evaluation']:
success = await inject_checker_router(user_id, inject_evaluation, data, context) success = inject_checker_router(user_id, inject_evaluation, data, context)
if not success: if not success:
logger.info(f"Task not completed: {inject['uuid']}") logger.info(f"Task not completed: {inject['uuid']}")
return False return False
@ -337,39 +338,35 @@ def is_valid_evaluation_context(user_id: int, inject_evaluation: dict, data: dic
return False return False
return False return False
async def inject_checker_router(user_id: int, inject_evaluation: dict, data: dict, context: dict) -> bool: def inject_checker_router(user_id: int, inject_evaluation: dict, data: dict, context: dict) -> bool:
if not is_valid_evaluation_context(user_id, inject_evaluation, data, context): if not is_valid_evaluation_context(user_id, inject_evaluation, data, context):
return False return False
if 'evaluation_strategy' not in inject_evaluation: if 'evaluation_strategy' not in inject_evaluation:
return False return False
data_to_validate = await get_data_to_validate(user_id, inject_evaluation, data) data_to_validate = get_data_to_validate(user_id, inject_evaluation, data)
if data_to_validate is None: if data_to_validate is None:
logger.debug('Could not fetch data to validate') logger.debug('Could not fetch data to validate')
return False return False
if inject_evaluation['evaluation_strategy'] == 'data_filtering': if inject_evaluation['evaluation_strategy'] == 'data_filtering':
return eval_data_filtering(user_id, inject_evaluation, data_to_validate, context) return eval_data_filtering(user_id, inject_evaluation, data_to_validate)
elif inject_evaluation['evaluation_strategy'] == 'query_mirror': elif inject_evaluation['evaluation_strategy'] == 'query_mirror':
expected_data = data_to_validate['expected_data'] expected_data = data_to_validate['expected_data']
data_to_validate = data_to_validate['data_to_validate'] data_to_validate = data_to_validate['data_to_validate']
return eval_query_mirror(user_id, expected_data, data_to_validate, context) return eval_query_mirror(user_id, expected_data, data_to_validate)
elif inject_evaluation['evaluation_strategy'] == 'query_search':
return eval_query_search(user_id, inject_evaluation, data_to_validate, context)
return False return False
async def get_data_to_validate(user_id: int, inject_evaluation: dict, data: dict) -> Union[dict, list, str, None]: def get_data_to_validate(user_id: int, inject_evaluation: dict, data: dict) -> Union[dict, list, str, None]:
data_to_validate = None data_to_validate = None
if inject_evaluation['evaluation_strategy'] == 'data_filtering': if inject_evaluation['evaluation_strategy'] == 'data_filtering':
event_id = parse_event_id_from_log(data) event_id = parse_event_id_from_log(data)
data_to_validate = await fetch_data_for_data_filtering(event_id=event_id) data_to_validate = fetch_data_for_data_filtering(event_id=event_id)
elif inject_evaluation['evaluation_strategy'] == 'query_mirror': elif inject_evaluation['evaluation_strategy'] == 'query_mirror':
perfomed_query = parse_performed_query_from_log(data) perfomed_query = parse_performed_query_from_log(data)
data_to_validate = await fetch_data_for_query_mirror(user_id, inject_evaluation, perfomed_query) data_to_validate = fetch_data_for_query_mirror(user_id, inject_evaluation, perfomed_query)
elif inject_evaluation['evaluation_strategy'] == 'query_search':
data_to_validate = await fetch_data_for_query_search(user_id, inject_evaluation)
return data_to_validate return data_to_validate
@ -415,14 +412,14 @@ def parse_performed_query_from_log(data: dict) -> Union[dict, None]:
return None return None
async def fetch_data_for_data_filtering(event_id=None) -> Union[None, dict]: def fetch_data_for_data_filtering(event_id=None) -> Union[None, dict]:
data = None data = None
if event_id is not None: if event_id is not None:
data = await misp_api.getEvent(event_id) data = misp_api.getEvent(event_id)
return data return data
async def fetch_data_for_query_mirror(user_id: int, inject_evaluation: dict, perfomed_query: dict) -> Union[None, dict]: def fetch_data_for_query_mirror(user_id: int, inject_evaluation: dict, perfomed_query: dict) -> Union[None, dict]:
data = None data = None
authkey = db.USER_ID_TO_AUTHKEY_MAPPING[user_id] authkey = db.USER_ID_TO_AUTHKEY_MAPPING[user_id]
if perfomed_query is not None: if perfomed_query is not None:
@ -432,8 +429,8 @@ async def fetch_data_for_query_mirror(user_id: int, inject_evaluation: dict, per
expected_method = query_context['request_method'] expected_method = query_context['request_method']
expected_url = query_context['url'] expected_url = query_context['url']
expected_payload = inject_evaluation['parameters'][0] expected_payload = inject_evaluation['parameters'][0]
expected_data = await misp_api.doRestQuery(authkey, expected_method, expected_url, expected_payload) expected_data = misp_api.doRestQuery(authkey, expected_method, expected_url, expected_payload)
data_to_validate = await misp_api.doRestQuery(authkey, perfomed_query['request_method'], perfomed_query['url'], perfomed_query['payload']) data_to_validate = misp_api.doRestQuery(authkey, perfomed_query['request_method'], perfomed_query['url'], perfomed_query['payload'])
data = { data = {
'expected_data' : expected_data, 'expected_data' : expected_data,
'data_to_validate' : data_to_validate, 'data_to_validate' : data_to_validate,
@ -441,20 +438,8 @@ async def fetch_data_for_query_mirror(user_id: int, inject_evaluation: dict, per
return data return data
async def fetch_data_for_query_search(user_id: int, inject_evaluation: dict) -> Union[None, dict]:
authkey = db.USER_ID_TO_AUTHKEY_MAPPING[user_id]
if 'evaluation_context' not in inject_evaluation and 'query_context' not in inject_evaluation['evaluation_context']:
return None
query_context = inject_evaluation['evaluation_context']['query_context']
search_method = query_context['request_method']
search_url = query_context['url']
search_payload = query_context['payload']
search_data = await misp_api.doRestQuery(authkey, search_method, search_url, search_payload)
return search_data
@debounce_check_active_tasks(debounce_seconds=2) @debounce_check_active_tasks(debounce_seconds=2)
async def check_active_tasks(user_id: int, data: dict, context: dict) -> bool: def check_active_tasks(user_id: int, data: dict, context: dict) -> bool:
succeeded_once = False succeeded_once = False
available_tasks = get_available_tasks_for_user(user_id) available_tasks = get_available_tasks_for_user(user_id)
for task_uuid in available_tasks: for task_uuid in available_tasks:
@ -462,7 +447,7 @@ async def check_active_tasks(user_id: int, data: dict, context: dict) -> bool:
if inject['exercise_uuid'] not in db.SELECTED_EXERCISES: if inject['exercise_uuid'] not in db.SELECTED_EXERCISES:
continue continue
logger.debug(f"[{task_uuid}] :: checking: {inject['name']}") logger.debug(f"[{task_uuid}] :: checking: {inject['name']}")
completed = await check_inject(user_id, inject, data, context) completed = check_inject(user_id, inject, data, context)
if completed: if completed:
succeeded_once = True succeeded_once = True
return succeeded_once return succeeded_once

View file

@ -138,15 +138,7 @@
{ {
"parameters": [ "parameters": [
{ {
".response[].Event.event_creator_email": { ".Event.info": {
"comparison": "equals",
"values": [
"{{user_email}}"
]
}
},
{
".response[].Event.info": {
"comparison": "contains", "comparison": "contains",
"values": [ "values": [
"event", "event",
@ -156,17 +148,9 @@
} }
], ],
"result": "MISP Event created", "result": "MISP Event created",
"evaluation_strategy": "query_search", "evaluation_strategy": "data_filtering",
"evaluation_context": { "evaluation_context": {
"request_is_rest": true, "request_is_rest": true
"query_context": {
"url": "/events/restSearch",
"request_method": "POST",
"payload": {
"timestamp": "10d",
"eventinfo": "%API%"
}
}
}, },
"score_range": [ "score_range": [
0, 0,

View file

@ -6,6 +6,7 @@ import operator
from config import logger from config import logger
# .Event.Attribute[] | select(.value == "evil.exe") | .Tag
def jq_extract(path: str, data: dict, extract_type='first'): def jq_extract(path: str, data: dict, extract_type='first'):
query = jq.compile(path).input_value(data) query = jq.compile(path).input_value(data)
try: try:
@ -14,42 +15,28 @@ def jq_extract(path: str, data: dict, extract_type='first'):
return None return None
# Replace the substring `{{variable}}` by context[variable] in the provided string
def apply_replacement_from_context(string: str, context: dict) -> str:
replacement_regex = r"{{(\w+)}}"
if r'{{' not in string and r'}}' not in string:
return string
matches = re.fullmatch(replacement_regex, string, re.MULTILINE)
if not matches:
return string
subst_str = matches.groups()[0]
subst = str(context.get(subst_str, ''))
return re.sub(replacement_regex, subst, string)
## ##
## Data Filtering ## Data Filtering
## ##
def condition_satisfied(evaluation_config: dict, data_to_validate: Union[dict, list, str], context: dict) -> bool: def condition_satisfied(evaluation_config: dict, data_to_validate: Union[dict, list, str]) -> bool:
if type(data_to_validate) is bool: if type(data_to_validate) is bool:
data_to_validate = "1" if data_to_validate else "0" data_to_validate = "1" if data_to_validate else "0"
if type(data_to_validate) is str: if type(data_to_validate) is str:
return eval_condition_str(evaluation_config, data_to_validate, context) return eval_condition_str(evaluation_config, data_to_validate)
elif type(data_to_validate) is list: elif type(data_to_validate) is list:
return eval_condition_list(evaluation_config, data_to_validate, context) return eval_condition_list(evaluation_config, data_to_validate)
elif type(data_to_validate) is dict: elif type(data_to_validate) is dict:
# Not sure how we could have condition on this # Not sure how we could have condition on this
return eval_condition_dict(evaluation_config, data_to_validate, context) return eval_condition_dict(evaluation_config, data_to_validate)
return False return False
def eval_condition_str(evaluation_config: dict, data_to_validate: str, context: dict) -> bool: def eval_condition_str(evaluation_config: dict, data_to_validate: str) -> bool:
comparison_type = evaluation_config['comparison'] comparison_type = evaluation_config['comparison']
values = evaluation_config['values'] values = evaluation_config['values']
if len(values) == 0: if len(values) == 0:
return False return False
values = [apply_replacement_from_context(v, context) for v in values]
if comparison_type == 'contains': if comparison_type == 'contains':
values = [v.lower() for v in values] values = [v.lower() for v in values]
@ -69,7 +56,7 @@ def eval_condition_str(evaluation_config: dict, data_to_validate: str, context:
return False return False
def eval_condition_list(evaluation_config: dict, data_to_validate: str, context: dict) -> bool: def eval_condition_list(evaluation_config: dict, data_to_validate: str) -> bool:
comparison_type = evaluation_config['comparison'] comparison_type = evaluation_config['comparison']
values = evaluation_config['values'] values = evaluation_config['values']
comparators = { comparators = {
@ -82,7 +69,7 @@ def eval_condition_list(evaluation_config: dict, data_to_validate: str, context:
if len(values) == 0: if len(values) == 0:
return False return False
values = [apply_replacement_from_context(v, context) for v in values]
if comparison_type == 'contains' or comparison_type == 'equals': if comparison_type == 'contains' or comparison_type == 'equals':
data_to_validate_set = set(data_to_validate) data_to_validate_set = set(data_to_validate)
@ -115,7 +102,7 @@ def eval_condition_list(evaluation_config: dict, data_to_validate: str, context:
return False return False
def eval_condition_dict(evaluation_config: dict, data_to_validate: str, context: dict) -> bool: def eval_condition_dict(evaluation_config: dict, data_to_validate: str) -> bool:
comparison_type = evaluation_config['comparison'] comparison_type = evaluation_config['comparison']
values = evaluation_config['values'] values = evaluation_config['values']
comparators = { comparators = {
@ -126,10 +113,6 @@ def eval_condition_dict(evaluation_config: dict, data_to_validate: str, context:
'=': operator.eq, '=': operator.eq,
} }
if len(values) == 0:
return False
values = [apply_replacement_from_context(v, context) for v in values]
comparison_type = evaluation_config['comparison'] comparison_type = evaluation_config['comparison']
if comparison_type == 'contains': if comparison_type == 'contains':
pass pass
@ -146,31 +129,21 @@ def eval_condition_dict(evaluation_config: dict, data_to_validate: str, context:
return False return False
def eval_data_filtering(user_id: int, inject_evaluation: dict, data: dict, context: dict) -> bool: def eval_data_filtering(user_id: int, inject_evaluation: dict, data: dict) -> bool:
for evaluation_params in inject_evaluation['parameters']: for evaluation_params in inject_evaluation['parameters']:
for evaluation_path, evaluation_config in evaluation_params.items(): for evaluation_path, evaluation_config in evaluation_params.items():
evaluation_path = apply_replacement_from_context(evaluation_path, context)
data_to_validate = jq_extract(evaluation_path, data, evaluation_config.get('extract_type', 'first')) data_to_validate = jq_extract(evaluation_path, data, evaluation_config.get('extract_type', 'first'))
if data_to_validate is None: if data_to_validate is None:
logger.debug('Could not extract data') logger.debug('Could not extract data')
return False return False
if not condition_satisfied(evaluation_config, data_to_validate, context): if not condition_satisfied(evaluation_config, data_to_validate):
return False return False
return True return True
## ##
## Query mirror ## Query comparison
## ##
def eval_query_mirror(user_id: int, expected_data, data_to_validate, context: dict) -> bool: def eval_query_mirror(user_id: int, expected_data, data_to_validate) -> bool:
return expected_data == data_to_validate return expected_data == data_to_validate
##
## Query search
##
def eval_query_search(user_id: int, inject_evaluation: dict, data: dict, context: dict) -> bool:
return eval_data_filtering(user_id, inject_evaluation, data, context)

View file

@ -146,8 +146,8 @@ async def handleMessage(topic, s, message):
user_id = notification_model.get_user_id(data) user_id = notification_model.get_user_id(data)
if user_id is not None: if user_id is not None:
if exercise_model.is_accepted_query(data): if exercise_model.is_accepted_query(data):
context = get_context(topic, user_id, data) context = get_context(data)
succeeded_once = await exercise_model.check_active_tasks(user_id, data, context) succeeded_once = exercise_model.check_active_tasks(user_id, data, context)
if succeeded_once: if succeeded_once:
await sendRefreshScore() await sendRefreshScore()
@ -157,13 +157,8 @@ async def sendRefreshScore():
await sio.emit('refresh_score') await sio.emit('refresh_score')
def get_context(topic: str, user_id: int, data: dict) -> dict: def get_context(data: dict) -> dict:
context = { context = {}
'zmq_topic': topic,
'user_id': user_id,
'user_email': db.USER_ID_TO_EMAIL_MAPPING.get(user_id, None),
'user_authkey': db.USER_ID_TO_AUTHKEY_MAPPING.get(user_id, None),
}
if 'Log' in data: if 'Log' in data:
if 'request_is_rest' in data['Log']: if 'request_is_rest' in data['Log']:
context['request_is_rest'] = data['Log']['request_is_rest'] context['request_is_rest'] = data['Log']['request_is_rest']
@ -205,11 +200,10 @@ async def forward_zmq_to_socketio():
while True: while True:
message = await zsocket.recv_string() message = await zsocket.recv_string()
topic, s, m = message.partition(" ") topic, s, m = message.partition(" ")
await handleMessage(topic, s, m)
try: try:
ZMQ_MESSAGE_COUNT += 1 ZMQ_MESSAGE_COUNT += 1
ZMQ_LAST_TIME = time.time() ZMQ_LAST_TIME = time.time()
# await handleMessage(topic, s, m) await handleMessage(topic, s, m)
except Exception as e: except Exception as e:
logger.error('Error handling message %s', e) logger.error('Error handling message %s', e)

View file

@ -1,21 +1,9 @@
<script setup> <script setup>
import { ref, onMounted } from "vue" import { computed } from "vue"
import { socketConnected, zmqLastTime } from "@/socket"; import { socketConnected, zmqLastTime } from "@/socket";
const zmqLastTimeSecond = ref('?') const zmqLastTimeSecond = computed(() => {
return parseInt(((new Date()).getTime() - zmqLastTime.value * 1000) / 1000)
function refreshLastTime() {
if (zmqLastTime.value !== false) {
zmqLastTimeSecond.value = parseInt(((new Date()).getTime() - zmqLastTime.value * 1000) / 1000)
} else {
zmqLastTimeSecond.value = '?'
}
}
onMounted(() => {
setInterval(() => {
refreshLastTime()
}, 1000)
}) })
</script> </script>
@ -25,21 +13,18 @@
<span :class="{ <span :class="{
'px-2 rounded-md inline-block w-48 leading-4': true, 'px-2 rounded-md inline-block w-48 leading-4': true,
'text-slate-900 dark:text-slate-400': socketConnected, 'text-slate-900 dark:text-slate-400': socketConnected,
'text-slate-50 bg-red-600 px-2 py-1': !socketConnected, 'text-slate-50 bg-red-600': !socketConnected,
}"> }">
<span class="mr-1">Socket.IO:</span> <span class="mr-1">Socket.IO:</span>
<span v-show="socketConnected" class="font-semibold text-green-600 dark:text-green-400">Connected</span> <span v-show="socketConnected" class="font-semibold text-green-600 dark:text-green-400">Connected</span>
<span v-show="!socketConnected" class="font-semibold text-slate-50">Disconnected</span> <span v-show="!socketConnected" class="font-semibold text-slate-50">Disconnected</span>
</span> </span>
<span <span class="text-xs font-thin leading-3 inline-block text-center">
v-if="socketConnected"
class="text-xs font-thin leading-3 inline-block text-center"
>
<template v-if="zmqLastTimeSecond == 0"> <template v-if="zmqLastTimeSecond == 0">
online online
</template> </template>
<template v-else> <template v-else>
Last keep-alive: {{ zmqLastTimeSecond }}s ago {{ zmqLastTimeSecond }}s
</template> </template>
</span> </span>
</span> </span>