diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..bcd9c6da24 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://www.buymeacoffee.com/carlospolop'] diff --git a/.github/workflows/peass_bot.yaml b/.github/workflows/peass_bot.yaml new file mode 100644 index 0000000000..64a448ca1a --- /dev/null +++ b/.github/workflows/peass_bot.yaml @@ -0,0 +1,44 @@ +name: peass_bot_action + +on: + schedule: + - cron: '0 * * * *' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@master + + - name: Setup Python + uses: actions/setup-python@v2.2.2 + + - name: Setup Dependencies + run: python3 -m pip install -r requirements.txt + + - name: Run CVEs Monitor + run: python3 peass_bot.py + shell: bash + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + VULNERS_API_KEY: ${{ secrets.VULNERS_API_KEY }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + + - name: Create local changes + run: git add scripts/cves_monitor_bot/output/cves_monitor_bot.json + + - name: Commit results to Github + run: | + git config --local user.email "" + git config --global user.name "actions-continuous-monitoring" + git commit -m "PD-Actions report" -a --allow-empty + + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b275c23a27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file diff --git a/README.md b/README.md index e0b415bd4c..e7a013a0aa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # PEASS_Bot -Use this bot to monitors new CVEs containing defined keywords and sends alets to Slack or Telegram. +Use this bot to monitor new CVEs containing defined keywords and send alets to Slack or Telegram. + +## See it in action + +Join the telegram group **[peass](https://t.me/peass)** to see the bot in action and be up to date with the latest privilege escalation vulnerabilities. + +## Configure one for yourself + +**Configuring your own PEASS_Bot** that notifies you about the new CVEs containing specific keywords is very easy! + +- Fork this repo +- Modify the file `config/cves_monitor_bot.yaml` and set your own keywords +- In the **github secrets** of your forked repo enter the following API keys: + - **VULNERS_API_KEY**: (Optional) This is used to find publicly available exploits. You can ue a Free API Key. + - **SLACK_WEBHOOK**: (Optional) Set the slack webhook to send messages to your slack group + - **TELEGRAM_BOT_TOKEN** and **TELEGRAM_CHAT_ID**: (Optional) Your Telegram bot token and the chat_id to send the messages to +- Check `.github/wordflows/peass_bot.yaml` and configure the cron (*once per hour by default*) + +*Note that the slack and telegram configurations are optional, but if you don't set any of them you won't receive any notifications* diff --git a/config/peass_bot.yaml b/config/peass_bot.yaml new file mode 100644 index 0000000000..bc30db1020 --- /dev/null +++ b/config/peass_bot.yaml @@ -0,0 +1,28 @@ +ALL_VALID: no + +DESCRIPTION_KEYWORDS_I: +- privilege escalation +- escalation +- privesc +- high integrity +- " sudo " +- " suid " +- " pe " +- " UAC " +- User Account Control +- linpeas +- winpeas +- escape +- a + +DESCRIPTION_KEYWORDS: +- ThisIsACaseSensitiveExample + +PRODUCT_KEYWORDS_I: +- sudo +- docker +- kubernetes + +PRODUCT_KEYWORDS: +- ThisIsACaseSensitiveExample + diff --git a/output/peass_bot.json b/output/peass_bot.json new file mode 100644 index 0000000000..be838362e6 --- /dev/null +++ b/output/peass_bot.json @@ -0,0 +1 @@ +{"LAST_NEW_CVE": "2021-04-07T11:15:00", "LAST_MODIFIED_CVE": "2021-05-07T11:15:00"} \ No newline at end of file diff --git a/peass_bot.py b/peass_bot.py new file mode 100644 index 0000000000..8928e69e07 --- /dev/null +++ b/peass_bot.py @@ -0,0 +1,324 @@ +import requests +import datetime +import pathlib +import json +import os +import yaml +import vulners + +from os.path import join +from enum import Enum + + +CIRCL_LU_URL = "https://cve.circl.lu/api/query" +CVES_JSON_PATH = join(pathlib.Path(__file__).parent.absolute(), "output/peass_bot.json") +LAST_NEW_CVE = datetime.datetime.now() - datetime.timedelta(days=1) +LAST_MODIFIED_CVE = datetime.datetime.now() - datetime.timedelta(days=1) +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" + +KEYWORDS_CONFIG_PATH = join(pathlib.Path(__file__).parent.absolute(), "config/peass_bot.yaml") +ALL_VALID = False +DESCRIPTION_KEYWORDS_I = [] +DESCRIPTION_KEYWORDS = [] +PRODUCT_KEYWORDS_I = [] +PRODUCT_KEYWORDS = [] + + +class Time_Type(Enum): + PUBLISHED = "Published" + LAST_MODIFIED = "last-modified" + + +################## LOAD CONFIGURATIONS #################### + +def load_keywords(): + ''' Load keywords from config file ''' + + global ALL_VALID + global DESCRIPTION_KEYWORDS_I, DESCRIPTION_KEYWORDS + global PRODUCT_KEYWORDS_I, PRODUCT_KEYWORDS + + with open(KEYWORDS_CONFIG_PATH, 'r') as yaml_file: + keywords_config = yaml.safe_load(yaml_file) + print(f"Loaded keywords: {keywords_config}") + ALL_VALID = keywords_config["ALL_VALID"] + DESCRIPTION_KEYWORDS_I = keywords_config["DESCRIPTION_KEYWORDS_I"] + DESCRIPTION_KEYWORDS = keywords_config["DESCRIPTION_KEYWORDS"] + PRODUCT_KEYWORDS_I = keywords_config["PRODUCT_KEYWORDS_I"] + PRODUCT_KEYWORDS = keywords_config["PRODUCT_KEYWORDS"] + + +def load_lasttimes(): + ''' Load lasttimes from json file ''' + + global LAST_NEW_CVE, LAST_MODIFIED_CVE + + try: + with open(CVES_JSON_PATH, 'r') as json_file: + cves_time = json.load(json_file) + LAST_NEW_CVE = datetime.datetime.strptime(cves_time["LAST_NEW_CVE"], TIME_FORMAT) + LAST_MODIFIED_CVE = datetime.datetime.strptime(cves_time["LAST_MODIFIED_CVE"], TIME_FORMAT) + + except Excepton as e: #If error, just keep the fault date (today - 1 day) + print(f"ERROR, using default last times.\n{e}") + pass + + print(f"Last new cve: {LAST_NEW_CVE}") + print(f"Last modified cve: {LAST_MODIFIED_CVE}") + + +def update_lasttimes(): + ''' Save lasttimes in json file ''' + + with open(CVES_JSON_PATH, 'w') as json_file: + json.dump({ + "LAST_NEW_CVE": LAST_NEW_CVE.strftime(TIME_FORMAT), + "LAST_MODIFIED_CVE": LAST_MODIFIED_CVE.strftime(TIME_FORMAT), + }, json_file) + + + +################## SEARCH CVES #################### + +def get_cves(tt_filter:Time_Type) -> dict: + ''' Given the headers for the API retrive CVEs from cve.circl.lu ''' + now = datetime.datetime.now() - datetime.timedelta(days=1) + now_str = now.strftime("%d-%m-%Y") + + headers = { + "time_modifier": "from", + "time_start": now_str, + "time_type": tt_filter.value, + "limit": "100", + } + r = requests.get(CIRCL_LU_URL, headers=headers) + + return r.json() + + +def get_new_cves() -> list: + ''' Get CVEs that are new ''' + + global LAST_NEW_CVE + + cves = get_cves(Time_Type.PUBLISHED) + filtered_cves, new_last_time = filter_cves( + cves["results"], + LAST_NEW_CVE, + Time_Type.PUBLISHED + ) + LAST_NEW_CVE = new_last_time + + return filtered_cves + + +def get_modified_cves() -> list: + ''' Get CVEs that has been modified ''' + + global LAST_MODIFIED_CVE + + cves = get_cves(Time_Type.LAST_MODIFIED) + filtered_cves, new_last_time = filter_cves( + cves["results"], + LAST_MODIFIED_CVE, + Time_Type.PUBLISHED + ) + LAST_MODIFIED_CVE = new_last_time + + return filtered_cves + + +def filter_cves(cves: list, last_time: datetime.datetime, tt_filter: Time_Type) -> list: + ''' Filter by time the given list of CVEs ''' + + filtered_cves = [] + new_last_time = last_time + + for cve in cves: + cve_time = datetime.datetime.strptime(cve[tt_filter.value], TIME_FORMAT) + if cve_time > last_time: + if ALL_VALID or is_summ_keyword_present(cve["summary"]) or \ + is_prod_keyword_present(str(cve["vulnerable_configuration"])): + + filtered_cves.append(cve) + + if cve_time > new_last_time: + new_last_time = cve_time + + return filtered_cves, new_last_time + + +def is_summ_keyword_present(summary: str): + ''' Given the summary check if any keyword is present ''' + + return any(w in summary for w in DESCRIPTION_KEYWORDS) or \ + any(w.lower() in summary.lower() for w in DESCRIPTION_KEYWORDS_I) + + +def is_prod_keyword_present(products: str): + ''' Given the summary check if any keyword is present ''' + + return any(w in products for w in PRODUCT_KEYWORDS) or \ + any(w.lower() in products.lower() for w in PRODUCT_KEYWORDS_I) + + +def search_exploits(cve: str) -> list: + ''' Given a CVE it will search for public exploits to abuse it ''' + + vulners_api_key = os.getenv('VULNERS_API_KEY') + + if vulners_api_key: + vulners_api = vulners.Vulners(api_key=vulners_api_key) + cve_data = vulners_api.searchExploit(cve) + return [v['vhref'] for v in cve_data] + + else: + print("VULNERS_API_KEY wasn't configured in the secrets!") + + return [] + + +#################### GENERATE MESSAGES ######################### + +def generate_new_cve_message(cve_data: dict) -> str: + ''' Generate new CVE message for sending to slack ''' + + message = f"šŸšØ *{cve_data['id']}* šŸšØ\n" + message += f"šŸ”® *CVSS*: {cve_data['cvss']}\n" + message += f"šŸ“… *Published*: {cve_data['Published']}\n" + message += "šŸ““ *Summary*: " + message += cve_data["summary"] if len(cve_data["summary"]) < 400 else cve_data["summary"][:400] + "..." + + if cve_data["vulnerable_configuration"]: + message += f"\nšŸ”“ *Vulnerable* (_limit to 10_): " + ", ".join(cve_data["vulnerable_configuration"][:10]) + + message += "\nšŸŸ¢ ā„¹ļø *More information* (_limit to 5_)\n" + "\n".join(cve_data["references"][:5]) + + message += "\n\n(Bot info in: https://github.com/carlospolop/PEASS_Bot)" + + return message + + +def generate_modified_cve_message(cve_data: dict) -> str: + ''' Generate modified CVE message for sending to slack ''' + + message = f"šŸ“£ *{cve_data['id']}*(_{cve_data['cvss']}_) was modified the {cve_data['last-modified'].split('T')[0]} (_originally published the {cve_data['Published'].split('T')[0]}_)\n" + return message + + +def generate_public_expls_message(public_expls: list) -> str: + ''' Given the list of public exploits, generate the message ''' + + message = "" + + if public_expls: + message = "šŸ˜ˆ *Public Exploits* šŸ˜ˆ\n" + "\n".join(public_expls) + + return message + + +#################### SEND MESSAGES ######################### + +def send_slack_mesage(message: str, public_expls_msg: str): + ''' Send a message to the slack group ''' + + slack_url = os.getenv('SLACK_WEBHOOK') + + if not slack_url: + print("SLACK_WEBHOOK wasn't configured in the secrets!") + return + + json_params = { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": message + } + }, + { + "type": "divider" + } + ] + } + + if public_expls_msg: + json_params["blocks"].append({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": public_expls_msg + } + }) + + requests.post(slack_url, json=json_params) + + +def send_telegram_message(message: str, public_expls_msg: str): + ''' Send a message to the telegram group ''' + + telegram_bot_token = os.getenv('TELEGRAM_BOT_TOKEN') + telegram_chat_id = os.getenv('TELEGRAM_CHAT_ID') + + if not telegram_bot_token: + print("TELEGRAM_BOT_TOKEN wasn't configured in the secrets!") + return + + if not telegram_chat_id: + print("TELEGRAM_CHAT_ID wasn't configured in the secrets!") + return + + if public_expls_msg: + message = message + "\n" + public_expls_msg + + message = message.replace(".", "\.").replace("-", "\-").replace("(", "\(").replace(")", "\)").replace("_", "").replace("[","\[").replace("]","\]").replace("{","\{").replace("}","\}") + r = requests.get(f'https://api.telegram.org/bot{telegram_bot_token}/sendMessage?parse_mode=MarkdownV2&text={message}&chat_id={telegram_chat_id}') + + resp = r.json() + if not resp['ok']: + requests.get(f'https://api.telegram.org/bot{telegram_bot_token}/sendMessage?parse_mode=MarkdownV2&text=Error with' + message.split("\n")[0] + f'{resp["description"]}&chat_id={telegram_chat_id}') + + +#################### MAIN ######################### + +def main(): + #Load configured keywords + load_keywords() + + #Start loading time of last checked ones + load_lasttimes() + + #Find a publish new CVEs + new_cves = get_new_cves() + + new_cves_ids = [ncve['id'] for ncve in new_cves] + print(f"New CVEs discovered: {new_cves_ids}") + + for new_cve in new_cves: + public_exploits = search_exploits(new_cve['id']) + cve_message = generate_new_cve_message(new_cve) + public_expls_msg = generate_public_expls_message(public_exploits) + send_slack_mesage(cve_message, public_expls_msg) + send_telegram_message(cve_message, public_expls_msg) + + #Find and publish modified CVEs + modified_cves = get_modified_cves() + + modified_cves = [mcve for mcve in modified_cves if not mcve['id'] in new_cves_ids] + modified_cves_ids = [mcve['id'] for mcve in modified_cves] + print(f"Modified CVEs discovered: {modified_cves_ids}") + + for modified_cve in modified_cves: + public_exploits = search_exploits(modified_cve['id']) + cve_message = generate_modified_cve_message(modified_cve) + public_expls_msg = generate_public_expls_message(public_exploits) + send_slack_mesage(cve_message, public_expls_msg) + send_telegram_message(cve_message, public_expls_msg) + + #Update last times + update_lasttimes() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..50933956a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests +pyyaml +vulners \ No newline at end of file