diff --git a/jsmon.py b/jsmon.py
index ee271ab..1464b4b 100755
--- a/jsmon.py
+++ b/jsmon.py
@@ -7,61 +7,94 @@
import json
import difflib
import jsbeautifier
-
+from requests.adapters import HTTPAdapter
+from urllib3.util.retry import Retry
+import urllib3
from decouple import config
+# Disable warnings about expired/invalid certificates
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+# Environment variables (do not put your real keys here!)
TELEGRAM_TOKEN = config("JSMON_TELEGRAM_TOKEN", default="CHANGEME")
TELEGRAM_CHAT_ID = config("JSMON_TELEGRAM_CHAT_ID", default="CHANGEME")
SLACK_TOKEN = config("JSMON_SLACK_TOKEN", default="CHANGEME")
SLACK_CHANNEL_ID = config("JSMON_SLACK_CHANNEL_ID", default="CHANGEME")
NOTIFY_SLACK = config("JSMON_NOTIFY_SLACK", default=False, cast=bool)
NOTIFY_TELEGRAM = config("JSMON_NOTIFY_TELEGRAM", default=False, cast=bool)
+
if NOTIFY_SLACK:
from slack import WebClient
from slack.errors import SlackApiError
- if(SLACK_TOKEN == "CHANGEME"):
+ if SLACK_TOKEN == "CHANGEME":
print("ERROR SLACK TOKEN NOT FOUND!")
exit(1)
- client=WebClient(token=SLACK_TOKEN)
+ client = WebClient(token=SLACK_TOKEN)
def is_valid_endpoint(endpoint):
regex = re.compile(
- r'^(?:http|ftp)s?://' # http:// or https://
- r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
- r'localhost|' #localhost...
- r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
- r'(?::\d+)?' # optional port
+ r'^(?:http|ftp)s?://' # http:// or https://
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
+ r'localhost|' # localhost...
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+ r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
- # check if valid url
return re.match(regex, endpoint) is not None
+
def get_endpoint_list(endpointdir):
endpoints = []
filenames = []
for (dp, dirnames, files) in os.walk(endpointdir):
filenames.extend(files)
- filenames = list(filter(lambda x: x[0]!=".", filenames))
+ filenames = list(filter(lambda x: x[0] != ".", filenames))
for file in filenames:
- with open("{}/{}".format(endpointdir,file), "r") as f:
+ with open(f"{endpointdir}/{file}", "r") as f:
endpoints.extend(f.readlines())
-
- # Load all endpoints from a dir into a list
return list(map(lambda x: x.strip(), endpoints))
+
def get_endpoint(endpoint):
- # get an endpoint, return its content
- r = requests.get(endpoint)
- return r.text
+ 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"
+ ),
+ "Accept": "*/*",
+ "Connection": "keep-alive",
+ }
+
+ session = requests.Session()
+ retry = Retry(
+ total=3,
+ backoff_factor=1,
+ status_forcelist=[500, 502, 503, 504],
+ raise_on_status=False,
+ )
+ adapter = HTTPAdapter(max_retries=retry)
+ session.mount("https://", adapter)
+ session.mount("http://", adapter)
+
+ try:
+ response = session.get(endpoint, headers=headers, timeout=10, verify=False)
+ response.raise_for_status()
+ return response.text
+ except requests.exceptions.SSLError as ssl_err:
+ print(f"[SSL ERROR] SSL failure when accessing {endpoint}: {ssl_err}")
+ return ""
+ except requests.exceptions.RequestException as e:
+ print(f"[ERROR] Failed to access {endpoint}: {e}")
+ return ""
+
def get_hash(string):
- # Hash a string
return hashlib.md5(string.encode("utf8")).hexdigest()[:10]
+
def save_endpoint(endpoint, ephash, eptext):
- # save endpoint content to file
- # add it to list of
with open("jsmon.json", "r") as jsm:
jsmd = json.load(jsm)
if endpoint in jsmd.keys():
@@ -70,47 +103,55 @@ def save_endpoint(endpoint, ephash, eptext):
jsmd[endpoint] = [ephash]
with open("jsmon.json", "w") as jsm:
- json.dump(jsmd,jsm)
+ json.dump(jsmd, jsm)
- with open("downloads/{}".format(ephash), "w") as epw:
+ with open(f"downloads/{ephash}", "w") as epw:
epw.write(eptext)
-
+
def get_previous_endpoint_hash(endpoint):
- # get previous endpoint version
- # or None if doesnt exist
with open("jsmon.json", "r") as jsm:
jsmd = json.load(jsm)
if endpoint in jsmd.keys():
return jsmd[endpoint][-1]
else:
return None
-
+
def get_file_stats(fhash):
- return os.stat("downloads/{}".format(fhash))
+ return os.stat(f"downloads/{fhash}")
+
-def get_diff(old,new):
+def get_diff(old, new):
opt = {
"indent_with_tabs": 1,
"keep_function_indentation": 0,
- }
- oldlines = open("downloads/{}".format(old), "r").readlines()
- newlines = open("downloads/{}".format(new), "r").readlines()
+ }
+ try:
+ oldlines = open(f"downloads/{old}", "r").readlines()
+ except FileNotFoundError:
+ oldlines = []
+
+ try:
+ newlines = open(f"downloads/{new}", "r").readlines()
+ except FileNotFoundError:
+ newlines = []
+
+ if not oldlines or not newlines:
+ return ""
+
oldbeautified = jsbeautifier.beautify("".join(oldlines), opt).splitlines()
newbeautified = jsbeautifier.beautify("".join(newlines), opt).splitlines()
- # print(oldbeautified)
- # print(newbeautified)
differ = difflib.HtmlDiff()
- html = differ.make_file(oldbeautified,newbeautified)
- #open("test.html", "w").write(html)
+ html = differ.make_file(oldbeautified, newbeautified)
return html
-def notify_telegram(endpoint,prev, new, diff, prevsize,newsize):
- print("[!!!] Endpoint [ {} ] has changed from {} to {}".format(endpoint, prev, new))
- log_entry = "{} has been updated from {}({}Bytes) to {}({}Bytes)".format(endpoint, prev,prevsize, new,newsize)
+def notify_telegram(endpoint, prev, new, diff, prevsize, newsize):
+ print(f"[!!!] Endpoint [ {endpoint} ] has changed from {prev} to {new}")
+ log_entry = (f"{endpoint} has been updated from {prev}({prevsize} Bytes) "
+ f"to {new}({newsize} Bytes)")
payload = {
'chat_id': TELEGRAM_CHAT_ID,
'caption': log_entry,
@@ -120,35 +161,44 @@ def notify_telegram(endpoint,prev, new, diff, prevsize,newsize):
'document': ('diff.html', diff)
}
- sendfile = requests.post("https://api.telegram.org/bot{token}/sendDocument".format(token=TELEGRAM_TOKEN),
+ sendfile = requests.post(f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendDocument",
files=fpayload, data=payload)
- #print(sendfile.content)
return sendfile
- #test2 = requests.post("https://api.telegram.org/bot{token}/sendMessage".format(token=TELEGRAM_TOKEN),
- # data=payload).content
-def notify_slack(endpoint,prev, new, diff, prevsize,newsize):
+def notify_slack(endpoint, prev, new, diff, prevsize, newsize):
try:
response = client.files_upload(
- initial_comment = "[JSmon] {} has been updated! Download below diff HTML file to check changes.".format(endpoint),
- channels = SLACK_CHANNEL_ID,
- content = diff,
- channel = SLACK_CHANNEL_ID,
- filetype = "html",
- filename = "diff.html",
- title = "Diff changes"
- )
+ initial_comment=f"[JSmon] {endpoint} has been updated! Download the diff HTML file below to check changes.",
+ channels=SLACK_CHANNEL_ID,
+ content=diff,
+ channel=SLACK_CHANNEL_ID,
+ filetype="html",
+ filename="diff.html",
+ title="Diff changes"
+ )
return response
except SlackApiError as e:
assert e.response["ok"] is False
- assert e.response["error"] # str like 'invalid_auth', 'channel_not_found'
print(f"Got an error: {e.response['error']}")
+
def notify(endpoint, prev, new):
- diff = get_diff(prev,new)
- prevsize = get_file_stats(prev).st_size
- newsize = get_file_stats(new).st_size
+ diff = get_diff(prev, new)
+ if not diff:
+ print(f"[!] Unable to generate diff for {endpoint} (file missing)")
+ return
+
+ try:
+ prevsize = get_file_stats(prev).st_size
+ except FileNotFoundError:
+ prevsize = 0
+
+ try:
+ newsize = get_file_stats(new).st_size
+ except FileNotFoundError:
+ newsize = 0
+
if NOTIFY_TELEGRAM:
notify_telegram(endpoint, prev, new, diff, prevsize, newsize)
@@ -159,15 +209,14 @@ def notify(endpoint, prev, new):
def main():
print("JSMon - Web File Monitor")
-
- if not(NOTIFY_SLACK or NOTIFY_TELEGRAM):
- print("You need to setup Slack or Telegram Notifications of JSMon to work!")
+ if not (NOTIFY_SLACK or NOTIFY_TELEGRAM):
+ print("You need to set up Slack or Telegram notifications for JSMon to work!")
exit(1)
if NOTIFY_TELEGRAM and "CHANGEME" in [TELEGRAM_TOKEN, TELEGRAM_CHAT_ID]:
- print("Please Set Up your Telegram Token And Chat ID!!!")
+ print("Please set up your Telegram Token and Chat ID!")
if NOTIFY_SLACK and "CHANGEME" in [SLACK_TOKEN, SLACK_CHANNEL_ID]:
- print("Please Set Up your Sllack Token And Channel ID!!!")
-
+ print("Please set up your Slack Token and Channel ID!")
+
allendpoints = get_endpoint_list('targets')
for ep in allendpoints:
@@ -179,10 +228,10 @@ def main():
else:
save_endpoint(ep, ep_hash, ep_text)
if prev_hash is not None:
- notify(ep,prev_hash, ep_hash)
+ notify(ep, prev_hash, ep_hash)
else:
- print("New Endpoint enrolled: {}".format(ep))
-
+ print(f"New endpoint enrolled: {ep}")
-main()
+if __name__ == "__main__":
+ main()