├── LICENSE ├── README.md ├── akamaru.py ├── requirements.txt └── utils ├── akamaru_logo.png ├── google_search_visibility.py ├── mitre_visibility.py ├── ransomlook_visibility.py ├── sentinelone_visibility.py └── util.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 eremit4 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akamaru 2 | >In the day-to-day life of a CTI or Threat Hunting team, one of the main tasks is trying to answer the questions: who are my enemies? what are the threat groups that affect my industry? 3 | Doing this mapping can take an analyst many hours, even weeks. Considering the Threat Intelligence Lifecycle, Akamaru aims to automate the initial phase of Collect by collecting the threat groups and some relevant TTPs from [MITRE](https://attack.mitre.org/groups/), [Ransomware Anthology](https://www.sentinelone.com/anthology/) (SentinelOne), and [Ransomlook](https://www.ransomlook.io/recent), just informing the name of the sector of interest (e.g. financial, education, defense) or the name of the group (e.g. Akira, Lockbit) in order to get more information about it. 4 | 5 | ![](./utils/akamaru_logo.png) 6 | 7 | ## 🐾 Setup 8 | 9 | Cloning the project: 10 | ```bash 11 | git clone https://github.com/eremit4/Akamaru.git 12 | ``` 13 | Optional - Creating a virtualenv before installing the dependencies 14 | > Note: The use of virtual environments is optional, but recommended. In this way, we avoid possible conflicts in different versions of the project's dependencies. 15 | > Learn how to install and use virtualenv according to your OS [here](https://virtualenv.pypa.io/en/latest/) 16 | 17 | Installing the dependencies: 18 | ```bash 19 | pip install -r requirements.txt 20 | ``` 21 | 22 | ## 🐶 Running 23 | 24 | Discovering the project capabilities: 25 | ```bash 26 | python akamaru.py --help 27 | ``` 28 | 29 | Discovering which sectors are supported: 30 | ```bash 31 | python akamaru.py -ss 32 | ``` 33 | 34 | Finding only the groups relevant to a given sector: 35 | ```bash 36 | python akamaru.py --sector 37 | ``` 38 | 39 | Finding the relevant groups for a given sector, their TTPs, and returning everything in a CSV file: 40 | ```bash 41 | python akamaru.py --sector --ttp --output 42 | ``` 43 | 44 | Looking for information about a particular group: 45 | ```bash 46 | python akamaru.py --group 47 | ``` 48 | 49 | Looking for ransomware activities: 50 | ```bash 51 | python akamaru.py --ransomware-activities 52 | ``` 53 | 54 | ## 🐕 Demo 55 | [![asciicast](https://asciinema.org/a/591986.svg)](https://asciinema.org/a/591986) 56 | 57 | ## 📝 License 58 | This project is under the [MIT License](LICENSE). 59 | -------------------------------------------------------------------------------- /akamaru.py: -------------------------------------------------------------------------------- 1 | from colorama import init, Fore 2 | from argparse import ArgumentParser, HelpFormatter 3 | from traceback import format_exc as print_traceback 4 | from utils.mitre_visibility import perform_mitre_visibility 5 | from utils.util import create_csv_report, print_supported_sectors 6 | from utils.sentinelone_visibility import performs_sentinel_visibility 7 | from utils.ransomlook_visibility import performs_ransomlook_visibility 8 | 9 | 10 | class CustomHelpFormatter(HelpFormatter): 11 | def __init__(self, prog): 12 | super().__init__(prog, max_help_position=50, width=100) 13 | 14 | def format_action_invocation(self, action) -> str: 15 | if not action.option_strings or action.nargs == 0: 16 | return super().format_action_invocation(action) 17 | default = self._get_default_metavar_for_optional(action) 18 | args_string = self._format_args(action, default) 19 | return ", ".join(action.option_strings) + " " + args_string 20 | 21 | 22 | def main(args_: ArgumentParser) -> None: 23 | """ 24 | Manages all Akamaru's process 25 | :argument args_: arguments from the command line 26 | :return: None 27 | """ 28 | parser, mitre_results, sentinel_results, ransomlook_results = args_.parse_args(), {}, {}, {} 29 | if parser.ttp and parser.sector: 30 | mitre_results = perform_mitre_visibility(sector=parser.sector, ttp=True) 31 | sentinel_results = performs_sentinel_visibility(sector=parser.sector) 32 | 33 | elif parser.sector: 34 | mitre_results = perform_mitre_visibility(sector=parser.sector) 35 | sentinel_results = performs_sentinel_visibility(sector=parser.sector) 36 | 37 | elif parser.group: 38 | mitre_results = perform_mitre_visibility(group=parser.group) 39 | sentinel_results = performs_sentinel_visibility(group=parser.group) 40 | 41 | elif parser.ransomware_activities: 42 | performs_ransomlook_visibility(general_activity=True) 43 | 44 | elif parser.supported_sectors: 45 | print_supported_sectors() 46 | 47 | if parser.output: 48 | create_csv_report(mitre=mitre_results, sentinel=sentinel_results, ttp=parser.ttp) 49 | 50 | if not parser.sector \ 51 | and not parser.group \ 52 | and not parser.ttp \ 53 | and not parser.ransomware_activities \ 54 | and not parser.supported_sectors \ 55 | and not parser.output: 56 | args_.print_help() 57 | 58 | 59 | if __name__ == '__main__': 60 | arg_style = lambda prog: CustomHelpFormatter(prog) 61 | args = ArgumentParser(description="", add_help=False, formatter_class=arg_style) 62 | # well known threat groups 63 | group_required = args.add_argument_group(title="Well Known Threat Groups") 64 | group_required.add_argument("-s", "--sector", metavar="", type=str, required=False, help="Receives the sector name of your interesting and returns the well-known groups related. Use the -ss option to know whats sectors are supported.") 65 | group_required.add_argument("-ss", "--supported-sectors", action="store_true", help="Returns the supported sectors by Akamaru.") 66 | group_required.add_argument("-t", "--ttp", action="store_true", help="Returns TTPs associated with groups collected from MITRE ATT&CK. It must be used with the flag. Due to information overload, using this option without the flag is not recommended.") 67 | group_required.add_argument("-g", "--group", metavar="", type=str, required=False, help="Receives the name of the threat group and returns the known information about them.") 68 | # ransomware activities 69 | group_required = args.add_argument_group(title="Ransomware Activities") 70 | group_required.add_argument("-r", "--ransomware-activities", action="store_true", help="Returns the most active ransomware groups over a time range.") 71 | # outputs 72 | group_required = args.add_argument_group(title="Outputs") 73 | group_required.add_argument("-o", "--output", action="store_true", help="Returns the or results in a CSV file, separated by semicolon.") 74 | # help 75 | group_required = args.add_argument_group(title="Help") 76 | group_required.add_argument("-h", "--help", action="help", help="Show this help screen.") 77 | 78 | # perform coloroma multiplatform 79 | init(strip=False) 80 | print(r"""{} 81 | _ 82 | ,/A\, 83 | .//`_`\\, 84 | ,//`____-`\\, 85 | ,//`[{}Akamaru{}]`\\, 86 | ,//`= == __- _`\\, 87 | //|__= __- == _ __|\\ 88 | ` | __ .-----. _ | ` 89 | | - _/ \- | 90 | |__ |{} .-"-. {}| __=| 91 | | _=|{}/) (\{}| | 92 | |-__ {}(/ {}- -{} \){} -__| 93 | |___ {}/`\_Y_/`\{}____| 94 | {}\) (/ 95 | {}[{}>{}] Sniffing out relevant threat groups 96 | [{}>{}] eremit4@protonmail.com 97 | 98 | """.format(Fore.LIGHTBLACK_EX, Fore.MAGENTA, 99 | Fore.LIGHTBLACK_EX, Fore.LIGHTWHITE_EX, 100 | Fore.LIGHTBLACK_EX, Fore.LIGHTWHITE_EX, 101 | Fore.LIGHTBLACK_EX, Fore.LIGHTWHITE_EX, 102 | Fore.LIGHTRED_EX, Fore.LIGHTWHITE_EX, 103 | Fore.LIGHTBLACK_EX, Fore.LIGHTWHITE_EX, 104 | Fore.LIGHTBLACK_EX, Fore.LIGHTWHITE_EX, 105 | Fore.WHITE, Fore.MAGENTA, Fore.WHITE, 106 | Fore.MAGENTA, Fore.WHITE)) 107 | try: 108 | main(args_=args) 109 | except KeyboardInterrupt: 110 | print(f"\n{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] OK! I will cancel operations and await your commands.\n") 111 | except Exception: 112 | print(f"\n{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] An error forced Akamaru to stop: {repr(print_traceback())}") 113 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm ~= 4.65 2 | colorama ~= 0.4.6 3 | requests~=2.32.3 4 | prettytable ~= 3.8.0 5 | beautifulsoup4 ~= 4.12.2 6 | -------------------------------------------------------------------------------- /utils/akamaru_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eremit4/Akamaru/4da8cefe5e16bef284e8c0a69c35c76c7da7654a/utils/akamaru_logo.png -------------------------------------------------------------------------------- /utils/google_search_visibility.py: -------------------------------------------------------------------------------- 1 | from requests import get 2 | from colorama import Fore 3 | from bs4 import BeautifulSoup 4 | 5 | 6 | def search_for_group_analysis_on_google(group_name: str, ) -> None: 7 | """ 8 | Search the target on Google to look for mentions of it. 9 | :param group_name: The name of the group to find analysis on Google Search. 10 | :return: None 11 | """ 12 | def google_search_request(dork: str) -> bytes: 13 | """ 14 | Performs the request on Google Search. 15 | :param dork: Google Dork. 16 | :return: Response in HTML content bytes. 17 | """ 18 | return get(url=f"https://www.google.com/search?q={dork}", 19 | headers={ 20 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 21 | "Accept": "*/*", 22 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 23 | "Connection": "keep-alive", 24 | "Upgrade-Insecure-Requests": "1"}).content 25 | 26 | a_tags, count = BeautifulSoup(google_search_request(dork=f"intext:{group_name} intext:ransomware intext:analysis"), 'html.parser').find_all("a"), 0 27 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Looking for analysis about {Fore.MAGENTA}{group_name}{Fore.WHITE} group") 28 | for link in a_tags: 29 | if link.attrs.get("href") and not check_blacklist(link_=link.attrs.get("href")) and choose_priority_sources(source_url=link.attrs.get('href')) and count < 6: 30 | print(f"\t{Fore.WHITE}[{Fore.MAGENTA}{count + 1}{Fore.WHITE}] {link.attrs.get('href')}") 31 | count += 1 32 | 33 | 34 | def check_blacklist(link_: str) -> bool: 35 | """ 36 | checks if the link collected is relevant or not 37 | :param link_: link collected 38 | :return: True if this link is blacklisted or False if not 39 | """ 40 | for term in ["google.com", "/search?q=", "youtube.com", "#", "oem.avira.com"]: 41 | if term in link_: 42 | return True 43 | return False 44 | 45 | 46 | def choose_priority_sources(source_url: str) -> bool: 47 | """ 48 | Choose some sources to suggest to the user. 49 | :param source_url: Source URL. 50 | :return: True if there is any keyword bellow in URL received, otherwise it returns False. 51 | """ 52 | for source in ["sentinelone", "trellix", "trendmicro", "mandiant", "sophos", "cisa.gov", "kaspersky", "paloalto", "crowstrike", "malwarebytes", "cyble", "socradar", "cybereason"]: 53 | if source in source_url: 54 | return True 55 | return False 56 | -------------------------------------------------------------------------------- /utils/mitre_visibility.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | from time import sleep 3 | from requests import get 4 | from colorama import Fore 5 | from bs4 import BeautifulSoup 6 | from traceback import format_exc 7 | from prettytable import PrettyTable 8 | from utils.util import get_sector_keywords, check_group_in_groups, check_sector_blacklist, make_url_tiny 9 | 10 | 11 | def get_elements_from_mitre_groups_page() -> BeautifulSoup: 12 | """ 13 | Gets the HTML elements from MITRE ATT&CK Groups page. 14 | :return: The Beautifulsoup object to be parser. 15 | """ 16 | response = get(url="https://attack.mitre.org/groups/", 17 | headers={ 18 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 19 | "Accept": "*/*", 20 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 21 | "Connection": "keep-alive", 22 | "Upgrade-Insecure-Requests": "1"}) 23 | if response.status_code == 200: 24 | return BeautifulSoup(response.content, "html.parser") 25 | 26 | 27 | def get_ttps_from_mitre_by_group_id(group_id: str) -> BeautifulSoup: 28 | """ 29 | Gets the HTML elements from MITRE ATT&CK Groups by group ID. 30 | :param group_id: The group ID from MITRE. 31 | :return: The Beautifulsoup object to be parser. 32 | """ 33 | response = get(url=f"https://attack.mitre.org/groups/{group_id}", 34 | headers={ 35 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 36 | "Accept": "*/*", 37 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 38 | "Connection": "keep-alive", 39 | "Upgrade-Insecure-Requests": "1"}) 40 | if response.status_code == 200: 41 | return BeautifulSoup(response.content, "html.parser") 42 | 43 | 44 | def get_technique_from_mitre(tech_id: str, sub_tech_id=None) -> BeautifulSoup: 45 | """ 46 | Gets the HTML elements of from Mitre ATT&CK Technique page by Technique ID. 47 | :param tech_id: The Technique ID from MITRE. 48 | :param sub_tech_id: The Sub-Technique ID from MITRE. 49 | :return: The Beautifulsoup object to be parser. 50 | """ 51 | if sub_tech_id is not None: 52 | path = f"{tech_id}/{sub_tech_id}" 53 | else: 54 | path = tech_id 55 | response = get(url=f"https://attack.mitre.org/techniques/{path}", 56 | headers={ 57 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 58 | "Accept": "*/*", 59 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 60 | "Connection": "keep-alive", 61 | "Upgrade-Insecure-Requests": "1"}) 62 | if response.status_code == 200: 63 | return BeautifulSoup(response.content, "html.parser") 64 | 65 | 66 | def mitre_groups_parser(sector=None) -> list: 67 | """ 68 | Parses the Beautifulsoup object and extract group information from MITRE ATT&CK. 69 | :param sector: The name of the sector of interest. 70 | :return: A dict of collected information. 71 | """ 72 | soup, groups = get_elements_from_mitre_groups_page(), list() 73 | try: 74 | for item in tqdm(iterable=soup.find_all("tr")[1:], 75 | desc=f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Collecting relevant groups from MITRE ATT&CK{Fore.BLUE}", 76 | bar_format="{l_bar}{bar:10}"): 77 | groups_raw = item.get_text().strip().split("\n") 78 | mitre_group_id, mitre_group_name = groups_raw[0].strip(), groups_raw[3].strip() 79 | if groups_raw[6] != "": 80 | mitre_associated_groups, mitre_group_desc = groups_raw[6].strip(), groups_raw[9].strip() 81 | else: 82 | mitre_associated_groups, mitre_group_desc = "None", groups_raw[8] 83 | 84 | group_data = { 85 | "name": mitre_group_name, 86 | "mitre_id": mitre_group_id, 87 | "url": f"https://attack.mitre.org/groups/{mitre_group_id}", 88 | "relations": mitre_associated_groups, 89 | "description": mitre_group_desc, 90 | } 91 | if sector is not None: 92 | for keyword_ in get_sector_keywords(sector_name=sector): 93 | mitre_group_desc = check_sector_blacklist(desc=mitre_group_desc, sector=sector) 94 | if sector is not None and keyword_ in mitre_group_desc and group_data not in groups: 95 | group_data["matrix"] = get_mitre_navigator_url(group_id=group_data["mitre_id"]).get("matrix") 96 | groups.append(group_data) 97 | else: 98 | groups.append(group_data) 99 | sleep(0.002) 100 | return groups 101 | except AttributeError: 102 | return [] 103 | 104 | 105 | def get_softwares_used_from_mitre(group_id: str) -> list: 106 | """ 107 | Gets the softwares used by a specific group on MITRE ATT&CK. 108 | :param group_id: MITRE Group ID. 109 | :return: A list with the softwares used. 110 | """ 111 | soup, softwares = get_ttps_from_mitre_by_group_id(group_id=group_id), list() 112 | for a_tag in tqdm(iterable=soup.find_all("a")[1:], 113 | desc=f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Collecting Softwares Used{Fore.BLUE}", 114 | bar_format="{l_bar}{bar:10}"): 115 | if "/software/S" in a_tag.attrs["href"] and a_tag.attrs["href"].split("/")[2] != a_tag.get_text(): 116 | softwares.append(a_tag.get_text()) 117 | return list(set(softwares)) 118 | 119 | 120 | def get_mitre_navigator_url(group_id: str) -> dict: 121 | """ 122 | Gets the MITRE ATT&CK Navigator URL by group. 123 | :param group_id: ID from threat group on MITRE ATT&CK. 124 | :return: A dict with the MITRE ATT&CK Navigator URL and content json. 125 | """ 126 | soup = get_ttps_from_mitre_by_group_id(group_id=group_id) 127 | for a_tag in soup.find_all("a", {"class": "dropdown-item"}): 128 | if "layer.json" in a_tag.attrs["href"]: 129 | return { 130 | "matrix": make_url_tiny(url=f"https://mitre-attack.github.io/attack-navigator//#layerURL=https://attack.mitre.org/{a_tag.attrs['href']}"), 131 | "json": make_url_tiny(url=f"https://attack.mitre.org/{a_tag.attrs['href']}") 132 | } 133 | 134 | 135 | def print_mitre_groups_table(groups_from_mitre: list, columns=None) -> None: 136 | """ 137 | Shows in the screen a table with groups collected from MITRE ATT&CK. 138 | :param groups_from_mitre: MITRE ATT&CK Group ID. 139 | :param columns: Columns to show. Options: ID, Group, Associated Groups. 140 | :return: None 141 | """ 142 | sector_table = PrettyTable() 143 | sector_table.field_names = ["ID", "MITRE Groups", "Related Groups", "Matrix URL"] 144 | for group_ in groups_from_mitre: 145 | sector_table.add_row( 146 | [ 147 | f"{Fore.WHITE}{group_['mitre_id']}{Fore.LIGHTBLUE_EX}", 148 | f"{Fore.WHITE}{group_['name']}{Fore.LIGHTBLUE_EX}", 149 | f"{Fore.WHITE}{group_['relations']}{Fore.LIGHTBLUE_EX}", 150 | f"{Fore.WHITE}{group_['matrix']}{Fore.LIGHTBLUE_EX}" 151 | ] 152 | ) 153 | if columns is None: 154 | print(f"{Fore.LIGHTBLUE_EX}{sector_table.get_string(fields=['ID', 'MITRE Groups', 'Matrix URL'])}") 155 | else: 156 | print(f"{Fore.LIGHTBLUE_EX}{sector_table.get_string(fields=columns)}") 157 | 158 | 159 | def print_mitre_softwares_table(tools: list) -> None: 160 | """ 161 | Shows in the screen a table with softwares used by groups. 162 | :param tools: Softwares collected of group page from MITRE ATT&CK. 163 | :return: None 164 | """ 165 | software_table = PrettyTable() 166 | software_table.field_names = ["Softwares Used"] 167 | for tool in tools: 168 | software_table.add_row([f"{Fore.WHITE}{tool}{Fore.LIGHTBLUE_EX}"]) 169 | print(f"{Fore.LIGHTBLUE_EX}{software_table.get_string(fields=['Softwares Used'])}") 170 | 171 | 172 | def perform_mitre_visibility(sector=None, group=None, ttp=None) -> dict: 173 | """ 174 | Performs the MITRE functions execution. 175 | :param sector: Sector name. 176 | :param group: Group name. 177 | :param ttp: A bool value to 'ttp' flag. 178 | :return: A dict with results. 179 | """ 180 | if (ttp and sector) is not None: 181 | mitre_output = dict() 182 | for group in mitre_groups_parser(sector=sector): 183 | try: 184 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Getting {Fore.BLUE}{group['name']}{Fore.WHITE} TTPs") 185 | group_tools = get_softwares_used_from_mitre(group_id=group["mitre_id"]) 186 | group["navigator_url"] = get_mitre_navigator_url(group_id=group["mitre_id"])["matrix"] 187 | if group_tools: 188 | group["softwares"] = group_tools 189 | else: 190 | group_tools = ["None"] 191 | mitre_output[group["name"]] = group 192 | # printing results 193 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Description:\n\t[{Fore.BLUE}+{Fore.WHITE}] {group['description']}") 194 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] ATT&CK Navigator:\n\t[{Fore.BLUE}+{Fore.WHITE}] {Fore.BLUE}{group['navigator_url']}{Fore.RESET}\n") 195 | print_mitre_groups_table(groups_from_mitre=[group], columns=["Associated Groups"]) 196 | print_mitre_softwares_table(tools=group_tools) 197 | except Exception: 198 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error on MITRE ATT&CK sector and ttp operations: {repr(format_exc())}") 199 | continue 200 | return {"mitre_groups": mitre_output} 201 | elif sector is not None: 202 | try: 203 | mitre_groups = mitre_groups_parser(sector=sector) 204 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Found {Fore.MAGENTA}{len(mitre_groups)}{Fore.WHITE} groups on MITRE ATT&CK") 205 | if mitre_groups: 206 | print_mitre_groups_table(groups_from_mitre=mitre_groups) 207 | return {"mitre_groups": mitre_groups} 208 | except Exception: 209 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error on MITRE ATT&CK sector operations: {repr(format_exc())}") 210 | return {} 211 | elif group: 212 | try: 213 | group_info = check_group_in_groups(groups=mitre_groups_parser(), group=group) 214 | if not group_info: 215 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] The {Fore.MAGENTA}{group}{Fore.WHITE} group was not found on MITRE ATT&CK") 216 | return {} 217 | else: 218 | group_tools = get_softwares_used_from_mitre(group_id=group_info["mitre_id"]) 219 | group_info["softwares"] = group_tools 220 | group_info["navigator_url"] = get_mitre_navigator_url(group_id=group_info["mitre_id"])["matrix"] 221 | # printing MITRE ATT&CK results 222 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Description:\n\t[{Fore.BLUE}+{Fore.WHITE}] {group_info['description']}") 223 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] ATT&CK Navigator:\n\t[{Fore.BLUE}+{Fore.WHITE}] {Fore.BLUE}{group_info['navigator_url']}{Fore.RESET}\n") 224 | print_mitre_groups_table(groups_from_mitre=[group_info], columns=["Associated Groups"]) 225 | print_mitre_softwares_table(tools=group_tools) 226 | return group_info 227 | except Exception: 228 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error on MITRE ATT&CK group operations: {repr(format_exc())}") 229 | return {} 230 | -------------------------------------------------------------------------------- /utils/ransomlook_visibility.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | from re import finditer, M as M_METHOD, IGNORECASE 3 | from requests import get 4 | from colorama import Fore 5 | from bs4 import BeautifulSoup 6 | from datetime import datetime 7 | from traceback import format_exc 8 | from prettytable import PrettyTable 9 | 10 | 11 | def get_elements_from_recent_activities() -> BeautifulSoup: 12 | """ 13 | Request to get the html elements from Ransomlook recent publications page 14 | :return: The Beautifulsoup object to be parser 15 | """ 16 | response = get(url="https://www.ransomlook.io/recent", 17 | headers={ 18 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 19 | "Accept": "*/*", 20 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 21 | "Connection": "keep-alive", 22 | "Upgrade-Insecure-Requests": "1"}) 23 | if response.status_code == 200: 24 | return BeautifulSoup(response.content, "html.parser") 25 | 26 | 27 | def get_group_profile(group: str) -> BeautifulSoup: 28 | """ 29 | Request to get the html elements from Ransomlook recent publications page 30 | :param group: group name 31 | :return: The Beautifulsoup object to be parser 32 | """ 33 | response = get(url=f"https://www.ransomlook.io/group/{group}", 34 | headers={ 35 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 36 | "Accept": "*/*", 37 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 38 | "Connection": "keep-alive", 39 | "Upgrade-Insecure-Requests": "1"}) 40 | if response.status_code == 200: 41 | return BeautifulSoup(response.content, "html.parser") 42 | 43 | 44 | def get_ransomware_activities() -> list: 45 | """ 46 | Collects the ransomware activities 47 | :return: A list of dictionaries with the date and the group name 48 | """ 49 | soup = get_elements_from_recent_activities() 50 | raw_table, activities = soup.find_all("tr")[1:], list() 51 | for element in tqdm(iterable=raw_table, 52 | desc=f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Collecting Ransomware Activities{Fore.BLUE}", 53 | bar_format="{l_bar}{bar:10}"): 54 | activities.append({"date": element.get_text().strip().split(" ")[0], "name": element.find("a").get_text()}) 55 | return activities 56 | 57 | 58 | def get_ransomware_activities_by_group(group_name: str) -> list: 59 | """ 60 | Performs group filtering in the ransomware activities. 61 | :param group_name: Group name. 62 | :return: A list of dicts with ransomware group activity data 63 | """ 64 | activity_by_group = list() 65 | for activity in get_ransomware_activities(): 66 | if group_name.lower() in activity["name"].lower(): 67 | activity_by_group.append(activity) 68 | return activity_by_group 69 | 70 | 71 | def count_group_hits(group_mentions: list) -> tuple: 72 | """ 73 | Take the list of ransomware activities by group and count the hits. 74 | :param group_mentions: A list with group activities. 75 | :return: 76 | """ 77 | group_hits_count, first_date, last_date = dict(), None, None 78 | for group in group_mentions: 79 | if (first_date and last_date) is None: 80 | first_date, last_date = group["date"], group["date"] 81 | if not group_hits_count.get(group["name"]): 82 | group_hits_count[group["name"]] = 1 83 | else: 84 | group_hits_count[group["name"]] += 1 85 | if datetime.strptime(first_date, "%Y-%m-%d").date() > datetime.strptime(group["date"], "%Y-%m-%d").date(): 86 | first_date = group["date"] 87 | continue 88 | if datetime.strptime(last_date, "%Y-%m-%d").date() < datetime.strptime(group["date"], "%Y-%m-%d").date(): 89 | last_date = group["date"] 90 | continue 91 | return group_hits_count, first_date, last_date 92 | 93 | 94 | def compare_sentinel_and_ransomlook(sentinel_groups: list, ransom_activities: dict) -> dict: 95 | """ 96 | Compares the groups collected from Sentinel and Ransomlook. 97 | :param sentinel_groups: Groups from SentinelOne. 98 | :param ransom_activities:Groups from Ransomlook. 99 | :return: A dict with common groups. 100 | """ 101 | common_groups = dict() 102 | for group in ransom_activities: 103 | for sentinel_group in sentinel_groups: 104 | if group.replace(" ", "") in sentinel_group["name"].strip().replace("0", "o").replace(" ", "").lower(): 105 | common_groups[group] = ransom_activities[group] 106 | return common_groups 107 | 108 | 109 | def print_ransomware_activities_table(victims_count: dict) -> None: 110 | """ 111 | Prints ransomware activity table. 112 | :param victims_count: A dict with group name and activity datetime data. 113 | :return: None. 114 | """ 115 | activities_table = PrettyTable() 116 | activities_table.field_names = ["Group", "Victims"] 117 | for group_hits in victims_count.items(): 118 | activities_table.add_row([f"{Fore.WHITE}{group_hits[0]}{Fore.LIGHTBLUE_EX}", f"{Fore.WHITE}{group_hits[1]}{Fore.LIGHTBLUE_EX}"]) 119 | print(f"{Fore.LIGHTBLUE_EX}{activities_table.get_string(fields=['Group', 'Victims'])}") 120 | 121 | 122 | def parse_group_profile(rl_groups: iter) -> dict: 123 | """ 124 | Collects information about the profile groups 125 | :param rl_groups: ransomlook groups 126 | :return: 127 | """ 128 | groups_profiles = dict() 129 | for group in tqdm(iterable=rl_groups, 130 | desc=f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Collecting Groups Profiles{Fore.BLUE}", 131 | bar_format="{l_bar}{bar:10}"): 132 | profile_page_soup = get_group_profile(group=group) 133 | raw_table = profile_page_soup.find_all(attrs={"id": "table"}) 134 | group_urls = list() 135 | for element in raw_table: 136 | # parsing URLs table 137 | for onion_url in finditer(r"(?:https?://)?(?:www)?(\S*?\.onion)\b", element.get_text(), M_METHOD | IGNORECASE): 138 | group_urls.append(onion_url.group(0)) 139 | groups_profiles[group] = {"urls": list(set(group_urls))} 140 | return groups_profiles 141 | 142 | 143 | def print_groups_urls_table(urls_from_groups: dict) -> None: 144 | """ 145 | Prints ransomware activity table. 146 | :param urls_from_groups: A dict with group name and activity datetime data. 147 | :return: None. 148 | """ 149 | for group in urls_from_groups.items(): 150 | activities_table = PrettyTable() 151 | title = f"{str(group[0]).title()} Urls" 152 | activities_table.field_names = [title] 153 | for url in group[1]["urls"]: 154 | activities_table.add_row([f"{Fore.WHITE}{url.strip()}{Fore.LIGHTBLUE_EX}"]) 155 | print(f"{Fore.LIGHTBLUE_EX}{activities_table.get_string(fields=[title])}") 156 | 157 | 158 | def performs_ransomlook_visibility(groups_from_sentinel=None, group=None, general_activity=None) -> dict: 159 | """ 160 | Invokes the Ransomlook's functions. 161 | :param groups_from_sentinel: A list with groups from SentinelOne. 162 | :param group: Group name. 163 | :param general_activity: A bool value to get the recent ransomware activities without filtering. 164 | :return: None. 165 | """ 166 | if groups_from_sentinel: 167 | try: 168 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Looking for activities of the aforementioned ransomware groups") 169 | groups_from_ransomlook, first_date, last_date = count_group_hits(group_mentions=get_ransomware_activities()) 170 | common_active_groups = compare_sentinel_and_ransomlook(sentinel_groups=groups_from_sentinel, 171 | ransom_activities=groups_from_ransomlook) 172 | if common_active_groups: 173 | rl_groups_profiles = parse_group_profile(rl_groups=common_active_groups.keys()) 174 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] The results bellow occurred between {Fore.MAGENTA}{first_date}{Fore.WHITE} and {Fore.MAGENTA}{last_date}{Fore.WHITE}") 175 | print_ransomware_activities_table(victims_count=common_active_groups) 176 | print_groups_urls_table(urls_from_groups=rl_groups_profiles) 177 | return {"first_date": first_date, "last_date": last_date, "groups": common_active_groups} 178 | else: 179 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] No activity was found between {Fore.MAGENTA}{first_date}{Fore.WHITE} and {Fore.MAGENTA}{last_date}{Fore.WHITE}") 180 | return {} 181 | except Exception: 182 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error getting ransomware activities for SentinelOne groups: {repr(format_exc())}") 183 | return {} 184 | 185 | elif group: 186 | try: 187 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Looking for recent activities related to the {Fore.MAGENTA}{group}{Fore.WHITE} group") 188 | group_activities = get_ransomware_activities_by_group(group_name=group) 189 | if group_activities: 190 | groups_from_ransomlook, first_date, last_date = count_group_hits(group_mentions=group_activities) 191 | rl_groups_profiles = parse_group_profile(rl_groups=group_activities) 192 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] The results bellow occurred between {Fore.MAGENTA}{first_date}{Fore.WHITE} and {Fore.MAGENTA}{last_date}{Fore.WHITE}") 193 | print_ransomware_activities_table(victims_count=groups_from_ransomlook) 194 | print_groups_urls_table(urls_from_groups=rl_groups_profiles) 195 | return {"first_date": first_date, "last_date": last_date, "groups": groups_from_ransomlook} 196 | else: 197 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] No activity was found {Fore.MAGENTA}{group}{Fore.WHITE} group") 198 | return {} 199 | except Exception: 200 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error getting ransomware activities for {Fore.MAGENTA}{group}{Fore.WHITE}: {repr(format_exc())}") 201 | return {} 202 | 203 | elif general_activity: 204 | try: 205 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Looking for recent ransomware activities") 206 | groups_from_ransomlook, first_date, last_date = count_group_hits(group_mentions=get_ransomware_activities()) 207 | rl_groups_profiles = parse_group_profile(rl_groups=groups_from_ransomlook) 208 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] The results bellow occurred between {Fore.MAGENTA}{first_date}{Fore.WHITE} and {Fore.MAGENTA}{last_date}{Fore.WHITE}") 209 | print_ransomware_activities_table(victims_count=groups_from_ransomlook) 210 | print_groups_urls_table(urls_from_groups=rl_groups_profiles) 211 | return {"first_date": first_date, "last_date": last_date, "groups": groups_from_ransomlook} 212 | except Exception: 213 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error getting general ransomware activities: {repr(format_exc())}") 214 | return {} 215 | -------------------------------------------------------------------------------- /utils/sentinelone_visibility.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | from requests import get 3 | from colorama import Fore 4 | from bs4 import BeautifulSoup 5 | from traceback import format_exc 6 | from prettytable import PrettyTable 7 | from utils.util import get_sector_keywords, check_group_in_groups 8 | from utils.ransomlook_visibility import performs_ransomlook_visibility 9 | from utils.google_search_visibility import search_for_group_analysis_on_google 10 | 11 | 12 | def get_elements_from_anthology_page() -> BeautifulSoup: 13 | """ 14 | Get html elements from SentinelOne Anthology page. 15 | :return: The Beautifulsoup object to be parser. 16 | """ 17 | response = get(url="https://www.sentinelone.com/anthology", 18 | headers={ 19 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0", 20 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 21 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 22 | "Connection": "keep-alive", 23 | "Upgrade-Insecure-Requests": "1"}) 24 | if response.status_code == 200: 25 | return BeautifulSoup(response.content, "html.parser") 26 | 27 | 28 | def get_elements_from_anthology_group_page(anthology_group_url: str) -> BeautifulSoup: 29 | """ 30 | Gets the html elements from SentinelOne Ransomware Anthology group page. 31 | :param anthology_group_url: Group url. 32 | :return: The Beautifulsoup object to be parser. 33 | """ 34 | response = get(url=anthology_group_url, 35 | headers={ 36 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 37 | "Accept": "*/*", 38 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 39 | "Connection": "keep-alive", 40 | "Upgrade-Insecure-Requests": "1"}) 41 | if response.status_code == 200: 42 | return BeautifulSoup(response.content, "html.parser") 43 | 44 | 45 | def get_sentinel_groups_information(sector=None) -> list: 46 | """ 47 | Acquire information like group name, url, and description of html elements collected from SentinelOne. 48 | :param sector: 49 | :return: A list of dict with information about groups. 50 | """ 51 | soup, groups = get_elements_from_anthology_page(), list() 52 | raw_table = soup.find_all("div", {"class": "anthology-entry"}) 53 | for element in tqdm(iterable=raw_table, 54 | desc=f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Collecting relevant groups from SentinelOne{Fore.BLUE}", 55 | bar_format="{l_bar}{bar:10}"): 56 | name = element.find("h3").get_text(), 57 | url = element.find("a").attrs["href"], 58 | desc = get_sentinel_group_description(group_url=element.find("a").attrs["href"]) 59 | group = {"name": name[0], "url": url[0], "description": desc} 60 | try: 61 | if sector is not None: 62 | for keyword_ in get_sector_keywords(sector_name=sector): 63 | if keyword_ in desc and group not in groups: 64 | groups.append(group) 65 | else: 66 | groups.append(group) 67 | except TypeError: 68 | pass 69 | return groups 70 | 71 | 72 | def get_sentinel_group_description(group_url: str) -> str: 73 | """ 74 | Get the group description. 75 | :param group_url: Group url. 76 | :return: The group description. 77 | """ 78 | try: 79 | blog_elements = get_elements_from_anthology_group_page(anthology_group_url=group_url).find_all("div", {"class": "content-wrapper"})[0].text.splitlines() 80 | for element in blog_elements: 81 | if "Target?" in element: 82 | description, position = "", blog_elements.index(element) + 1 83 | while "?" not in blog_elements[position]: 84 | if len(blog_elements[position]) != 0: 85 | description += f" {blog_elements[position]}" 86 | position += 1 87 | else: 88 | position += 1 89 | return description.strip() 90 | if "Target?" in element and len(blog_elements[blog_elements.index(element) + 1]) > 0: 91 | return blog_elements[blog_elements.index(element) + 1].strip() 92 | except AttributeError: 93 | pass 94 | 95 | 96 | def try_to_find_a_hide_group_from_sentinelone(group_name: str): 97 | """ 98 | Try to access a hide article about a specific group. 99 | :param group_name: Group name. 100 | :return: A dict with group information. 101 | """ 102 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] The {Fore.MAGENTA}{group_name}{Fore.WHITE} group is not listed on the Ransomware Anthology main page") 103 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Trying to find {Fore.MAGENTA}{group_name}{Fore.WHITE} group manually") 104 | group_url = f"https://www.sentinelone.com/anthology/{group_name}" 105 | response = get(url=group_url, 106 | headers={ 107 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0", 108 | "Accept": "*/*", 109 | "Accept-Language": "pt - BR, pt;q = 0.8, en - US;q = 0.5, en;q = 0.3", 110 | "Connection": "keep-alive", 111 | "Upgrade-Insecure-Requests": "1"}) 112 | if response.status_code == 200: 113 | return {"name": group_name, "url": group_url, "description": get_sentinel_group_description(group_url=group_url)} 114 | else: 115 | return {} 116 | 117 | 118 | def print_sentinel_groups_table(groups_from_sentinel: list) -> None: 119 | """ 120 | Shows in the screen a table with groups collected from MITRE ATT&CK. 121 | :param groups_from_sentinel: MITRE ATT&CK Group ID. 122 | :return: None 123 | """ 124 | sector_table = PrettyTable() 125 | sector_table.field_names = ["SentinelOne Groups"] 126 | for group_ in groups_from_sentinel: 127 | sector_table.add_row([f"{Fore.WHITE}{group_['name'].strip()}{Fore.LIGHTBLUE_EX}"]) 128 | print(f"{Fore.LIGHTBLUE_EX}{sector_table.get_string(fields=['SentinelOne Groups'])}") 129 | 130 | 131 | def performs_sentinel_visibility(sector=None, group=None) -> dict: 132 | """ 133 | Performs the MITRE functions execution. 134 | :param sector: Sector name. 135 | :param group: Group name. 136 | :return: None 137 | """ 138 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Preparing to get ransomware groups from SentinelOne. Sometimes the page may be slow") 139 | if sector: 140 | try: 141 | sentinel_groups = get_sentinel_groups_information(sector=sector) 142 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Found {Fore.LIGHTBLUE_EX}{len(sentinel_groups)} {Fore.WHITE}ransomware groups on SentinelOne") 143 | if sentinel_groups: 144 | print_sentinel_groups_table(groups_from_sentinel=sentinel_groups) 145 | activities = performs_ransomlook_visibility(groups_from_sentinel=sentinel_groups) 146 | for actor in activities.get("groups"): 147 | search_for_group_analysis_on_google(group_name=actor) 148 | return {"groups": sentinel_groups, "activities": activities.get("groups")} 149 | else: 150 | return {} 151 | except Exception: 152 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error on SentinelOne sector operation: {repr(format_exc())}") 153 | return {} 154 | elif group: 155 | try: 156 | group_info = check_group_in_groups(groups=get_sentinel_groups_information(), group=group) 157 | if not group_info: 158 | # trying to find the group manually (some articles it's not listed on the main page) 159 | group_info = try_to_find_a_hide_group_from_sentinelone(group_name=group) 160 | if not group_info: 161 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] The {Fore.MAGENTA}{group}{Fore.WHITE} group was not found on SentinelOne") 162 | return {} 163 | else: 164 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Description:\n\t[{Fore.BLUE}+{Fore.WHITE}] {group_info['description']}") 165 | group_info["activities"] = performs_ransomlook_visibility(group=group) 166 | for actor in group_info["activities"]["groups"]: 167 | search_for_group_analysis_on_google(group_name=actor) 168 | return group_info 169 | else: 170 | print(f"\n{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Description:\n\t[{Fore.BLUE}+{Fore.WHITE}] {group_info['description']}") 171 | group_info["activities"] = performs_ransomlook_visibility(group=group) 172 | for actor in group_info["activities"]["groups"]: 173 | search_for_group_analysis_on_google(group_name=actor) 174 | return group_info 175 | except Exception: 176 | print(f"{Fore.WHITE}[{Fore.MAGENTA}!{Fore.WHITE}] Error on SentinelOne groups operation: {repr(format_exc())}") 177 | return {} 178 | -------------------------------------------------------------------------------- /utils/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from os import mkdir 3 | from os.path import isdir 4 | from colorama import Fore 5 | from datetime import datetime 6 | from contextlib import closing 7 | from prettytable import PrettyTable 8 | from urllib.parse import urlencode 9 | from urllib.request import urlopen 10 | 11 | import sys 12 | 13 | 14 | def get_sector_keywords(sector_name: str) -> list: 15 | """ 16 | Choose the keywords by sector. 17 | :param sector_name: Sector name. 18 | :return: A list with the keywords. 19 | """ 20 | if sector_name == "financial": 21 | return ["economic", "bank", "financial", "finance", "investment firms", "payment card"] 22 | if sector_name == "healthcare": 23 | return ["health", "healthcare", "hospital", "pharmaceutical", "medical", "disease", "medical", "COVID-19"] 24 | if sector_name == "ics": 25 | return ["ICS", "manufacturing", "manufacturers", "oil", "mining", "chemistry", "energy", "critical infrastructure", "nuclear", "petroleum", "semicondutor", "airline", "aerospace", "aviation", "engineering industries", "industrial control systems"] 26 | if sector_name == "defense": 27 | return ["military", "defense", "army"] 28 | if sector_name == "government": 29 | return ["government", "presidential election", "democratic", "diplomatic", "legal services", "political", "ministries", "judiciary", "policy"] 30 | if sector_name == "technology": 31 | return ["technology", "big tech", "high tech", "high-tech", "video game", "gaming", "internet service"] 32 | if sector_name == "telecom": 33 | return ["telecom", "telephony"] 34 | if sector_name == "education": 35 | return ["education", "college", "universit", "academic", "school", "educational"] 36 | if sector_name == "retail": 37 | return ["retail", "commerce", "restaurant"] 38 | if sector_name == "media": 39 | return ["media sector", "television", "media outlets", "journalist", "opposition bloggers", "regional news", "high-profile personalities", "social media"] 40 | if sector_name == "law": 41 | return ["law firms", "legal services"] 42 | if sector_name == "tourism": 43 | return ["hospitality", "tourism", "travel", "hotel"] 44 | 45 | 46 | def check_sector_blacklist(desc: str, sector: str) -> str: 47 | """ 48 | Checks if some blacklisted terms appear in the description and removes them. 49 | :param desc: Description of the group. 50 | :param sector: Sector name. 51 | :return: The new description without the blacklisted terms. 52 | """ 53 | blacklist = { 54 | "financial": [""], 55 | "healthcare": [""], 56 | "ics": ["cryptocurrency-mining"], 57 | "defense": [""], 58 | "government": [""], 59 | "technology": [""], 60 | "education": [""], 61 | "media": [""], 62 | "law": [""], 63 | "tourism": [""] 64 | } 65 | for keyword in blacklist.get(sector): 66 | desc = desc.replace(keyword, "") 67 | return desc 68 | 69 | 70 | def check_group_in_groups(group: str, groups: list) -> dict: 71 | """ 72 | Checks if the group typed by user exists in groups collected. 73 | :param group: Group name. 74 | :param groups: Groups collected. 75 | :return: A dict with information about a group. 76 | """ 77 | for group_ in groups: 78 | if group.lower() in group_["name"].lower(): 79 | return group_ 80 | return {} 81 | 82 | 83 | def create_csv_report(mitre=None, sentinel=None, ttp=None) -> None: 84 | """ 85 | Create a CSV report. 86 | :param mitre: Results from MITRE ATT&CK. 87 | :param sentinel: Results from SentinelOne. 88 | :param ttp: A boolean value that indicates if the flag was provided. 89 | :return: None. 90 | """ 91 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] Preparing the report") 92 | report_name = f"akamaru_report_{datetime.now().strftime('%d-%m-%Y')}.csv" 93 | if not isdir("./akamaru_output"): 94 | mkdir("./akamaru_output") 95 | report_file = open(file=f"./akamaru_output/{report_name}", mode="w") 96 | # writing the header 97 | report_file.write(f"Group;Source;Url;Groups Related;Softwares\n") 98 | # writing other lines 99 | if mitre is not None: 100 | if mitre.get("mitre_groups"): 101 | for data in mitre.get("mitre_groups").items(): 102 | if ttp is not None: 103 | softwares = str(data[1].get("softwares")).replace("[", "").replace("]", "").replace("'", "").strip() 104 | report_file.write(f"{data[1].get('name').strip()};MITRE ATT&CK;{data[1].get('navigator_url').strip()};{data[1].get('relations').strip()};{softwares}\n") 105 | else: 106 | report_file.write(f"{data[1].get('name').strip()};MITRE ATT&CK;{data[1].get('navigator_url').strip()};{data[1].get('relations').strip()};None\n") 107 | elif mitre.get("softwares"): 108 | softwares = str(mitre.get("softwares")).replace("[", "").replace("]", "").replace("'", "").strip() 109 | report_file.write(f"{mitre.get('name')};MITRE ATT&CK;{mitre.get('url')};{mitre.get('relations')};{softwares}\n") 110 | 111 | if sentinel is not None: 112 | if sentinel.get("groups"): 113 | for group in sentinel.get("groups"): 114 | report_file.write(f"{str(group.get('name').strip())};SentinelOne;{group.get('url').strip()};None;None\n") 115 | 116 | report_file.close() 117 | print(f"{Fore.WHITE}[{Fore.BLUE}>{Fore.WHITE}] The {Fore.MAGENTA}{report_name}{Fore.WHITE} report was successfully created and is located in the {Fore.MAGENTA}output{Fore.WHITE} directory") 118 | 119 | 120 | def print_supported_sectors() -> None: 121 | """ 122 | Shows on the screen which sectors are supported 123 | :return: None 124 | """ 125 | sectors_table = PrettyTable() 126 | sectors_table.field_names = ["Supported Sectors"] 127 | for sector in sorted(["financial", "healthcare", "ics", "defense", "government", "technology", "education", "media", "law", "tourism"]): 128 | sectors_table.add_row([f"{Fore.WHITE}{sector}{Fore.LIGHTBLUE_EX}"]) 129 | print(f"{Fore.LIGHTBLUE_EX}{sectors_table.get_string(fields=['Supported Sectors'])}") 130 | 131 | 132 | def make_url_tiny(url: str) -> str: 133 | """ 134 | Convert a long URL in a tiny URL 135 | :param url: URL to be transformed 136 | :return: Tiny URL 137 | """ 138 | request_url = f"http://tinyurl.com/api-create.php?{urlencode({'url':url})}" 139 | with closing(urlopen(request_url)) as response: 140 | return response.read().decode("utf-8") 141 | --------------------------------------------------------------------------------