Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 113 additions & 64 deletions jsmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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 <code>{}</code>(<b>{}</b>Bytes) to <code>{}</code>(<b>{}</b>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 <code>{prev}</code>(<b>{prevsize}</b> Bytes) "
f"to <code>{new}</code>(<b>{newsize}</b> Bytes)")
payload = {
'chat_id': TELEGRAM_CHAT_ID,
'caption': log_entry,
Expand All @@ -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)

Expand All @@ -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:
Expand All @@ -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()