blob: da79faeac7c3148c1ebef987050d5348e07fbd2f [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.metron.threatintel.triage;
import com.google.common.collect.ImmutableList;
import org.adrianwalker.multilinestring.Multiline;
import org.apache.metron.common.configuration.enrichment.SensorEnrichmentConfig;
import org.apache.metron.common.configuration.enrichment.threatintel.RuleScore;
import org.apache.metron.common.configuration.enrichment.threatintel.ThreatScore;
import org.apache.metron.stellar.dsl.Context;
import org.apache.metron.stellar.dsl.StellarFunctions;
import org.apache.metron.common.utils.JSONUtils;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class ThreatTriageTest {
private static final double delta = 1e-10;
/**
* {
* "threatIntel": {
* "triageConfig": {
* "riskLevelRules" : [
* {
* "name": "rule 1",
* "rule": "user.type in [ 'admin', 'power' ] and asset.type == 'web'",
* "score": 10
* },
* {
* "name": "rule 2",
* "comment": "web type!",
* "rule": "asset.type == 'web'",
* "score": 5
* },
* {
* "name": "rule 3",
* "rule": "user.type == 'normal' and asset.type == 'web'",
* "score": 0
* },
* {
* "name": "rule 4",
* "rule": "user.type in whitelist",
* "score": -1,
* "reason": "user.type"
* }
* ],
* "aggregator": "MAX"
* },
* "config": {
* "whitelist": [ "abnormal" ]
* }
* }
* }
*/
@Multiline
public static String smokeTestProcessorConfig;
@Test
public void smokeTest() throws Exception {
ThreatTriageProcessor threatTriageProcessor = getProcessor(smokeTestProcessorConfig);
Assert.assertEquals(
"Expected a score of 0",
0d,
new ThreatTriageProcessor(
new SensorEnrichmentConfig(),
StellarFunctions.FUNCTION_RESOLVER(),
Context.EMPTY_CONTEXT()).apply(
new HashMap<Object, Object>() {{
put("user.type", "admin");
put("asset.type", "web");
}}).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of 10",
10d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "admin");
put("asset.type", "web");
}}
).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of 5",
5d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "normal");
put("asset.type", "web");
}}
).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of 0",
0d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "foo");
put("asset.type", "bar");
}}).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of -Inf",
Double.NEGATIVE_INFINITY,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "abnormal");
put("asset.type", "bar");
}}).getScore(),
1e-10);
}
/**
* Each individual rule that was applied when scoring a threat should
* be captured in the overall threat score.
*/
@Test
public void testThreatScoreWithMultipleRules() throws Exception {
Map<Object, Object> message = new HashMap<Object, Object>() {{
put("user.type", "admin");
put("asset.type", "web");
}};
ThreatScore score = getProcessor(smokeTestProcessorConfig).apply(message);
// expect rules 1 and 2 to have been applied
List<String> expectedNames = ImmutableList.of("rule 1", "rule 2");
Assert.assertEquals(2, score.getRuleScores().size());
score.getRuleScores().forEach(ruleScore ->
Assert.assertTrue(expectedNames.contains(ruleScore.getRule().getName()))
);
}
/**
* Each individual rule that was applied when scoring a threat should
* be captured in the overall threat score.
*/
@Test
public void testThreatScoreWithOneRule() throws Exception {
Map<Object, Object> message = new HashMap<Object, Object>() {{
put("user.type", "abnormal");
put("asset.type", "invalid");
}};
ThreatScore score = getProcessor(smokeTestProcessorConfig).apply(message);
// expect rule 4 to have been applied
List<String> expectedNames = ImmutableList.of("rule 4");
Assert.assertEquals(1, score.getRuleScores().size());
score.getRuleScores().forEach(ruleScore ->
Assert.assertTrue(expectedNames.contains(ruleScore.getRule().getName()))
);
}
/**
* Each individual rule that was applied when scoring a threat should
* be captured in the overall threat score.
*/
@Test
public void testThreatScoreWithNoRules() throws Exception {
Map<Object, Object> message = new HashMap<Object, Object>() {{
put("user.type", "foo");
put("asset.type", "bar");
}};
ThreatScore score = getProcessor(smokeTestProcessorConfig).apply(message);
// expect no rules to have been applied
Assert.assertEquals(0, score.getRuleScores().size());
}
/**
* {
* "threatIntel": {
* "triageConfig": {
* "riskLevelRules" : [
* {
* "rule" : "user.type in [ 'admin', 'power' ] and asset.type == 'web'",
* "score" : 10
* },
* {
* "rule" : "asset.type == 'web'",
* "score" : 5
* },
* {
* "rule" : "user.type == 'normal' and asset.type == 'web'",
* "score" : 0
* }
* ],
* "aggregator" : "POSITIVE_MEAN"
* }
* }
* }
*/
@Multiline
public static String positiveMeanProcessorConfig;
@Test
public void testPositiveMeanAggregationScores() throws Exception {
ThreatTriageProcessor threatTriageProcessor = getProcessor(positiveMeanProcessorConfig);
Assert.assertEquals(
"Expected a score of 0",
5d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "normal");
put("asset.type", "web");
}}).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of 7.5",
(10 + 5)/2.0,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "admin");
put("asset.type", "web");
}}).getScore(),
1e-10);
Assert.assertEquals(
"Expected a score of 0",
0d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("user.type", "foo");
put("asset.type", "bar");
}}).getScore(),
1e-10);
}
/**
* {
* "threatIntel" : {
* "triageConfig": {
* "riskLevelRules": [
* {
* "rule" : "not(IN_SUBNET(ip_dst_addr, '192.168.0.0/24'))",
* "score" : 10
* }
* ],
* "aggregator" : "MAX"
* }
* }
* }
*/
@Multiline
private static String testWithStellarFunction;
@Test
public void testWithStellarFunction() throws Exception {
ThreatTriageProcessor threatTriageProcessor = getProcessor(testWithStellarFunction);
Assert.assertEquals(
10d,
threatTriageProcessor.apply(
new HashMap<Object, Object>() {{
put("ip_dst_addr", "172.2.2.2");
}}).getScore(),
1e-10);
}
/**
* {
* "threatIntel": {
* "triageConfig": {
* "riskLevelRules" : [
* {
* "name": "Rule Name",
* "comment": "Rule Comment",
* "rule": "2 == 2",
* "score": 10,
* "reason": "variable.name"
* }
* ],
* "aggregator": "MAX"
* }
* }
* }
*/
@Multiline
public static String testReasonConfig;
/**
* The 'reason' field contained within a rule is a Stellar expression that is
* executed within the context of the message that the rule is applied to.
*/
@Test
public void testReason() throws Exception {
Map<Object, Object> message = new HashMap<Object, Object>() {{
put("variable.name", "variable.value");
}};
ThreatScore score = getProcessor(testReasonConfig).apply(message);
assertEquals(1, score.getRuleScores().size());
for(RuleScore ruleScore : score.getRuleScores()) {
// the 'reason' is the result of executing the rule's 'reason' expression
assertEquals("variable.value", ruleScore.getReason());
}
}
/**
* If the 'reason' expression refers to a missing variable (the result
* of a data quality issue) it should not throw an exception.
*/
@Test
public void testInvalidReason() throws Exception {
Map<Object, Object> message = new HashMap<Object, Object>() {{
// there is no 'variable.name' in the message
}};
ThreatScore score = getProcessor(testReasonConfig).apply(message);
assertEquals(1, score.getRuleScores().size());
for(RuleScore ruleScore : score.getRuleScores()) {
// the 'reason' is the result of executing the rule's 'reason' expression
assertEquals(null, ruleScore.getReason());
}
}
/**
* {
* "threatIntel" : {
* "triageConfig": {
* "riskLevelRules": [
* {
* "rule" : "true",
* "score" : 10
* }
* ],
* "aggregator" : "MAX"
* }
* }
* }
*/
@Multiline
private static String shouldAllowNumericRuleScore;
@Test
public void shouldAllowNumericRuleScore() throws Exception {
Map<String, Object> message = new HashMap<>();
ThreatTriageProcessor threatTriageProcessor = getProcessor(shouldAllowNumericRuleScore);
Assert.assertEquals(10d, threatTriageProcessor.apply(message).getScore(), 1e-10);
}
/**
* {
* "threatIntel" : {
* "triageConfig": {
* "riskLevelRules": [
* {
* "rule" : "true",
* "score" : "priority * 10.1"
* }
* ],
* "aggregator" : "MAX"
* }
* }
* }
*/
@Multiline
private static String shouldAllowScoreAsStellarExpression;
@Test
public void shouldAllowScoreAsStellarExpression() throws Exception {
// the message being triaged has a field 'priority' that is referenced in the score expression
Map<Object, Object> message = new HashMap<Object, Object>() {{
put("priority", 100);
}};
ThreatTriageProcessor threatTriageProcessor = getProcessor(shouldAllowScoreAsStellarExpression);
Assert.assertEquals(1010.0d, threatTriageProcessor.apply(message).getScore(), 1e-10);
}
private static ThreatTriageProcessor getProcessor(String config) throws IOException {
SensorEnrichmentConfig c = JSONUtils.INSTANCE.load(config, SensorEnrichmentConfig.class);
return new ThreatTriageProcessor(c, StellarFunctions.FUNCTION_RESOLVER(), Context.EMPTY_CONTEXT());
}
}