UNPKG

@appsensorlike/appsensorlike

Version:

A port of OWASP AppSensor reference implementation

456 lines (455 loc) 20 kB
import { AttackAnalysisEngine, EventAnalysisEngine, ResponseAnalysisEngine } from "../../core/analysis/analysis.js"; import { AppSensorEvent, AppSensorServer, Attack, Interval, INTERVAL_UNITS, Response, User, Utils } from "../../core/core.js"; import { SearchCriteria } from "../../core/criteria/criteria.js"; import { Notification } from "../../core/rule/rule.js"; import { Logger } from "../../logging/logging.js"; class AggregateAttackAnalysisEngine extends AttackAnalysisEngine { constructor() { super(...arguments); this.appSensorServer = new AppSensorServer(); } getAppSensorServer() { return this.appSensorServer; } setAppSensorServer(appSensorServer) { this.appSensorServer = appSensorServer; } /** * This method analyzes {@link Attack} objects that are added * to the system (either via direct addition or generated by the event analysis * engine), generates an appropriate {@link Response} object, * and adds it to the configured {@link ResponseStore} * * @param event the {@link Attack} that was added to the {@link AttackStore} */ // @Override async analyze(attack) { if (attack != null && attack.getRule() != null) { const response = await this.findAppropriateResponse(attack); if (response != null) { let userName = Utils.getUserName(attack.getUser()); Logger.getServerLogger().info("AggregateAttackAnalysisEngine.analyze:", `Response set for user <${userName}> - storing response action ${response.getAction()}`); const responseStore = this.appSensorServer.getResponseStore(); if (responseStore !== null) { await responseStore.addResponse(response); } } } } /** * Find/generate {@link Response} appropriate for specified {@link Attack}. * * @param attack {@link Attack} that is being analyzed * @return {@link Response} to be executed for given {@link Attack} */ async findAppropriateResponse(attack) { const triggeringRule = attack.getRule(); const criteria = new SearchCriteria(). setUser(attack.getUser()). setRule(triggeringRule); const serverConfig = this.appSensorServer.getConfiguration(); if (serverConfig !== null) { criteria.setDetectionSystemIds(serverConfig.getRelatedDetectionSystems(attack.getDetectionSystem())); } //grab any existing responses let existingResponses = []; const responseStore = this.appSensorServer.getResponseStore(); if (responseStore !== null) { existingResponses = await responseStore.findResponses(criteria); } let responseAction = null; let interval = null; const possibleResponses = this.findPossibleResponses(triggeringRule); if (existingResponses === null || existingResponses.length === 0) { //no responses yet, just grab first configured response from rule if (possibleResponses.length > 0) { const response = possibleResponses[0]; responseAction = response.getAction(); interval = response.getInterval(); } } else { for (const configuredResponse of possibleResponses) { responseAction = configuredResponse.getAction(); interval = configuredResponse.getInterval(); if (!this.isPreviousResponse(configuredResponse, existingResponses)) { //if we find that this response doesn't already exist, use it break; } //if we reach here, we will just use the last configured response (repeat last response) } } if (responseAction == null) { let name = ''; if (triggeringRule) { name = triggeringRule.getName(); } throw new Error("No appropriate response was configured for this rule: " + name); } const response = new Response(). setUser(attack.getUser()). setTimestamp(attack.getTimestamp()). setAction(responseAction). setInterval(interval). setDetectionSystem(attack.getDetectionSystem()) //addition to the original code to track what caused the response .setDetectionPoint(attack.getDetectionPoint()) .setRule(attack.getRule()); return response; } /** * Lookup configured {@link Response} objects for specified {@link Rule} * * @param rule triggered {@link Rule} * @return collection of {@link Response} objects for given {@link Rule} */ findPossibleResponses(rule) { let possibleResponses = []; const serverConfig = this.appSensorServer.getConfiguration(); if (serverConfig !== null) { for (const configuredRule of serverConfig.getRules()) { if (configuredRule.equals(rule)) { possibleResponses = configuredRule.getResponses(); break; } } } return possibleResponses; } /** * Test a given {@link Response} to see if it's been executed before. * * @param response {@link Response} to test to see if it's been executed before * @param existingResponses set of previously executed {@link Response}s * @return true if {@link Response} has been executed before */ isPreviousResponse(response, existingResponses) { let previousResponse = false; for (const existingResponse of existingResponses) { if (response.getAction() === existingResponse.getAction()) { previousResponse = true; } } return previousResponse; } } class AggregateEventAnalysisEngine extends EventAnalysisEngine { constructor() { super(...arguments); this.appSensorServer = new AppSensorServer(); } getAppSensorServer() { return this.appSensorServer; } setAppSensorServer(appSensorServer) { this.appSensorServer = appSensorServer; } /** * This method determines whether an {@link Event} that has been added to the system * has triggered a {@link Rule}. If so, an {@link Attack} is generated. * * @param event the {@link Event} that was added to the {@link EventStore} */ // @Override async analyze(triggerEvent) { const serverConfiguration = this.appSensorServer.getConfiguration(); if (serverConfiguration !== null) { const rules = serverConfiguration.findRules(triggerEvent); for (const rule of rules) { // if (this.checkRule(triggerEvent, rule)) { // this.generateAttack(triggerEvent, rule); // } const ruleFulfilled = await this.checkRule(triggerEvent, rule); if (ruleFulfilled) { await this.generateAttack(triggerEvent, rule); } } } } /** * Evaluates a {@link Rule}'s logic by compiling a list of all {@link Notification}s * and then evaluating each {@link Expression} within the {@link Rule}. All {@link Expression}s * must evaluate to true within the {@link Rule}'s window for the {@link Rule} to evaluate to * true. The process follows the "sliding window" pattern. * * @param event the {@link Event} that triggered analysis * @param rule the {@link Rule} being evaluated * @return the boolean evaluation of the {@link Rule} */ async checkRule(triggerEvent, rule) { const notifications = await this.getNotifications(triggerEvent, rule); //PriorityQueue in java let windowedNotifications = []; const expressions = rule.getExpressions().slice(); let currentExpression = expressions.shift(); let tail = undefined; while (notifications.length > 0) { tail = notifications.shift(); if (tail && currentExpression) { windowedNotifications.push(tail); windowedNotifications.sort((a, b) => { return Notification.getStartTimeAscendingComparator(a, b); }); const tailEndTime = tail.getEndTime(); const currExpWindow = currentExpression.getWindow(); if (currExpWindow !== null) { this.trim(windowedNotifications, new Date(tailEndTime.getTime() - currExpWindow.toMillis())); } if (this.checkExpression(currentExpression, windowedNotifications)) { if (expressions.length > 0) { currentExpression = expressions.shift(); windowedNotifications = []; this.trim(notifications, tailEndTime); } else { return true; } } } } return false; } /** * Evaluates an {@link Expression}'s logic by evaluating all {@link Clause}s. Any * {@link Clause} must evaluate to true for the {@link Expression} to evaluate to true. * * Equivalent to checking "OR" logic between {@link Clause}s. * * @param expression the {@link Expression} being evaluated * @param notifications the {@link Notification}s in the current "sliding window" * @return the boolean evaluation of the {@link Expression} */ checkExpression(expression, notifications) { if (expression) { for (const clause of expression.getClauses()) { if (this.checkClause(clause, notifications)) { return true; } } } return false; } /** * Evaluates a {@link Clause}'s logic by checking if each {@link MonitorPoint} * within the {@link Clause} is in the current "sliding window". * * Equivalent to checking "AND" logic between {@link RuleDetectionPoint}s. * * @param clause the {@link Clause} being evaluated * @param notifications the {@link Notification}s in the current "sliding window" * @return the boolean evaluation of the {@link Clause} */ checkClause(clause, notifications) { const windowDetectionPoints = []; for (const notification of notifications) { const monitPoint = notification.getMonitorPoint(); if (monitPoint !== null) { windowDetectionPoints.push(monitPoint); } } for (const detectionPoint of clause.getMonitorPoints()) { let contains = false; for (let i = 0; i < windowDetectionPoints.length; i++) { if (windowDetectionPoints[i] === detectionPoint || windowDetectionPoints[i].equals(detectionPoint)) { contains = true; break; } } if (!contains) { return false; } } return true; } /** * Pops {@link Notification}s out of the queue until the start time of the queue's head * is after the parameter time. The queue of notifications MUST be sorted in ascending * order by start time. * * @param notifications the queue of {@link Notification}s being trimmed * @param time the time that all {@link Notification}s in the queue must be after */ trim(notifications, time) { while (notifications.length > 0) { const startTime = notifications[0].getStartTime(); if (!(time !== null && startTime.getTime() > time.getTime())) { notifications.shift(); } else { break; } } } /** * Builds a queue of all {@link Notification}s from the events relating to the * current {@link Rule}. The {@link Notification}s are ordered in the Queue by * start time. * * @param triggerEvent the {@link Event} that triggered analysis * @param rule the {@link Rule} being evaluated * @return a queue of {@link TriggerEvents} */ async getNotifications(triggerEvent, rule) { const notificationQueue = []; const events = await this.getApplicableEvents(triggerEvent, rule); const detectionPoints = rule.getAllDetectionPoints(); for (const detectionPoint of detectionPoints) { const eventQueue = []; for (const event of events) { const eventDetPoint = event.getDetectionPoint(); if (eventDetPoint !== null && eventDetPoint.typeAndThresholdMatches(detectionPoint)) { eventQueue.push(event); const detPointThreshold = detectionPoint.getThreshold(); if (this.isThresholdViolated(eventQueue, event, detPointThreshold)) { const queueDuration = this.getQueueInterval(eventQueue, event).toMillis(); const start = new Date(eventQueue[0].getTimestamp()); const notification = new Notification(queueDuration, INTERVAL_UNITS.MILLISECONDS, start, detectionPoint); notificationQueue.push(notification); } if (detPointThreshold !== null && eventQueue.length >= detPointThreshold.getCount()) { eventQueue.shift(); } } } } notificationQueue.sort((a, b) => { return Notification.getEndTimeAscendingComparator(a, b); }); return notificationQueue; } /** * Determines whether a queue of {@link Event}s crosses a {@link Threshold} in the correct * amount of time. * * @param queue a queue of {@link Event}s * @param tailEvent the {@link Event} at the tail of the queue * @param threshold the {@link Threshold} to evaluate * @return boolean evaluation of the {@link Threshold} */ isThresholdViolated(queue, tailEvent, threshold) { let queueInterval = null; if (threshold !== null && queue.length >= threshold.getCount()) { queueInterval = this.getQueueInterval(queue, tailEvent); const thresholdInt = threshold.getInterval(); if (queueInterval !== null && thresholdInt !== null && queueInterval.toMillis() <= thresholdInt.toMillis()) { return true; } } return false; } /** * Determines the time between the {@link Event} at the head of the queue and the * {@link Event} at the tail of the queue. * * @param queue a queue of {@link Event}s * @param tailEvent the {@link Event} at the tail of the queue * @return the duration of the queue as an {@link Interval} */ getQueueInterval(queue, tailEvent) { const endTime = tailEvent.getTimestamp(); const startTime = queue[0].getTimestamp(); return new Interval(endTime.getTime() - startTime.getTime(), INTERVAL_UNITS.MILLISECONDS); } /** * Generates an attack from the given {@link Rule} and triggered {@link Event} * * @param triggerEvent the {@link Event} that triggered the {@link Rule} * @param rule the {@link Rule} being evaluated */ async generateAttack(triggerEvent, rule) { let userName = Utils.getUserName(triggerEvent.getUser()); Logger.getServerLogger().info("AggregateEventAnalysisEngine.generateAttack:", `Attack generated on rule: ${rule.getGuid()} by user: ${userName}`); const attack = new Attack(). setUser(new User(userName)). setRule(rule). setTimestamp(triggerEvent.getTimestamp()). setDetectionSystem(triggerEvent.getDetectionSystem()). setResource(triggerEvent.getResource()); const attackStore = this.appSensorServer.getAttackStore(); if (attackStore !== null) { await attackStore.addAttack(attack); } } /** * Finds all {@link Event}s related to the {@link Rule} being evaluated. * * @param triggerEvent the {@link Event} that triggered the {@link Rule} * @param rule the {@link Rule} being evaluated * @return a list of {@link Event}s applicable to the {@link Rule} */ async getApplicableEvents(triggerEvent, rule) { let events = []; let ruleStartTime = triggerEvent.getTimestamp(); const ruleWindow = rule.getWindow(); if (ruleWindow !== null) { ruleStartTime = new Date(ruleStartTime.getTime() - ruleWindow.toMillis()); } const lastAttackTime = await this.findMostRecentAttackTime(triggerEvent, rule); const earliest = (ruleStartTime.getTime() > lastAttackTime.getTime()) ? ruleStartTime : lastAttackTime; const criteria = new SearchCriteria(). setUser(triggerEvent.getUser()). setEarliest(new Date(earliest.getTime() + 1)). setRule(rule); const serverConfig = this.appSensorServer.getConfiguration(); if (serverConfig !== null) { criteria.setDetectionSystemIds(serverConfig.getRelatedDetectionSystems(triggerEvent.getDetectionSystem())); } const eventStore = this.appSensorServer.getEventStore(); if (eventStore !== null) { events = await eventStore.findEvents(criteria); } events.sort((a, b) => { return AppSensorEvent.getTimeAscendingComparator(a, b); }); return events; } /** * Finds the most recent {@link Attack} from the {@link Rule} being evaluated. * * @param triggerEvent the {@link Event} that triggered the {@link Rule} * @param rule the {@link Rule} being evaluated * @return a {@link DateTime} of the most recent attack related to the {@link Rule} */ async findMostRecentAttackTime(triggerEvent, rule) { let newest = new Date(0); let userName = Utils.getUserName(triggerEvent.getUser()); let criteria = new SearchCriteria(). setUser(new User(userName)). setRule(rule); const serverConfig = this.appSensorServer.getConfiguration(); if (serverConfig !== null) { criteria.setDetectionSystemIds(serverConfig.getRelatedDetectionSystems(triggerEvent.getDetectionSystem())); } const attackStore = this.appSensorServer.getAttackStore(); if (attackStore !== null) { const attacks = await attackStore.findAttacks(criteria); for (const attack of attacks) { const attackRule = attack.getRule(); if (attackRule !== null && attackRule.guidMatches(rule)) { const attackTimestamp = attack.getTimestamp(); if (attackTimestamp.getTime() > newest.getTime()) { newest = new Date(attackTimestamp); } } } } return newest; } } class AggregateResponseAnalysisEngine extends ResponseAnalysisEngine { /** * This method simply logs responses. * * @param response {@link Response} that has been added to the {@link ResponseStore}. */ async analyze(response) { if (response != null) { let userName = Utils.getUserName(response.getUser()); Logger.getServerLogger().trace("AggregateResponseAnalysisEngine.analyze:", `NO-OP Response for user <${userName}> - should be executing response action ${response.getAction()}`); } } } export { AggregateAttackAnalysisEngine, AggregateEventAnalysisEngine, AggregateResponseAnalysisEngine };