design.unstructured.stix.evaluator.mapper.StixMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of stix-pattern-evaluator Show documentation
Show all versions of stix-pattern-evaluator Show documentation
A Cyber Threat Intelligence (CTI) STIX v2.1 pattern compiler and expression evaluator
/*
* stix-pattern-evaluator
* Copyright (C) 2020 - Christopher Carver
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package design.unstructured.stix.evaluator.mapper;
import com.google.common.base.CaseFormat;
import design.unstructured.stix.evaluator.mapper.annotations.StixAnnotationType;
import design.unstructured.stix.evaluator.mapper.annotations.StixEntity;
import design.unstructured.stix.evaluator.mapper.annotations.StixProperty;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The purpose of this class is to bring the Cyber Observable data model to your
* java objects. This implementation is still a WIP and should be used with
* caution.
*
* @author ccarv
*/
public class StixMapper implements ObjectPathResolver {
private static final Logger logger = LoggerFactory.getLogger(StixMapper.class);
private final Map, StixAnnotationType> observables = new HashMap<>();
private final Map observableTree = new HashMap<>();
private final List pathFilter = new ArrayList<>();
/**
* Use to split STIX observable object path (process:parent_ref:name) into a
* string array.
*/
private final Function propertySplitter = property -> {
String propertyReplaced = property;
for (String filter : pathFilter) {
propertyReplaced = propertyReplaced.replace(filter + ".", "");
}
return propertyReplaced.split("(\\.)|(:)");
};
/**
* Joins a string array back to a STIX observable object path.
*/
private final Function propertyJoiner = properties -> {
StringBuilder joined = new StringBuilder();
for (int i = 0; i < properties.length; i++) {
char delimeter = (i == 0 ? ':' : '.');
joined.append(properties[i]).append(delimeter);
}
return joined.substring(0, joined.length() - 1);
};
/**
* The StixObservableTreeIntrospector uses reflection (type introspection) to
* build a STIX observable tree. This tree is essentially a cache of resolvable
* STIX observables to their Java object counterparts. Object path collisions
* will throw an exception.
*/
class StixObservableTreeIntrospector {
private final Set> visitedClasses = new HashSet<>();
private final Map tree = new HashMap<>();
private final StixEntity entity;
private final Class> clazz;
StixObservableTreeIntrospector(final Class> clazz) {
this.entity = clazz.getAnnotation(StixEntity.class);
this.clazz = clazz;
}
public Map map() {
logger.debug("-> analyzing entity {}", clazz);
StixObservablePropertyNode node = null;
String entityName = entity.name();
if (entityName.isEmpty()) {
entityName = clazz.getSimpleName().toLowerCase();
logger.trace("--> @StixEntity did not have a name, using class name as observable", entityName);
}
logger.debug("--> @StixEntity '{}'", entityName);
node = new StixObservablePropertyNode(null, entityName, null, clazz, false);
tree.put(entityName, node);
build(clazz, node);
return tree;
}
private void build(Class> clazz, StixObservablePropertyNode nodeParent) {
// Need to keep track of classes that have already been mapped to avoid infinite
// recursion
visitedClasses.add(clazz);
getFields(clazz).entrySet().forEach((entry) -> {
boolean isReferenceNode = entry.getKey().isEmpty();
Field field = entry.getValue();
String propertyName = (!isReferenceNode ? entry.getKey() : "&" + field.getName());
String[] properties = propertySplitter.apply(propertyName);
Boolean isGeneric = isGenericJavaType(field.getType());
if (!observables.containsKey(field.getType()) && !isGeneric) {
logger.warn(
"field '{}' type '{}' has no interpreter and is not available as a @StixObject, add @StixObject annotation to custom type or add a custom interpreter.",
field.getName(), field.getType().getName());
} else {
if (properties.length > 1) {
logger.trace("@StixProperty {} is overriding the path but thats OK. :)", propertyName);
}
StixObservablePropertyNode node = new StixObservablePropertyNode(nodeParent, propertyName, field,
clazz, isReferenceNode);
field.setAccessible(true);
tree.put(isReferenceNode ? node.toPath() + ":" + propertyName : node.toPath(), node);
if (nodeParent != null) {
nodeParent.addChild(propertyName, node);
}
logger.debug("mapped field '{}' => '{}' {fullPath={}}", field.getName(), propertyName,
node.toPath());
if (!isGeneric && !visitedClasses.contains(field.getType())) {
logger.trace("entering nested field '{}=>{}'...", field.getName(), propertyName);
build(field.getType(), node);
} else if (visitedClasses.contains(field.getType())) {
logger.trace("nested field '{}=>{}' type '{}' has already been mapped", field.getName(),
propertyName, field.getType());
}
}
});
}
}
/**
* Provide a set of classes that use one of the STIX annotations from the mapper
* annotations package.
*
* @see design.unstructured.stix.evaluator.mapper.annotations}
* @param stixClasses
*/
public StixMapper(final Set> stixClasses) {
logger.debug("scanning for @StixEntity and @StixObject in {} classes", stixClasses.size());
// Scan for STIX Entity annotations
stixClasses.forEach((clazz) -> {
logger.debug("class [{}] matched stix annotation type, scanning for properties...", clazz.getName());
observables.put(clazz, (clazz.isAnnotationPresent(StixEntity.class) ? StixAnnotationType.ENTITY
: StixAnnotationType.OBJECT));
});
logger.debug("finished scanning metagrid system artifacts, found {} stix observables", observables.size());
logger.debug("building stix observable tree on all @StixEntity...");
observables.entrySet().stream().filter((entry) -> entry.getValue().equals(StixAnnotationType.ENTITY))
.forEach(entry -> {
Class> clazz = entry.getKey();
observableTree.putAll(new StixObservableTreeIntrospector(clazz).map());
});
logger.debug("finished building stix observable tree");
if (logger.isTraceEnabled()) {
observableTree.entrySet().forEach(
(entry) -> logger.trace("Observable: " + entry.getKey() + " = " + entry.getValue().toString()));
}
}
/**
* Adds a path filter that will be used during getFilter(...).
*/
public void addPathFilter(final String filter) {
pathFilter.add(filter);
}
/**
* Gets the value from an object using the specified object path. The path must
* point to a valid property, otherwise an empty string value will be returned.
*
* @param object
* @param path
* @return
* @throws StixMapperException
*/
@Override
public Object getValue(final Object object, final String path) throws StixMapperException {
Object value = null;
if (observables.containsKey(object.getClass())) {
List nodePath = buildNodePath(path);
if (nodePath != null) {
Object instance = object;
for (StixObservablePropertyNode node : nodePath) {
logger.trace("observing {} node '{}'...", node.isReference() ? "reference" : "property",
node.getName());
if (node.getField() != null && instance != null) {
try {
instance = node.getField().get(instance);
} catch (IllegalArgumentException | IllegalAccessException ex) {
throw new StixMapperException(ex.getMessage());
}
} else if (instance != null) {
logger.trace("property node '{}' is an entity and has no nested type, skipping",
node.getName());
}
}
value = instance;
}
}
return value;
}
/**
*
* @return
*/
private List buildNodePath(final String path) throws StixMapperException {
List nodePath;
StixObservablePropertyNode node;
logger.trace("analyzing path '{}' for observables", path);
if ((node = observableTree.get(path)) != null) {
// Our path was available in the stixTree
nodePath = node.getPath();
logger.trace("found observable node directly from path string '{}', navigating path", path);
} else {
// Our path wasn't available, need to walk the tree manually
String[] properties = propertySplitter.apply(path);
nodePath = new ArrayList<>();
node = observableTree.get(properties[0]);
if (node == null) {
throw new StixMapperException("Unable to find root observable node for path '" + path
+ "', the property '" + properties[0] + "' was not found");
}
nodePath.add(node);
for (int i = 1; i < properties.length; i++) {
node = node.getChildren().get(properties[i]);
if (node != null) {
logger.trace("found child node '{}'", node.getName());
nodePath.add(node);
if (observables.get(node.getClazz()).equals(StixAnnotationType.ENTITY)) {
String newPath = propertyJoiner.apply(ArrayUtils.remove(properties, i));
logger.trace(
"child node '{}' class type is type @StixEntity, using existing cache for lookup of path '{}'",
node.getName(), newPath);
nodePath.addAll(buildNodePath(newPath));
break;
}
} else {
throw new StixMapperException("Unable to find observable node for path '" + path + "', property '"
+ properties[i] + "' was not found");
}
}
}
return nodePath;
}
private static Map getFields(Class> clazz) {
return getFields(clazz, false);
}
private static Map getFields(Class> clazz, boolean filterForStixProperties) {
Map fields = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
// Check to see if StixProperty overrides the field name
if (field.isAnnotationPresent(StixProperty.class)) {
for (String property : field.getAnnotation(StixProperty.class).name()) {
fields.put(property, field);
logger.trace("@StixProperty annotation found [{} => {}::{}]", property, clazz.getSimpleName(),
field.getName());
}
} else if (!filterForStixProperties) {
fields.put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()), field);
}
}
return fields;
}
public static boolean isGenericJavaType(Class> type) {
return (type.isPrimitive() && type != void.class) || type == Double.class || type == Float.class
|| type == Long.class || type == Integer.class || type == Short.class || type == Character.class
|| type == Byte.class || type == Boolean.class || type == String.class;
}
/**
* Returns a map of STIX observable classes. These are the same classes passed
* to the constructor that contained either a {@code StixEntity} or
* {@code StixObject} annotation.
*
* @return
*/
public Map, StixAnnotationType> getObservables() {
return observables;
}
/**
* Returns the STIX observable tree. Your key will represent the STIX Observable
* as a string and the value will be a {@code StixObservablePropertyNode}.
*
* @return
*/
public Map getObservableTree() {
return observableTree;
}
}