All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.airbus_cyber_security.graylog.wizard.alert.rest.Conversions Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2018 Airbus CyberSecurity (SAS)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Server Side Public License, version 1,
 * as published by MongoDB, Inc.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * Server Side Public License for more details.
 *
 * You should have received a copy of the Server Side Public License
 * along with this program. If not, see
 * .
 */

package com.airbus_cyber_security.graylog.wizard.alert.rest;

import com.airbus_cyber_security.graylog.wizard.alert.business.FieldRulesUtilities;
import com.airbus_cyber_security.graylog.wizard.alert.model.TriggeringConditions;
import com.airbus_cyber_security.graylog.wizard.alert.rest.models.AlertRuleStream;
import com.airbus_cyber_security.graylog.wizard.alert.model.FieldRule;
import com.airbus_cyber_security.graylog.wizard.alert.rest.models.requests.AlertRuleRequest;
import com.airbus_cyber_security.graylog.events.processor.correlation.CorrelationCountProcessorConfig;
import com.airbus_cyber_security.graylog.events.processor.correlation.checks.OrderType;
import com.airbus_cyber_security.graylog.wizard.alert.model.AlertType;
import com.airbus_cyber_security.graylog.wizard.database.Description;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import org.graylog.events.conditions.Expr;
import org.graylog.events.conditions.Expression;
import org.graylog.events.processor.EventProcessorConfig;
import org.graylog.events.processor.aggregation.AggregationConditions;
import org.graylog.events.processor.aggregation.AggregationEventProcessorConfig;
import org.graylog.events.processor.aggregation.AggregationFunction;
import org.graylog.events.processor.aggregation.AggregationSeries;
import org.graylog2.plugin.streams.Stream;
import org.graylog2.plugin.streams.StreamRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
import java.util.*;

/**
 * Converts from business model to rest model and vice versa
 */
// TODO should try to find a way to split this class. Too long
public class Conversions {

    private static final Logger LOG = LoggerFactory.getLogger(Conversions.class);

    private static final int MILLISECONDS_IN_A_MINUTE = 60 * 1000;

    // TODO should rather parse the conditionParameters as soon as it gets in the system, in order to get a tidy class with getters
    //      there is even a way to do it nicely with Jackson: see jackson-docs polymorphic type handling, jsonsubtypes
    private static final String FIELD = "field";
    private static final String TYPE = "type";
    private static final String GROUPING_FIELDS = "grouping_fields";
    private static final String DISTINCT_BY = "distinct_by";
    private static final String TIME = "time";
    private static final String GRACE = "grace";
    private static final String ADDITIONAL_THRESHOLD = "additional_threshold";
    private static final String ADDITIONAL_THRESHOLD_TYPE = "additional_threshold_type";
    private static final String THRESHOLD_TYPE = "threshold_type";
    private static final String THRESHOLD = "threshold";
    private static final String THRESHOLD_TYPE_MORE = ">";
    private static final String THRESHOLD_TYPE_LESS = "<";

    private final FieldRulesUtilities fieldRulesUtilities;

    @Inject
    public Conversions(FieldRulesUtilities fieldRulesUtilities) {
        this.fieldRulesUtilities = fieldRulesUtilities;
    }

    // TODO should avoid these conversions by always working with ms (from the IHM down to the server)
    private long convertMillisecondsToMinutes(long value) {
        return value / MILLISECONDS_IN_A_MINUTE;
    }

    private long convertMinutesToMilliseconds(long value) {
        return value * MILLISECONDS_IN_A_MINUTE;
    }

    // TODO should introduce constants for MORE and LESS
    private String convertCorrelationCountThresholdType(String thresholdType) {
        if (thresholdType.equals("MORE")) {
            return THRESHOLD_TYPE_MORE;
        } else {
            return THRESHOLD_TYPE_LESS;
        }
    }

    private int convertThreshold(Expression expression) {
        Expression expressionRight;
        if (expression instanceof Expr.Greater) {
            expressionRight = ((Expr.Greater) expression).right();
        } else if (expression instanceof Expr.GreaterEqual) {
            expressionRight = ((Expr.GreaterEqual) expression).right();
        } else if (expression instanceof Expr.Lesser) {
            expressionRight = ((Expr.Lesser) expression).right();
        } else if (expression instanceof Expr.LesserEqual) {
            expressionRight = ((Expr.LesserEqual) expression).right();
        } else if (expression instanceof Expr.Equal) {
            expressionRight = ((Expr.Equal) expression).right();
        } else {
            LOG.error("Can't get threshold, error cast Expression");
            return 0;
        }

        if (expressionRight instanceof Expr.NumberValue) {
            return (int) ((Expr.NumberValue) expressionRight).value();
        } else {
            LOG.error("Can't get threshold, error cast Right Expression");
            return 0;
        }
    }

    Map getConditionParameters(EventProcessorConfig eventConfig) {
        Map parametersCondition = Maps.newHashMap();

        switch (eventConfig.type()) {
            case "correlation-count":
                CorrelationCountProcessorConfig correlationConfig = (CorrelationCountProcessorConfig) eventConfig;
                parametersCondition.put(THRESHOLD, correlationConfig.threshold());
                String thresholdType = convertCorrelationCountThresholdType(correlationConfig.thresholdType());
                String additionalThresholdType = convertCorrelationCountThresholdType(correlationConfig.additionalThresholdType());
                parametersCondition.put(THRESHOLD_TYPE, thresholdType);
                parametersCondition.put(ADDITIONAL_THRESHOLD, correlationConfig.additionalThreshold());
                parametersCondition.put(ADDITIONAL_THRESHOLD_TYPE, additionalThresholdType);
                parametersCondition.put(TIME, this.convertMillisecondsToMinutes(correlationConfig.searchWithinMs()));
                parametersCondition.put(GROUPING_FIELDS, correlationConfig.groupingFields());
                parametersCondition.put(GRACE, this.convertMillisecondsToMinutes(correlationConfig.executeEveryMs()));
                break;
            case "aggregation-v1":
                AggregationEventProcessorConfig aggregationConfig = (AggregationEventProcessorConfig) eventConfig;
                parametersCondition.put(TIME, this.convertMillisecondsToMinutes(aggregationConfig.searchWithinMs()));
                parametersCondition.put(GRACE, this.convertMillisecondsToMinutes(aggregationConfig.executeEveryMs()));
                parametersCondition.put(THRESHOLD, convertThreshold(aggregationConfig.conditions().get().expression().get()));
                parametersCondition.put(THRESHOLD_TYPE, aggregationConfig.conditions().get().expression().get().expr());
                AggregationSeries series = aggregationConfig.series().get(0);
                parametersCondition.put(TYPE, series.function().toString());
                String distinctBy = "";
                Optional seriesField = series.field();
                if (seriesField.isPresent()) {
                    // TODO think about this, but there is some code smell here...
                    // It is because AggregationEventProcessorConfig is used both for Count and Statistical conditions
                    distinctBy = seriesField.get();
                    // TODO should introduce constants here for "field"...
                    parametersCondition.put(FIELD, distinctBy);
                }
                parametersCondition.put(GROUPING_FIELDS, aggregationConfig.groupBy());
                parametersCondition.put(DISTINCT_BY, distinctBy);
                break;
            default:
                throw new UnsupportedOperationException();
        }
        return parametersCondition;
    }

    private boolean isValidTitle(String title) {
        return !(title == null || title.isEmpty());
    }

    private boolean isValidStream(AlertRuleStream stream) {
        if (!stream.getMatchingType().equals(Stream.MatchingType.AND) && !stream.getMatchingType().equals(Stream.MatchingType.OR)) {
            return false;
        }
        for (FieldRule fieldRule: stream.getFieldRules()) {
            if (!fieldRulesUtilities.isValidFieldRule(fieldRule)) {
                return false;
            }
        }
        return true;
    }

    private boolean isValidStatThresholdType(String thresholdType) {
        return (thresholdType.equals("<") || thresholdType.equals("<=") ||
                thresholdType.equals(">") || thresholdType.equals(">=") || thresholdType.equals("=="));
    }

    private boolean isValidCondStatistical(Map conditionParameters) {
        if (!conditionParameters.containsKey(TYPE)) {
            LOG.debug("Missing condition parameter {}", TYPE);
            return false;
        }
        if (!conditionParameters.containsKey(FIELD)) {
            LOG.debug("Missing condition parameter {}", FIELD);
            return false;
        }
        String thresholdType = conditionParameters.get(THRESHOLD_TYPE).toString();
        if (!isValidStatThresholdType(thresholdType)) {
            LOG.debug("Invalid condition parameter {}, {}", THRESHOLD_TYPE, thresholdType);
            return false;
        }
        return true;
    }

    private boolean isValidThresholdType(String thresholdType) {
        return (thresholdType.equals(THRESHOLD_TYPE_MORE) || thresholdType.equals(THRESHOLD_TYPE_LESS));
    }

    private boolean isValidCondCorrelation(Map conditionParameters, AlertRuleStream secondStream) {
        return (conditionParameters.containsKey(ADDITIONAL_THRESHOLD) &&
                conditionParameters.containsKey(ADDITIONAL_THRESHOLD_TYPE) &&
                isValidThresholdType(conditionParameters.get(THRESHOLD_TYPE).toString()) &&
                isValidThresholdType(conditionParameters.get(ADDITIONAL_THRESHOLD_TYPE).toString()) &&
                isValidStream(secondStream));
    }

    private boolean isValidCondOr(Map conditionParameters, AlertRuleStream secondStream) {
        return (isValidThresholdType(conditionParameters.get(THRESHOLD_TYPE).toString()) &&
                isValidStream(secondStream));
    }

    private boolean isValidCondition(AlertType alertType, Map conditionParameters, AlertRuleStream secondStream) {
        if (!conditionParameters.containsKey(TIME)) {
            LOG.debug("Missing condition parameter: {}", TIME);
            return false;
        }
        if (!conditionParameters.containsKey(THRESHOLD)) {
            LOG.debug("Missing condition parameter: {}", THRESHOLD);
            return false;
        }
        if (!conditionParameters.containsKey(THRESHOLD_TYPE)) {
            LOG.debug("Missing condition parameter: {}", THRESHOLD_TYPE);
            return false;
        }
        return switch (alertType) {
            case STATISTICAL -> isValidCondStatistical(conditionParameters);
            case THEN, AND -> isValidCondCorrelation(conditionParameters, secondStream);
            case OR -> isValidCondOr(conditionParameters, secondStream);
            default -> true;
        };
    }

    public boolean isValidRequest(AlertRuleRequest request){
        return (isValidTitle(request.getTitle()) &&
                isValidStream(request.getStream()) &&
                isValidCondition(request.getConditionType(), request.conditionParameters(), request.getSecondStream()));
    }

    public void checkIsValidRequest(AlertRuleRequest request) {
        if (!this.isValidRequest(request)) {
            LOG.error("Invalid alert rule request");
            throw new BadRequestException("Invalid alert rule request.");
        }
    }

    private List getListFieldRule(List listStreamRule) {
        List listFieldRule = new ArrayList<>();
        for (StreamRule streamRule: listStreamRule) {
            if (streamRule.getInverted()) {
                listFieldRule.add(FieldRule.create(streamRule.getId(), streamRule.getField(), -streamRule.getType().toInteger(), streamRule.getValue()));
            } else {
                listFieldRule.add(FieldRule.create(streamRule.getId(), streamRule.getField(), streamRule.getType().toInteger(), streamRule.getValue()));
            }
        }
        return listFieldRule;
    }

    // TODO inline
    AlertRuleStream constructAlertRuleStream(Stream stream, TriggeringConditions conditions) {
        // TODO why is this check necessary?
        if (stream == null) {
            return null;
        }

        List fieldRules = new ArrayList<>();
        if (conditions.pipeline() != null) {
            List pipelineFieldRules = conditions.pipeline().fieldRules();
            Optional.ofNullable(pipelineFieldRules).ifPresent(fieldRules::addAll);
        }
        Optional.ofNullable(this.getListFieldRule(stream.getStreamRules())).ifPresent(fieldRules::addAll);
        return AlertRuleStream.create(stream.getId(), stream.getMatchingType(), fieldRules);
    }

    private String convertThresholdTypeToCorrelation(String thresholdType) {
        if (thresholdType.equals(THRESHOLD_TYPE_MORE)) {
            return "MORE";
        } else {
            return "LESS";
        }
    }

    private int accessThreshold(Map conditionParameter) {
        return (int) conditionParameter.get(THRESHOLD);
    }

    // TODO move method to AlertRuleUtils?
    // TODO instead of a String, the type could already be a com.airbus_cyber_security.graylog.events.processor.correlation.checks.OrderType
    EventProcessorConfig createCorrelationCondition(AlertType type, String streamID, String streamID2, Map conditionParameter) {
        OrderType messageOrder;
        if (type == AlertType.THEN) {
            messageOrder = OrderType.AFTER;
        } else {
            messageOrder = OrderType.ANY;
        }
        String thresholdType = convertThresholdTypeToCorrelation((String) conditionParameter.get(THRESHOLD_TYPE));
        String additionalThresholdType = convertThresholdTypeToCorrelation((String) conditionParameter.get(ADDITIONAL_THRESHOLD_TYPE));

        int threshold = this.accessThreshold(conditionParameter);

        long searchWithinMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(TIME).toString()));
        long executeEveryMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(GRACE).toString()));

        return CorrelationCountProcessorConfig.builder()
                .stream(streamID)
                .thresholdType(thresholdType)
                .threshold(threshold)
                .additionalStream(streamID2)
                .additionalThresholdType(additionalThresholdType)
                .additionalThreshold((int) conditionParameter.get(ADDITIONAL_THRESHOLD))
                .messagesOrder(messageOrder)
                .searchWithinMs(searchWithinMs)
                .executeEveryMs(executeEveryMs)
                // TODO CorrelationCountProcessorConfig.groupingFields should be of type List (or better just Collection/Iterable) rather than Set
                .groupingFields((List) conditionParameter.get(GROUPING_FIELDS))
                .comment(Description.COMMENT_ALERT_WIZARD)
                .searchQuery("*")
                .build();
    }

    private Expression createExpressionFromNumberThreshold(String identifier, String thresholdType, int threshold) {
        Expr.NumberReference left = Expr.NumberReference.create(identifier);
        Expr.NumberValue right = Expr.NumberValue.create(threshold);
        switch (thresholdType) {
            case THRESHOLD_TYPE_MORE:
                return Expr.Greater.create(left, right);
            case THRESHOLD_TYPE_LESS:
                return Expr.Lesser.create(left, right);
            default:
                throw new BadRequestException("createExpressionFromNumberThreshold: unexpected threshold type " + thresholdType);
        }
    }

    public EventProcessorConfig createAggregationCondition(String streamIdentifier, Map conditionParameter) {
        List groupByFields = (List) conditionParameter.get(GROUPING_FIELDS);
        String distinctBy = (String) conditionParameter.get(DISTINCT_BY);

        Set streams = ImmutableSet.of(streamIdentifier);
        // TODO extract method to parse searchWithinMs
        long searchWithinMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(TIME).toString()));
        // TODO extract method to parse executeEveryMs
        long executeEveryMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(GRACE).toString()));

        String thresholdType = (String) conditionParameter.get(THRESHOLD_TYPE);
        int threshold = this.accessThreshold(conditionParameter);

        String identifier = UUID.randomUUID().toString();
        AggregationSeries.Builder seriesBuilder = AggregationSeries.builder().id(identifier);

        if (distinctBy.isEmpty()) {
            seriesBuilder.function(AggregationFunction.COUNT);
        } else {
            seriesBuilder.function(AggregationFunction.CARD).field(distinctBy);
        }

        AggregationSeries series = seriesBuilder.build();

        Expression expression = createExpressionFromNumberThreshold(identifier, thresholdType, threshold);
        AggregationConditions conditions = AggregationConditions.builder()
                .expression(expression)
                .build();

        return AggregationEventProcessorConfig.builder()
                .query("")
                .streams(streams)
                .groupBy(groupByFields)
                .series(ImmutableList.of(series))
                .conditions(conditions)
                .executeEveryMs(executeEveryMs)
                .searchWithinMs(searchWithinMs)
                .build();
    }

    private AggregationFunction mapTypeToAggregationFunction(String type) {
        switch (type) {
            case "AVG":
                return AggregationFunction.AVG;
            case "MIN":
                return AggregationFunction.MIN;
            case "MAX":
                return AggregationFunction.MAX;
            case "SUM":
                return AggregationFunction.SUM;
            case "STDDEV":
                return AggregationFunction.STDDEV;
            case "CARD":
                return AggregationFunction.CARD;
            case "COUNT":
                return AggregationFunction.COUNT;
            case "SUMOFSQUARES":
                return AggregationFunction.SUMOFSQUARES;
            case "VARIANCE":
                return AggregationFunction.VARIANCE;
            default:
                throw new BadRequestException();
        }
    }

    private Expression createExpressionFromThreshold(String identifier, String thresholdType, int threshold) {
        Expr.NumberReference left = Expr.NumberReference.create(identifier);
        Expr.NumberValue right = Expr.NumberValue.create(threshold);
        switch (thresholdType) {
            case ">":
                return Expr.Greater.create(left, right);
            case ">=":
                return Expr.GreaterEqual.create(left, right);
            case "<":
                return Expr.Lesser.create(left, right);
            case "<=":
                return Expr.LesserEqual.create(left, right);
            case "==":
                return Expr.Equal.create(left, right);
            default:
                throw new BadRequestException();
        }
    }

    public EventProcessorConfig createStatisticalCondition(String streamID, Map conditionParameter) {
        String type = conditionParameter.get(TYPE).toString();
        LOG.debug("Begin Stat, type: {}", type);
        // TODO extract method to parse searchWithinMs
        long searchWithinMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(TIME).toString()));
        // TODO extract method to parse executeEveryMs
        long executeEveryMs = this.convertMinutesToMilliseconds(Long.parseLong(conditionParameter.get(GRACE).toString()));

        int threshold = this.accessThreshold(conditionParameter);

        String identifier = UUID.randomUUID().toString();
        AggregationSeries serie = AggregationSeries.builder()
                .id(identifier)
                .function(mapTypeToAggregationFunction(type))
                .field(conditionParameter.get(FIELD).toString())
                .build();

        Expression expression = createExpressionFromThreshold(identifier,
                conditionParameter.get(THRESHOLD_TYPE).toString(),
                threshold);

        return AggregationEventProcessorConfig.builder()
                .query("")
                .streams(new HashSet<>(Collections.singleton(streamID)))
                .series(ImmutableList.of(serie))
                .groupBy(ImmutableList.of())
                .conditions(AggregationConditions.builder()
                        .expression(expression)
                        .build())
                .searchWithinMs(searchWithinMs)
                .executeEveryMs(executeEveryMs)
                .build();
    }

    public EventProcessorConfig createEventConfiguration(AlertType alertType, Map conditionParameter, String streamIdentifier) {
        if (alertType == AlertType.STATISTICAL) {
            return createStatisticalCondition(streamIdentifier, conditionParameter);
        } else {
            return createAggregationCondition(streamIdentifier, conditionParameter);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy