new: [app] Better setting split and added misp setting remediation

This commit is contained in:
Sami Mokaddem 2024-07-10 10:14:44 +02:00
parent 81bb48ff54
commit 2cd4820f1c
11 changed files with 157 additions and 70 deletions

39
appConfig.py Normal file
View file

@ -0,0 +1,39 @@
live_logs_accepted_scope = {
'events': ['add', 'edit', 'delete', 'restSearch',],
'attributes': ['add', 'add_attachment', 'edit', 'revise_object', 'delete', 'restSearch',],
'eventReports': ['add', 'edit', 'delete',],
'tags': '*',
}
user_activity_accepted_scope = {
'events': ['view', 'add', 'edit', 'delete', 'restSearch',],
'attributes': ['add', 'add_attachment', 'edit', 'delete', 'restSearch',],
'objects': ['add', 'edit', 'revise_object', 'delete',],
'eventReports': ['view', 'add', 'edit', 'delete',],
'tags': '*',
}
misp_settings = {
'Plugin.ZeroMQ_enable': True,
'Plugin.ZeroMQ_audit_notifications_enable': True,
'Plugin.ZeroMQ_event_notifications_enable': True,
'Plugin.ZeroMQ_attribute_notifications_enable': True,
'MISP.log_new_audit': False,
'MISP.log_paranoid': True,
'MISP.log_paranoid_skip_db': True,
'MISP.log_paranoid_include_post_body': True,
'MISP.log_auth': True,
'Security.allow_unsafe_cleartext_apikey_logging': True,
}
import logging
logger = logging.getLogger('misp-exercise-dashboard')
format = '[%(levelname)s] %(asctime)s - %(message)s'
formatter = logging.Formatter(format)
logging.basicConfig(filename='misp-exercise-dashboard.log', encoding='utf-8', level=logging.DEBUG, format=format)
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)

View file

@ -6,28 +6,3 @@ zmq_url = 'tcp://localhost:50000'
misp_url = 'https://localhost/' misp_url = 'https://localhost/'
misp_apikey = 'FI4gCRghRZvLVjlLPLTFZ852x2njkkgPSz0zQ3E0' misp_apikey = 'FI4gCRghRZvLVjlLPLTFZ852x2njkkgPSz0zQ3E0'
misp_skipssl = True misp_skipssl = True
live_logs_accepted_scope = {
'events': ['add', 'edit', 'delete', 'restSearch',],
'attributes': ['add', 'add_attachment', 'edit', 'revise_object', 'delete', 'restSearch',],
'eventReports': ['add', 'edit', 'delete',],
'tags': '*',
}
user_activity_accepted_scope = {
'events': ['view', 'add', 'edit', 'delete', 'restSearch',],
'attributes': ['add', 'add_attachment', 'edit', 'delete', 'restSearch',],
'objects': ['add', 'edit', 'revise_object', 'delete',],
'eventReports': ['view', 'add', 'edit', 'delete',],
'tags': '*',
}
import logging
logger = logging.getLogger('misp-exercise-dashboard')
format = '[%(levelname)s] %(asctime)s - %(message)s'
formatter = logging.Formatter(format)
logging.basicConfig(filename='misp-exercise-dashboard.log', encoding='utf-8', level=logging.DEBUG, format=format)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)

View file

@ -11,8 +11,7 @@ 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, eval_query_search
import misp_api import misp_api
import config from appConfig import logger
from config import logger
ACTIVE_EXERCISES_DIR = "active_exercises" ACTIVE_EXERCISES_DIR = "active_exercises"

View file

@ -3,7 +3,7 @@ from typing import Union
import jq import jq
import re import re
import operator import operator
from config import logger from appConfig import logger
def jq_extract(path: str, data: dict, extract_type='first'): def jq_extract(path: str, data: dict, extract_type='first'):

View file

@ -11,7 +11,8 @@ from requests_cache import CachedSession
from requests.packages.urllib3.exceptions import InsecureRequestWarning # type: ignore from requests.packages.urllib3.exceptions import InsecureRequestWarning # type: ignore
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
from config import misp_url, misp_apikey, misp_skipssl, logger from config import misp_url, misp_apikey, misp_skipssl
from appConfig import logger, misp_settings
requestSession = CachedSession(cache_name='misp_cache', expire_after=timedelta(seconds=5)) requestSession = CachedSession(cache_name='misp_cache', expire_after=timedelta(seconds=5))
adapterCache = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50) adapterCache = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)
@ -83,20 +84,24 @@ async def getVersion() -> Union[None, dict]:
async def getSettings() -> Union[None, dict]: async def getSettings() -> Union[None, dict]:
SETTING_TO_QUERY = [
'Plugin.ZeroMQ_enable',
'Plugin.ZeroMQ_audit_notifications_enable',
'Plugin.ZeroMQ_event_notifications_enable',
'Plugin.ZeroMQ_attribute_notifications_enable',
'MISP.log_paranoid',
'MISP.log_paranoid_skip_db',
'MISP.log_paranoid_include_post_body',
'MISP.log_auth',
'Security.allow_unsafe_cleartext_apikey_logging',
]
settings = await get(f'/servers/serverSettings.json') settings = await get(f'/servers/serverSettings.json')
if not settings: if not settings:
return None return None
return { data = {}
setting['setting']: setting['value'] for setting in settings.get('finalSettings', []) if setting['setting'] in SETTING_TO_QUERY for settingName, expectedSettingValue in misp_settings.items():
data[settingName] = {
'expected_value': expectedSettingValue,
'value': None
} }
for setting in settings.get('finalSettings', []):
if setting['setting'] in misp_settings:
data[setting['setting']]['value'] = setting['value']
return data
async def remediateSetting(setting) ->dict:
if setting in misp_settings:
payload = {
'value': misp_settings[setting],
}
return await post(f'/servers/serverSettingsEdit/{setting}', payload)

View file

@ -5,6 +5,7 @@ import re
from typing import Union from typing import Union
import db import db
import config import config
import appConfig
from urllib.parse import parse_qs from urllib.parse import parse_qs
@ -192,10 +193,10 @@ def is_accepted_notification(notification) -> bool:
return False return False
scope, action = get_scope_action_from_url(notification['url']) scope, action = get_scope_action_from_url(notification['url'])
if scope in config.live_logs_accepted_scope: if scope in appConfig.live_logs_accepted_scope:
if config.live_logs_accepted_scope == '*': if appConfig.live_logs_accepted_scope == '*':
return True return True
elif action in config.live_logs_accepted_scope[scope]: elif action in appConfig.live_logs_accepted_scope[scope]:
return True return True
return False return False
@ -209,9 +210,9 @@ def is_accepted_user_activity(notification) -> bool:
return False return False
scope, action = get_scope_action_from_url(notification['url']) scope, action = get_scope_action_from_url(notification['url'])
if scope in config.user_activity_accepted_scope: if scope in appConfig.user_activity_accepted_scope:
if config.user_activity_accepted_scope == '*': if appConfig.user_activity_accepted_scope == '*':
return True return True
elif action in config.user_activity_accepted_scope[scope]: elif action in appConfig.user_activity_accepted_scope[scope]:
return True return True
return False return False

View file

@ -14,7 +14,7 @@ import exercise as exercise_model
import notification as notification_model import notification as notification_model
import db import db
import config import config
from config import logger from appConfig import logger
import misp_api import misp_api
@ -123,6 +123,10 @@ async def toggle_verbose_mode(sid, payload):
async def toggle_apiquery_mode(sid, payload): async def toggle_apiquery_mode(sid, payload):
return notification_model.set_apiquery_mode(payload['apiquery']) return notification_model.set_apiquery_mode(payload['apiquery'])
@sio.event
async def remediate_setting(sid, payload):
return await doSettingRemediation(payload['name'])
@sio.on('*') @sio.on('*')
async def any_event(event, sid, data={}): async def any_event(event, sid, data={}):
logger.info('>> Unhandled event %s', event) logger.info('>> Unhandled event %s', event)
@ -201,6 +205,11 @@ async def getDiagnostic() -> dict:
return diagnostic return diagnostic
async def doSettingRemediation(setting) -> dict:
result = await misp_api.remediateSetting(setting)
return result
async def notification_history(): async def notification_history():
global ZMQ_MESSAGE_COUNT_LAST_TIMESPAN global ZMQ_MESSAGE_COUNT_LAST_TIMESPAN
while True: while True:

View file

@ -1,10 +1,11 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { exercises, selected_exercises, diagnostic, fullReload, resetAllExerciseProgress, resetLiveLogs, changeExerciseSelection, debouncedGetDiangostic } from "@/socket"; import { exercises, selected_exercises, diagnostic, fullReload, resetAllExerciseProgress, resetLiveLogs, changeExerciseSelection, debouncedGetDiangostic, remediateSetting } from "@/socket";
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faScrewdriverWrench, faTrash, faSuitcaseMedical, faGraduationCap, faBan, faRotate } from '@fortawesome/free-solid-svg-icons' import { faScrewdriverWrench, faTrash, faSuitcaseMedical, faGraduationCap, faBan, faRotate, faHammer, faCheck } from '@fortawesome/free-solid-svg-icons'
const admin_modal = ref(null) const admin_modal = ref(null)
const clickedButtons = ref([])
const diagnosticLoading = computed(() => Object.keys(diagnostic.value).length == 0) const diagnosticLoading = computed(() => Object.keys(diagnostic.value).length == 0)
const isMISPOnline = computed(() => diagnostic.value.version?.version !== undefined) const isMISPOnline = computed(() => diagnostic.value.version?.version !== undefined)
@ -15,10 +16,16 @@
changeExerciseSelection(exec_uuid, state_enabled); changeExerciseSelection(exec_uuid, state_enabled);
} }
function settingHandler(setting) {
remediateSetting(setting)
}
function showTheModal() { function showTheModal() {
admin_modal.value.showModal() admin_modal.value.showModal()
clickedButtons.value = []
debouncedGetDiangostic() debouncedGetDiangostic()
} }
</script> </script>
<template> <template>
@ -131,23 +138,54 @@
<div v-if="diagnosticLoading" class="flex justify-center"> <div v-if="diagnosticLoading" class="flex justify-center">
<span class="loading loading-dots loading-lg"></span> <span class="loading loading-dots loading-lg"></span>
</div> </div>
<div <table v-else class="bg-white dark:bg-slate-700 rounded-lg shadow-xl w-full mt-2">
v-for="(value, setting) in diagnostic['settings']" <thead>
<tr>
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Setting</th>
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Value</th>
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Expected Value</th>
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-center">Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="(settingValues, setting) in diagnostic['settings']"
:key="setting" :key="setting"
> >
<div> <td class="font-mono font-semibold text-base px-2">{{ setting }}</td>
<label class="label cursor-pointer justify-start p-0 pt-1"> <td
<input :class="`font-mono text-base tracking-tight px-2 ${settingValues.expected_value != settingValues.value ? 'text-red-600 dark:text-red-600' : ''}`"
type="checkbox" >
:checked="value" <i v-if="settingValues.value === undefined || settingValues.value === null" class="text-nowrap">- none -</i>
:value="setting" {{ settingValues.value }}
:class="`checkbox ${value ? 'checkbox-success' : 'checkbox-danger'} [--fallback-bc:#cbd5e1]`" </td>
disabled <td class="font-mono text-base tracking-tight px-2">{{ settingValues.expected_value }}</td>
/> <td class="px-2 text-center">
<span class="font-mono font-semibold text-base ml-3">{{ setting }}</span> <span v-if="settingValues.error === true"
</label> class="text-red-600 dark:text-red-600"
</div> >Error: {{ settingValues.errorMessage }}</span>
</div> <button
v-else-if="settingValues.expected_value != settingValues.value"
@click="clickedButtons.push(setting) && settingHandler(setting)"
:disabled="clickedButtons.includes(setting)"
class="h-8 min-h-8 px-2 font-semibold bg-green-600 text-slate-200 hover:bg-green-700 btn gap-1"
>
<template v-if="!clickedButtons.includes(setting)">
<FontAwesomeIcon :icon="faHammer" size="sm" fixed-width></FontAwesomeIcon>
Remediate
</template>
<template v-else>
<span class="loading loading-dots loading-sm"></span>
</template>
</button>
<span v-else class="text-base font-bold text-green-600 dark:text-green-600">
<FontAwesomeIcon :icon="faCheck" class=""></FontAwesomeIcon>
OK
</span>
</td>
</tr>
</tbody>
</table>
</div> </div>
</template> </template>

View file

@ -96,6 +96,18 @@ export function toggleApiQueryMode(enabled) {
sendToggleApiQueryMode(enabled) sendToggleApiQueryMode(enabled)
} }
export function remediateSetting(setting) {
sendRemediateSetting(setting, (result) => {
console.log(result);
if (result.success) {
state.diagnostic['settings'][setting].value = state.diagnostic['settings'][setting].expected_value
} else {
state.diagnostic['settings'][setting].error = true
state.diagnostic['settings'][setting].errorMessage = result.message
}
})
}
export const debouncedGetProgress = debounce(getProgress, 200, {leading: true}) export const debouncedGetProgress = debounce(getProgress, 200, {leading: true})
export const debouncedGetDiangostic = debounce(getDiangostic, 1000, {leading: true}) export const debouncedGetDiangostic = debounce(getDiangostic, 1000, {leading: true})
@ -181,6 +193,15 @@ function sendToggleApiQueryMode(enabled) {
socket.emit("toggle_apiquery_mode", payload, () => {}) socket.emit("toggle_apiquery_mode", payload, () => {})
} }
function sendRemediateSetting(setting, cb) {
const payload = {
name: setting
}
socket.emit("remediate_setting", payload, (result) => {
cb(result)
})
}
/* Event listener */ /* Event listener */
socket.on("connect", () => { socket.on("connect", () => {