def is_point_equality_ioc(pattern_str: str) -> bool: """ Predicate to check if a STIX-2 pattern is a point-IoC, i.e., if the pattern only consists of a single EqualityComparisonExpression @param pattern_str The STIX-2 pattern string to inspect """ try: pattern = Pattern(pattern_str) # InspectionListener https://github.com/oasis-open/cti-pattern-validator/blob/e926d0a14adf88de08acb908a51db1f453c13647/stix2patterns/v21/inspector.py#L5 # E.g., pattern = "[domain-name:value = 'evil.com']" # => il = pattern_data(comparisons={'domain-name': [(['value'], '=', "'evil.com'")]}, observation_ops=set(), qualifiers=set()) # => cybox_types = ['domain-name'] il = pattern.inspect() cybox_types = list(il.comparisons.keys()) return ( len(il.observation_ops) == 0 and len(il.qualifiers) == 0 and len(il.comparisons) == 1 and len(cybox_types) == 1 # must be point-indicator (one field only) and len(il.comparisons[cybox_types[0]][0]) == 3 # ('value', '=', 'evil.com') and il.comparisons[cybox_types[0]][0][1] == "=" # equality comparison ) except Exception: return False
def test_config_continue_path_through_ref_always(num_trials, default_object_generator): pattern_config = Config( min_pattern_size=5, max_pattern_size=10, probability_continue_path_through_ref=1, ) generator = PatternGenerator(default_object_generator, "2.1", pattern_config) for _ in range(num_trials): pattern_str = generator.generate() pattern_obj = Pattern(pattern_str) pattern_data = pattern_obj.inspect() for comparisons in pattern_data.comparisons.values(): for path, _, _ in comparisons: # If all paths continue through refs, a ref prop can't be last, # and a refs prop can't be second-to-last. path_len = len(path) for i, path_elt in enumerate(path): if isinstance(path_elt, str): assert not path_elt.endswith( "_ref") or i < path_len - 1 assert not path_elt.endswith( "_refs") or i < path_len - 2
def test_config_repeat_count(num_trials, default_object_generator): pattern_config = Config( min_repeat_count=3, max_repeat_count=7, # Ensure there are lots of qualifiers, to increase the probability we # will actually see some REPEATS qualifiers to test. There's not # actually any guarantee any will show up in the pattern! # ("probability_qualifier=1" below guarantees some qualifiers will be # included, but not which type of qualifiers they are.) min_pattern_size=5, max_pattern_size=10, probability_qualifier=1, ) generator = PatternGenerator(default_object_generator, "2.1", pattern_config) for _ in range(num_trials): pattern_str = generator.generate() pattern_obj = Pattern(pattern_str) pattern_data = pattern_obj.inspect() for qualifier in pattern_data.qualifiers: m = _REPEATS_RE.match(qualifier) if m: repeat_count = int(m.group(1)) assert 3 <= repeat_count <= 7
def test_config_probability_index_star_step_never( num_trials, default_object_generator ): pattern_config = Config( min_pattern_size=5, max_pattern_size=10, probability_index_star_step=0, ) generator = PatternGenerator( default_object_generator, "2.1", pattern_config ) for _ in range(num_trials): pattern_str = generator.generate() pattern_obj = Pattern(pattern_str) pattern_data = pattern_obj.inspect() for comparisons in pattern_data.comparisons.values(): for path, _, _ in comparisons: # If always using integer steps into lists, there should never # be any star steps. assert not any( path_elt is stix2patterns.inspector.INDEX_STAR for path_elt in path )
def test_config_pattern_size(num_trials, default_object_generator): pattern_config = Config(min_pattern_size=3, max_pattern_size=7) generator = PatternGenerator(default_object_generator, "2.1", pattern_config) for _ in range(num_trials): pattern_str = generator.generate() pattern_obj = Pattern(pattern_str) pattern_data = pattern_obj.inspect() comparison_data = pattern_data.comparisons pattern_size = sum( len(comparisons) for comparisons in comparison_data.values()) assert 3 <= pattern_size <= 7
def test_config_within_count(num_trials, default_object_generator): pattern_config = Config( min_within_count=3, max_within_count=7, min_pattern_size=5, max_pattern_size=10, probability_qualifier=1, ) generator = PatternGenerator(default_object_generator, "2.1", pattern_config) for _ in range(num_trials): pattern_str = generator.generate() pattern_obj = Pattern(pattern_str) pattern_data = pattern_obj.inspect() for qualifier in pattern_data.qualifiers: m = _WITHIN_RE.match(qualifier) if m: within_count = int(m.group(1)) assert 3 <= within_count <= 7
def test_comparisons(pattern, expected_comparisons): compiled_pattern = Pattern(pattern) pattern_data = compiled_pattern.inspect() assert pattern_data.comparisons == expected_comparisons
def test_observation_ops(pattern, expected_obs_ops): compiled_pattern = Pattern(pattern) pattern_data = compiled_pattern.inspect() assert pattern_data.observation_ops == expected_obs_ops
def test_qualifiers(pattern, expected_qualifiers): compiled_pattern = Pattern(pattern) pattern_data = compiled_pattern.inspect() assert pattern_data.qualifiers == expected_qualifiers
def patterns(instance, options): """Ensure that the syntax of the pattern of an indicator is valid, and that objects and properties referenced by the pattern are valid. """ if (instance['type'] != 'indicator' or instance.get('pattern_type', '') != 'stix' or isinstance(instance.get('pattern', None), str) is False): return pattern = instance['pattern'] if 'pattern_version' in instance: pattern_version = instance['pattern_version'] elif 'spec_version' in instance: pattern_version = instance['spec_version'] else: pattern_version = '2.1' errors = pattern_validator(pattern, pattern_version) # Check pattern syntax if errors: for e in errors: yield PatternError(str(e), instance['id']) return p = Pattern(pattern) inspection = p.inspect().comparisons for objtype in inspection: # Check observable object types if objtype in enums.OBSERVABLE_TYPES: pass elif (not TYPE_FORMAT_RE.match(objtype) or len(objtype) < 3 or len(objtype) > 250): yield PatternError("'%s' is not a valid observable type name" % objtype, instance['id']) elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix']) and 'extensions-use' in options.disabled and not CUSTOM_TYPE_PREFIX_RE.match(objtype)): yield PatternError("Custom Observable Object type '%s' should start " "with 'x-' followed by a source unique identifier " "(like a domain name with dots replaced by " "hyphens), a hyphen and then the name" % objtype, instance['id']) elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix-lax']) and 'extensions-use' in options.disabled and not CUSTOM_TYPE_LAX_PREFIX_RE.match(objtype)): yield PatternError("Custom Observable Object type '%s' should start " "with 'x-'" % objtype, instance['id']) # Check observable object properties expression_list = inspection[objtype] for exp in expression_list: path = exp[0] # Get the property name without list index, dictionary key, or referenced object property prop = path[0] if objtype in enums.OBSERVABLE_PROPERTIES and prop in enums.OBSERVABLE_PROPERTIES[objtype]: continue elif not PROPERTY_FORMAT_RE.match(prop): yield PatternError("'%s' is not a valid observable property name" % prop, instance['id']) elif objtype not in enums.OBSERVABLE_TYPES: continue # custom SCOs aren't required to use x_ prefix on properties elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix']) and 'extensions-use' in options.disabled and not CUSTOM_PROPERTY_PREFIX_RE.match(prop)): yield PatternError("Cyber Observable Object custom property '%s' " "should start with 'x_' followed by a source " "unique identifier (like a domain name with " "dots replaced by underscores), an " "underscore and then the name" % prop, instance['id']) elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix-lax']) and 'extensions-use' in options.disabled and not CUSTOM_PROPERTY_LAX_PREFIX_RE.match(prop)): yield PatternError("Cyber Observable Object custom property '%s' " "should start with 'x_'" % prop, instance['id'])