def handle_campaign(config: Dict[Text, Any], incident: Dict[Text, Any], incident_id: Text) -> None: """ Create incident -attributedTo-> campaign facts If we have a mapping from campaign (UUID) to name, a campaign -name-> will also be created """ campaign = ("{}-{}".format(config["veris_prefix"], incident["campaign_id"]) if incident.get("campaign_id") else None) if not campaign: return handle_fact( config["actapi"].fact("attributedTo").source("incident", incident_id).destination( "campaign", campaign), output_format=config["output_format"], ) name = config["campaign_map"].get(format(campaign)) if name: handle_fact( config["actapi"].fact("name", name).source("campaign", campaign), output_format=config["output_format"], ) else: warning( "No name found for campaign {}. Make sure veris-campaign is provided and the ID is included in csv field (without prefix)" .format(campaign))
def handle_organizations( config: Dict[Text, Any], incident: Dict[Text, Any], incident_id: Text ) -> None: """ Create facts: * incident -targets-> organization * organization -locatedIn-> country """ victim = incident.get("victim", {}).get("victim_id") if not victim: return handle_fact( config["actapi"] .fact("targets") .source("incident", incident_id) .destination("organization", victim), output_format=config["output_format"], ) for country_code in incident.get("victim", {}).get("country", []): country = config["cn_map"].get(country_code) if country: handle_fact( config["actapi"] .fact("locatedIn") .source("organization", victim) .destination("country", country), output_format=config["output_format"], )
def process( actapi: act.api.Act, country_list: List[Dict[str, str]], output_format: Text = "json", ) -> None: """ Loop over all ISO-3166 countries and construct facts for county -memberOf-> subRegion and subRegion -memberOf-> region. """ for c_map in country_list: country_name = c_map["name"] sub_region = c_map["sub-region"] region = c_map["region"] if country_name and sub_region: handle_fact( actapi.fact("memberOf") .source("country", country_name) .destination("subRegion", sub_region), output_format=output_format, ) else: warning("Missing name or sub-region: {}".format(c_map)) if sub_region and region: handle_fact( actapi.fact("memberOf") .source("subRegion", sub_region) .destination("region", region), output_format=output_format, ) else: warning("Missing sub-region or region: {}".format(c_map))
def add_sectors(client: act.api.Act, ta_cards: List, vocab: List, output_format: Text = "json") -> None: """ Only submit sectors if in STIX vocabulary """ for actor in ta_cards: if "observed-sectors" in actor: for sector in actor["observed-sectors"]: if sector.lower( ) in vocab["definitions"]["industry-sector-ov"]["enum"]: for ta in actor["actor"].split(","): chain = act.api.fact.fact_chain( client.fact("memberOf").source( "organization", "*").destination("sector", sector.lower()), client.fact("targets").source("incident", "*").destination( "organization", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ) for fact in chain: handle_fact(fact, output_format=output_format)
def handle_reports( config: Dict[Text, Any], incident: Dict[Text, Any], incident_id: Text ) -> None: """ Extract all references in incident. For each (URL-)reference, check whether we should download the content and creaa a sha256 hash digest of it (must match config["hash_url_matching"]). The sha256 hash is also cached locally. """ # Split references by ";" references = [ ref.strip() for ref in incident.get("reference", "").split(";") if ref.strip() ] if references: for ref in references: if not re.search(config["hash_url_matching"], ref): continue report_hash = config["db_cache"].query_download_update(ref) if report_hash: handle_fact( config["actapi"] .fact("mentions") .source("report", report_hash) .destination("incident", incident_id), output_format=config["output_format"], )
def handle_threat_actor( config: Dict[Text, Any], incident: Dict[Text, Any], incident_id: Text ) -> None: """ Creat facts from actor->external, where the variety includes at least on variety specified in config["threat_actor_variety"] """ external_actor = incident.get("actor", {}).get("external", {}) if not external_actor: return # No threat actors # Varieties are "tags" on actors and we do not want to include all type of actors # Make sure at least one of the varieties are in the list of the configured varieties to include if any( [ variety in config["threat_actor_variety"] for variety in external_actor.get("variety", []) ] ): threat_actors = [ ta.strip() for ta in incident.get("actor", {}).get("external", {}).get("name", []) ] for ta in threat_actors: handle_fact( config["actapi"] .fact("attributedTo") .source("incident", incident_id) .destination("threatActor", ta), output_format=config["output_format"], )
def add_to_act(actapi: act.api.Act, doc: Dict, output_format: Text = "json") -> None: """Add a report to the ACT platform""" report_id: Text = doc["hexdigest"] title: Text = doc.get("title", "No title") indicators: Dict = doc.get("indicators", {}) try: # Report title handle_fact( actapi.fact("name", title).source("report", report_id), output_format) except act.api.base.ResponseError as e: error("Unable to create fact: %s" % e) # Loop over all items under indicators in report for scio_indicator_type in EXTRACT_INDICATORS: # Get object type from ACT (default to object type in SCIO) act_indicator_type = SCIO_INDICATOR_ACT_MAP.get( scio_indicator_type, scio_indicator_type) report_mentions_fact(actapi, act_indicator_type, indicators.get(scio_indicator_type, []), report_id, output_format) # For SHA256, create content object for sha256 in list(set(indicators.get("sha256", []))): handle_fact( actapi.fact("represents").source("hash", sha256).destination( "content", sha256), output_format) # Add all URI components for uri in list(set(indicators.get("uri", []))): try: handle_uri(actapi, uri, output_format=output_format) except act.api.schema.MissingField: error("Unable to create facts from uri: {}".format(uri)) # Locations (countries, regions, sub regions) for location_type in EXTRACT_GEONAMES: locations = doc.get("geonames", {}).get(location_type, []) report_mentions_fact(actapi, SCIO_GEONAMES_ACT_MAP[location_type], locations, report_id, output_format) # Threat actor report_mentions_fact(actapi, "threatActor", doc.get("threat-actor", {}).get("names", []), report_id, output_format) # Tools report_mentions_fact( actapi, "tool", [tool.lower() for tool in doc.get("tools", {}).get("names", [])], report_id, output_format) # Sector report_mentions_fact(actapi, "sector", doc.get("sectors", []), report_id, output_format)
def handle_tool(config: Dict[Text, Any], incident: Dict[Text, Any], incident_id: Text) -> None: "Create content -classifiedAs-> tool, and fact chain from content to incident. " # Both "," and ";" are used to separate tools :( tools = [ malware.strip().lower() for malware in re.split( r';|,', incident.get("action", {}).get("malware", {}).get("name", "")) if malware ] for tool in tools: chain = act.api.fact.fact_chain( config["actapi"].fact("classifiedAs").source("content", "*").destination( "tool", tool), config["actapi"].fact("observedIn").source("content", "*").destination( "event", "*"), config["actapi"].fact("attributedTo").source( "event", "*").destination("incident", incident_id), ) for fact in chain: handle_fact(fact, output_format=config["output_format"])
def process(client: act.api.Act, ta_cards: List, output_format: Text = "json") -> None: "Extract threat actor cards from ThaiCERT" # Keep list of names with an alias points to # There should only be one to have a meaningfull alias ta_alias_names = defaultdict(set) ta_aliases = {} for actor in ta_cards: ta_name = actor["names"][0]["name"] ta_aliases[ta_name] = [] for alias in actor["names"][1:]: alias_name = alias["name"] # names might be identical after format/normalization if ta_name == alias_name: continue ta_alias_names[alias_name].add(ta_name) ta_aliases[ta_name].append(alias_name) for ta_name in ta_aliases: for alias_name in ta_aliases[ta_name]: # This alias is mentioned for multiple main threat # actors so we skip this alias if len(ta_alias_names[alias_name]) > 1: warning( f"Skipping TA alias {alias_name} <-> {ta_name} since {alias_name} is alias for multiple names: {ta_alias_names[alias_name]}" ) continue handle_fact( client.fact("alias").bidirectional("threatActor", ta_name, "threatActor", alias_name), output_format=output_format, ) for actor in ta_cards: if "operations" in actor: for operation in actor["operations"]: if len(operation["activity"].split("\n")[0].split()) < 4: for ta in actor["actor"].split(","): handle_facts( act.api.fact.fact_chain( client.fact("attributedTo").source( "incident", "*").destination( "campaign", operation["activity"].split("\n")[0]), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ), output_format=output_format, )
def add_groups(client: Act, matrice: AttckMatrice, output_format: Text = "json") -> List: """ extract objects/facts related to ATT&CK Threat Actors Args: attack (AttckMatrice): Attack matrice output_format (Text): "json" or "str" output format """ notify: List = [] for actor in matrice.actors: if deprecated_or_revoked(actor): # Object is revoked, add to notification list but do not add to facts that should be added to the platform notify.append(actor) continue for alias in actor.alias: if actor.name != alias: handle_fact( client.fact("alias").bidirectional( "threatActor", actor.name, "threatActor", alias, ), output_format=output_format, ) for tool in actor.known_tools: handle_facts(act.api.fact.fact_chain( client.fact("classifiedAs").source("content", "*").destination( "tool", tool), client.fact("observedIn").source("content", "*").destination( "incident", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", actor.name), ), output_format=output_format) for technique in actor.techniques: handle_facts(act.api.fact.fact_chain( client.fact("observedIn").source("technique", technique.id).destination( "incident", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", actor.name), ), output_format=output_format) return notify
def add_ta_campaign(client: Act, output_format: Text, threat_actor: Text, campaign: Text) -> None: """ Threat Actor Campaign """ chain = act.api.fact.fact_chain( client.fact("attributedTo").source("incident", "*").destination( "campaign", campaign), client.fact("attributedTo").source("incident", "*").destination( "threatActor", threat_actor)) for fact in chain: handle_fact(fact, output_format=output_format)
def report_mentions_fact(actapi: act.api.Act, object_type: Text, object_values: List[Text], report_id: Text, output_format: Text) -> None: """Add mentions fact to report""" for value in list(set(object_values)): try: handle_fact( actapi.fact("mentions", object_type).source( "report", report_id).destination(object_type, value), output_format) except act.api.base.ResponseError as e: error("Unable to create linked fact: %s" % e)
def add_ta_located_in(client: Act, output_format: Text, threat_actor: Text, located_in: Text) -> None: """ Threat actor located in """ chain = act.api.fact.fact_chain( client.fact("locatedIn").source("organization", "*").destination( "country", located_in), client.fact("attributedTo").source("threatActor", threat_actor).destination( "organization", "*")) for fact in chain: handle_fact(fact, output_format=output_format)
def add_ta_sectors(client: Act, output_format: Text, threat_actor: Text, sectors: List[Text]) -> None: """ Threat Actor Sectors """ for sector in sectors: chain = act.api.fact.fact_chain( client.fact("targets").source("incident", "*").destination( "organization", "*"), client.fact("memberOf").source("organization", "*").destination("sector", sector), client.fact("attributedTo").source("incident", "*").destination( "threatActor", threat_actor)) for fact in chain: handle_fact(fact, output_format=output_format)
def add_ta_target_country(client: Act, output_format: Text, threat_actor: Text, target_countries: List[Text]) -> None: """ Threat actor target countries """ for target_country in target_countries: chain = act.api.fact.fact_chain( client.fact("targets").source("incident", "*").destination( "organization", "*"), client.fact("locatedIn").source("organization", "*").destination( "country", target_country), client.fact("attributedTo").source("incident", "*").destination( "threatActor", threat_actor)) for fact in chain: handle_fact(fact, output_format=output_format)
def report_mentions_fact( actapi: act.api.Act, object_type: Text, object_values: Set[Text], report_id: Text, output_format: Text, ) -> None: """Add mentions fact to report""" for value in set(object_values): handle_fact( actapi.fact("mentions").source("report", report_id).destination( object_type, value), output_format, )
def add_ta_techniques(client: Act, output_format: Text, threat_actor: Text, techniques: List[Text]) -> None: """ Threat Actor Techniques """ for technique in techniques: chain = act.api.fact.fact_chain( client.fact("attributedTo").source("incident", "*").destination( "threatActor", threat_actor), client.fact("attributedTo").source("event", "*").destination( "incident", "*"), client.fact("classifiedAs").source("event", "*").destination( "technique", technique)) for fact in chain: handle_fact(fact, output_format=output_format)
def test_validate_same_object() -> None: api = act.api.Act( "", None, strict_validator=True, object_formatter=object_format, object_validator=object_validates, ) act.api.helpers.handle_fact.cache_clear() with pytest.raises(ValidationError, match=r"Source object can not be equal to.*"): handle_fact( api.fact("mentions").source("report", "xyz").destination("report", "xyz"))
def test_validator_strict() -> None: api = act.api.Act( "", None, strict_validator=True, object_formatter=object_format, object_validator=object_validates, ) act.api.helpers.handle_fact.cache_clear() with pytest.raises(ValidationError, match=r"Destination object does not validate.*"): handle_fact( api.fact("mentions").source("report", "xyz").destination( "uri", ".X7f://cve-2014-0224"))
def add_software(client: Act, matrice: AttckMatrice, output_format: Text = "json") -> List: """ extract objects/facts related to ATT&CK Software Insert to ACT if client.baseurl is set, if not, print to stdout Args: attack (AttckMatrice): Attack matrice output_format (Text): "json" or "str" output format """ notify: List = [] # Enterprise matrice has malwares and tools, but preattack has none of them for software in getattr(matrice, "malwares", []) + getattr( matrice, "tools", []): if deprecated_or_revoked(software): # Object is revoked/deprecated, add to notification list but do not add to facts that should be added to the platform notify.append(software) continue tool_name = software.name # Tool category handle_fact( client.fact("category", software.type).source("tool", tool_name), output_format=output_format, ) for alias in software.alias: alias_name = alias if tool_name != alias_name: # Tool category (alias) handle_fact( client.fact("category", software.type).source("tool", alias_name), output_format=output_format, ) handle_fact( client.fact("alias").bidirectional("tool", tool_name, "tool", alias_name), output_format=output_format, ) for technique in software.techniques: handle_fact( client.fact("implements").source("tool", software.name).destination( "technique", technique.id), output_format=output_format, ) return notify
def add_software(client, attack: MemoryStore, output_format: Text = "json") -> List[stix2.AttackPattern]: """ extract objects/facts related to ATT&CK Software Insert to ACT if client.baseurl is set, if not, print to stdout Args: attack (stix2): Stix attack instance """ notify = [] for software in attack.query([Filter("type", "in", ["tool", "malware"])]): tool_name = software.name.lower() # Tool category handle_fact(client.fact("category", software.type).source("tool", tool_name), output_format=output_format) if getattr(software, "revoked", None): # Object is revoked, add to notification list but do not add to facts that should be added to the platform notify.append(software) continue if getattr(software, "x_mitre_deprecated", None): # Object is revoked, add to notification list AND continue to add to facts that should be added to the platform notify.append(software) for alias in getattr(software, "x_mitre_aliases", []): if tool_name != alias.lower(): # Tool category (alias) handle_fact(client.fact("category", software.type).source( "tool", alias.lower()), output_format=output_format) handle_fact(client.fact("alias").bidirectional( "tool", tool_name, "tool", alias.lower()), output_format=output_format) # ATT&CK concept STIX Properties # ========================================================================== # Technqiues relationship where relationship_type == "uses", points to # a target object with type == "attack-pattern" for technique in attack.related_to(software, relationship_type="uses"): if technique.type != "attack-pattern": continue handle_fact(client.fact("implements").source( "tool", software.name.lower()).destination("technique", technique.name), output_format=output_format) return notify
def add_ta_tools(client: Act, output_format: Text, threat_actor: Text, tools: List[Text]) -> None: """ Threat Actor Tools """ for tool in tools: chain = act.api.fact.fact_chain( client.fact("classifiedAs").source("content", "*").destination( "tool", tool.lower()), client.fact("observedIn").source("content", "*").destination("event", "*"), client.fact("attributedTo").source("event", "*").destination( "incident", "*"), client.fact("attributedTo").source("incident", "*").destination( "threatActor", threat_actor)) for fact in chain: handle_fact(fact, output_format=output_format)
def add_countries(client: act.api.Act, ta_cards: List, countries: List, output_format: Text = "json") -> None: """ Only submit country if ISO-3166 country """ for actor in ta_cards: if "country" in actor: for country in actor["country"]: if country.lower() in countries: chain = act.api.fact.fact_chain( client.fact("locatedIn").source("organization", "*").destination( "country", country), client.fact("attributedTo").source( "threatActor", actor["actor"]).destination("organization", "*"), ) for fact in chain: handle_fact(fact, output_format=output_format) if "observed-countries" in actor: for country in actor["observed-countries"]: if country.lower() in countries: for ta in actor["actor"].split(","): chain = act.api.fact.fact_chain( client.fact("locatedIn").source( "organization", "*").destination("country", country), client.fact("targets").source("incident", "*").destination( "organization", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ) for fact in chain: handle_fact(fact, output_format=output_format)
def add_tools(client: act.api.Act, ta_cards: List, tools: List, output_format: Text = "json") -> None: """ Submit tool aliases and actor tools """ tool_vocab = [tool["tool"] for tool in tools] for actor in ta_cards: if "tools" in actor: for tool in actor["tools"]: if tool in tool_vocab: for ta in actor["actor"].split(","): chain = act.api.fact.fact_chain( client.fact("classifiedAs").source( "content", "*").destination("tool", tool.lower()), client.fact("observedIn").source("content", "*").destination( "event", "*"), client.fact("attributedTo").source( "event", "*").destination("incident", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ) for fact in chain: try: handle_fact(fact, output_format=output_format) except act.api.base.ValidationError as err: error("ResponseError while storing objects: %s" % err) for values in tools: aliases = set([tool["name"].strip() for tool in values["names"]] + [values["tool"]]) for tool1, tool2 in combinations(aliases, 2): fact = client.fact("alias").bidirectional("tool", tool1, "tool", tool2) handle_fact(fact)
def main() -> None: """main function""" # Look for default ini file in "/etc/actworkers.ini" and ~/config/actworkers/actworkers.ini # (or replace .config with $XDG_CONFIG_DIR if set) args = cli.handle_args(parseargs()) actapi = worker.init_act(args) fact_type_definition_path = (pathlib.Path( args.fact_type_definition).expanduser().resolve()) if not fact_type_definition_path.is_file(): print(f"{fact_type_definition_path} is not a file.") sys.exit(1) with fact_type_definition_path.open() as typedef: graph = graph_from_type_def(typedef, args.avoid, args.include, args.avoid_cost) start_type, start_value = args.start end_type, end_value = args.end start, end = find_start_and_end_nodes(graph, start_type, end_type) if not start: print(f"{start_type} is not an object type") sys.exit(1) if not end: print(f"{end_type} is not an object type") sys.exit(1) res = dijkstra(graph, start, end) chain = fact_chain_from_path_result(actapi, res, start, end, start_value, end_value) for fact in chain: handle_fact(fact, output_format=args.output_format)
def add_techniques(client, attack: MemoryStore, output_format: Text = "json") -> List[stix2.AttackPattern]: """ extract objects/facts related to ATT&CK techniques Args: attack (stix2): Stix attack instance """ notify = [] # ATT&CK concept STIX Object type ACT object # ========================================================= # Technique attack-pattern technique # Filter out ATT&CK techniques (attack-pattern) from bundle for technique in attack.query([Filter("type", "=", "attack-pattern")]): if getattr(technique, "revoked", None): # Object is revoked, add to notification list but do not add to facts that should be added to the platform notify.append(technique) continue if getattr(technique, "x_mitre_deprecated", None): # Object is revoked, add to notification list AND continue to add to facts that should be added to the platform notify.append(technique) # Mitre ATT&CK Tactics are implemented in STIX as kill chain phases with kill_chain_name "mitre-attack" for tactic in technique.kill_chain_phases: if tactic.kill_chain_name != "mitre-attack": continue handle_fact(client.fact("accomplishes").source( "technique", technique.name).destination("tactic", tactic.phase_name), output_format=output_format) return notify
def process(client: act.api.Act, ta_cards: List, output_format: Text = "json") -> None: "Extract threat actor cards from ThaiCERT" for actor in ta_cards: if len(actor["names"]) > 1: for alias in actor["names"][1:]: handle_fact( client.fact("alias").bidirectional( "threatActor", actor["names"][0]["name"].lower(), "threatActor", alias["name"].lower(), ), output_format=output_format, ) if "operations" in actor: for operation in actor["operations"]: if len(operation["activity"].split("\n")[0].split()) < 4: for ta in actor["actor"].split(","): chain = act.api.fact.fact_chain( client.fact("attributedTo").source( "incident", "*").destination( "campaign", operation["activity"].split("\n")[0]), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ) for fact in chain: try: handle_fact(fact, output_format=output_format) except act.api.base.ValidationError as err: warning("ValidationError while storing objects: %s" % err)
def add_tools(client: act.api.Act, ta_cards: List, tools: List, output_format: Text = "json") -> None: """ Submit tool aliases and actor tools """ tool_vocab = [tool["tool"] for tool in tools] for actor in ta_cards: if "tools" in actor: for tool in actor["tools"]: if tool in tool_vocab: for ta in actor["actor"].split(","): handle_facts( act.api.fact.fact_chain( client.fact("classifiedAs").source( "content", "*").destination("tool", tool), client.fact("observedIn").source( "content", "*").destination("incident", "*"), client.fact("attributedTo").source( "incident", "*").destination("threatActor", ta), ), output_format=output_format, ) for values in tools: aliases = set([tool["name"].strip() for tool in values["names"]] + [values["tool"]]) for tool1, tool2 in combinations(aliases, 2): if tool1 == tool2: continue fact = client.fact("alias").bidirectional("tool", tool1, "tool", tool2) handle_fact(fact)
def handle_techniques( client: Act, technique: "AttckTechnique", main_technique: Optional["AttckTechnique"], output_format: Text = "json", ) -> List: """ Args: client: Act Client technique (str): Technique or subtechnique ID main_technique (str): If set, technique is a sub technique output_format (str): Fact output if sent to stdout (text | json) """ if deprecated_or_revoked(technique): # Object is revoked/deprecated, add to notification list but do not add to facts that should be added to the platform return [technique] if main_technique: handle_fact( client.fact("subTechniqueOf").source("technique", technique.id).destination( "technique", main_technique.id), output_format=output_format, ) handle_fact( client.fact("name", technique.name).source("technique", technique.id), output_format=output_format, ) # Mitre ATT&CK Tactics are implemented in STIX as kill chain phases with kill_chain_name "mitre-attack" for tactic in technique.tactics: handle_fact( client.fact("accomplishes").source("technique", technique.id).destination( "tactic", tactic.id), output_format=output_format, ) handle_fact( client.fact("name", tactic.name).source("tactic", tactic.id), output_format=output_format, ) return []
def test_validator_no_validator(caplog) -> None: api = act.api.Act("", None) act.api.helpers.handle_fact.cache_clear() # Should return None if fact does not validate fact = handle_fact( api.fact("mentions").source("report", "xyz").destination("uri", "X7f://cve-2014-0224")) # No validator is specified so the above should return a fact assert fact is not None # Should not log errors assert caplog.text == ""