Properly align links at top of web page and add header text to page
This commit is contained in:
parent
3d7f2407b6
commit
9db68b263d
@ -33,19 +33,23 @@ tfoot tr {
|
|||||||
|
|
||||||
tbody tr:nth-child(odd) {background-color: #dfdfdf}
|
tbody tr:nth-child(odd) {background-color: #dfdfdf}
|
||||||
|
|
||||||
|
div.linksattop {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
a.prevlink {
|
a.prevlink {
|
||||||
float: left;
|
|
||||||
width:33.33333%;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
div.divseeinbrowser {
|
|
||||||
float: right;
|
div.divshowindex {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nextlink {
|
a.nextlink {
|
||||||
float: right;
|
|
||||||
width:33.33333%;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cssclass1 {background-color:#ffff99;}
|
.cssclass1 {background-color:#ffff99;}
|
||||||
.cssclass2 {background-color:lightcoral;}
|
.cssclass2 {background-color:lightcoral;}
|
||||||
|
@ -43,6 +43,8 @@
|
|||||||
<!---Navigation here-->
|
<!---Navigation here-->
|
||||||
<br />
|
<br />
|
||||||
<h2>${title}</h2>
|
<h2>${title}</h2>
|
||||||
|
<br />
|
||||||
|
<!---Add in header information here -->
|
||||||
<br />
|
<br />
|
||||||
<table style="border-collapse:collapse;">
|
<table style="border-collapse:collapse;">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
# optional arguments:
|
# optional arguments:
|
||||||
# -h, --help show this help message and exit
|
# -h, --help show this help message and exit
|
||||||
# -d DATE, --date DATE Specify a valid date (yyyy-mm-dd) for the analysis
|
# -d DATE, --date DATE Specify a valid date (yyyy-mm-dd) for the analysis
|
||||||
|
# -ef EMAILFILE, --emailfile EMAILFILE
|
||||||
|
# Save an html file of the email sent (y/N)
|
||||||
#
|
#
|
||||||
# Re-written in python from Mailstats.pl (Perl) to conform to SME11 / Postfix / qpsmtpd log formats
|
# Re-written in python from Mailstats.pl (Perl) to conform to SME11 / Postfix / qpsmtpd log formats
|
||||||
# and html output added
|
# and html output added
|
||||||
@ -16,6 +18,9 @@
|
|||||||
# Todo
|
# Todo
|
||||||
# 2 Other stats
|
# 2 Other stats
|
||||||
# 3. Extra bits for sub tables
|
# 3. Extra bits for sub tables
|
||||||
|
# 4. Percent char causes sort to fail - look at adding it in the template
|
||||||
|
# 5. Chase disparity in counts betweeen old mailstats and this
|
||||||
|
# 6. Count emails delivered over 25/587/465
|
||||||
#
|
#
|
||||||
# Centos7:
|
# Centos7:
|
||||||
# yum install python3-chameleon --enablerepo=epel
|
# yum install python3-chameleon --enablerepo=epel
|
||||||
@ -155,7 +160,6 @@ def is_private_ip(ip):
|
|||||||
ipaddress.ip_network('172.16.0.0/12'),
|
ipaddress.ip_network('172.16.0.0/12'),
|
||||||
ipaddress.ip_network('192.168.0.0/16'),
|
ipaddress.ip_network('192.168.0.0/16'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Check if the IP address is within any of these ranges
|
# Check if the IP address is within any of these ranges
|
||||||
for private_range in private_ranges:
|
for private_range in private_ranges:
|
||||||
if ip_addr in private_range:
|
if ip_addr in private_range:
|
||||||
@ -387,12 +391,9 @@ def insert_string_after(original:str, to_insert:str, after:str) -> str:
|
|||||||
:return: The new string with to_insert inserted after after.
|
:return: The new string with to_insert inserted after after.
|
||||||
"""
|
"""
|
||||||
position = original.find(after)
|
position = original.find(after)
|
||||||
#print(position)
|
|
||||||
|
|
||||||
if position == -1:
|
if position == -1:
|
||||||
# 'after' string is not found in 'original'
|
print(f"insert_string_after:({after}) string is not found in original")
|
||||||
return original
|
return original
|
||||||
#print(f"{len(after)}")
|
|
||||||
# Position of the insertion point
|
# Position of the insertion point
|
||||||
insert_pos = position + len(after)
|
insert_pos = position + len(after)
|
||||||
|
|
||||||
@ -576,6 +577,53 @@ def replace_between(text, start, end, replacement):
|
|||||||
replaced_text = re.sub(pattern, replacement, text, flags=re.DOTALL)
|
replaced_text = re.sub(pattern, replacement, text, flags=re.DOTALL)
|
||||||
return replaced_text
|
return replaced_text
|
||||||
|
|
||||||
|
def get_heading():
|
||||||
|
#
|
||||||
|
# Needs from anaytsis
|
||||||
|
# SATagLevel - done
|
||||||
|
# SARejectLevel - done
|
||||||
|
# warnnoreject - done
|
||||||
|
# totalexamined - done
|
||||||
|
# emailperhour - done
|
||||||
|
# spamavg - done
|
||||||
|
# rejectspamavg - done
|
||||||
|
# hamavg - done
|
||||||
|
# DMARCSendCount - done
|
||||||
|
# hamcount - done
|
||||||
|
# DMARCOkCount - deone
|
||||||
|
|
||||||
|
# Clam Version/DB Count/Last DB update
|
||||||
|
clam_output = subprocess.getoutput("freshclam -V")
|
||||||
|
clam_info = f"Clam Version/DB Count/Last DB update: {clam_output}"
|
||||||
|
|
||||||
|
# SpamAssassin Version
|
||||||
|
sa_output = subprocess.getoutput("spamassassin -V")
|
||||||
|
sa_info = f"SpamAssassin Version: {sa_output}"
|
||||||
|
|
||||||
|
# Tag level and Reject level
|
||||||
|
tag_reject_info = f"Tag level: {SATagLevel}; Reject level: {SARejectLevel} {warnnoreject}"
|
||||||
|
|
||||||
|
# SMTP connection stats
|
||||||
|
smtp_stats = f"All External SMTP connections accepted: {totalexternalsmtpsessions}\n"\
|
||||||
|
f"All Internal SMTP connections accepted: {totalinternalsmtpsessions}\n"\
|
||||||
|
f"Emails per hour: {emailperhour:.1f}/hr\n"\
|
||||||
|
f"Average spam score (accepted): {spamavg or 0:.2f}\n"\
|
||||||
|
f"Average spam score (rejected): {rejectspamavg or 0:.2f}\n"\
|
||||||
|
f"Average ham score: {hamavg or 0:.2f}\n"\
|
||||||
|
f"Number of DMARC reporting emails sent: {DMARCSendCount or 0} (not shown on table)"
|
||||||
|
|
||||||
|
# DMARC approved emails
|
||||||
|
dmarc_info = ""
|
||||||
|
if hamcount != 0:
|
||||||
|
dmarc_ok_percentage = DMARCOkCount * 100 / hamcount
|
||||||
|
dmarc_info = f"Number of emails approved through DMARC: {DMARCOkCount or 0} ({dmarc_ok_percentage:.2f}% of Ham count)"
|
||||||
|
|
||||||
|
# Accumulate all strings
|
||||||
|
header_str = "\n".join([clam_info, sa_info, tag_reject_info, smtp_stats, dmarc_info])
|
||||||
|
# switch newlines to <br />
|
||||||
|
header_str = header_str.replace("\n","<br />")
|
||||||
|
return header_str
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
chameleon_version = pkg_resources.get_distribution("Chameleon").version
|
chameleon_version = pkg_resources.get_distribution("Chameleon").version
|
||||||
@ -620,6 +668,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
SARejectLevel = int(get_value(ConfigDB, "spamassassin", "RejectLevel","12")) #12 #$cdb->get('spamassassin')->prop('RejectLevel');
|
SARejectLevel = int(get_value(ConfigDB, "spamassassin", "RejectLevel","12")) #12 #$cdb->get('spamassassin')->prop('RejectLevel');
|
||||||
SATagLevel = int(get_value(ConfigDB, "spamassassin", "TagLevel","4")) #4 #$cdb->get('spamassassin')->prop('TagLevel');
|
SATagLevel = int(get_value(ConfigDB, "spamassassin", "TagLevel","4")) #4 #$cdb->get('spamassassin')->prop('TagLevel');
|
||||||
|
if SARejectLevel == 0:
|
||||||
|
warnnoreject = "(*Warning* 0 = no reject)"
|
||||||
|
else:
|
||||||
|
warnnoreject = ""
|
||||||
|
|
||||||
EmailAddress = get_value(ConfigDB,"mailstats","Email","admin@"+DomainName)
|
EmailAddress = get_value(ConfigDB,"mailstats","Email","admin@"+DomainName)
|
||||||
if '@' not in EmailAddress:
|
if '@' not in EmailAddress:
|
||||||
@ -699,10 +751,19 @@ if __name__ == "__main__":
|
|||||||
# Initial call to print the progress bar
|
# Initial call to print the progress bar
|
||||||
#unless none to show
|
#unless none to show
|
||||||
if sorted_len > 0:
|
if sorted_len > 0:
|
||||||
|
spamavg = 0;
|
||||||
|
spamqueuedcount = 0
|
||||||
|
hamcount = 0
|
||||||
|
hamavg = 0
|
||||||
|
rejectspamcount = 0
|
||||||
|
rejectspamavg = 0
|
||||||
|
DMARCSendCount = 0
|
||||||
|
totalexamined = 0
|
||||||
if isThonny:
|
if isThonny:
|
||||||
print_progress_bar(0, sorted_len, prefix='Progress:', suffix='Complete', length=50)
|
print_progress_bar(0, sorted_len, prefix='Progress:', suffix='Complete', length=50)
|
||||||
for timestamp, data in sorted_log_dict.items():
|
for timestamp, data in sorted_log_dict.items():
|
||||||
i += 1
|
i += 1
|
||||||
|
totalexamined += 1
|
||||||
if isThonny:
|
if isThonny:
|
||||||
print_progress_bar(i, sorted_len, prefix='Scanning for main table:', suffix='Complete', length=50)
|
print_progress_bar(i, sorted_len, prefix='Scanning for main table:', suffix='Complete', length=50)
|
||||||
#print(f"{i*100/len}%")
|
#print(f"{i*100/len}%")
|
||||||
@ -728,23 +789,42 @@ if __name__ == "__main__":
|
|||||||
if parsed_data['action'] == '(queue)':
|
if parsed_data['action'] == '(queue)':
|
||||||
columnCounts_2d[hour][Ham] += 1
|
columnCounts_2d[hour][Ham] += 1
|
||||||
columnCounts_2d[ColTotals][Ham] += 1
|
columnCounts_2d[ColTotals][Ham] += 1
|
||||||
#spamassasin
|
# spamassassin not rejected
|
||||||
|
if parsed_data.get('spam-status') is not None and isinstance(parsed_data['spam-status'], str):
|
||||||
|
if parsed_data['spam-status'].lower().startswith('no'):
|
||||||
|
#Extract other parameters from this string
|
||||||
|
# example: No, score=-3.9
|
||||||
|
spam_pattern = r'score=(-?\d+\.\d+) required=(-?\d+\.\d+)'
|
||||||
|
match = re.search(spam_pattern, parsed_data['spam-status'])
|
||||||
|
if match:
|
||||||
|
score = float(match.group(1))
|
||||||
|
if score < SATagLevel:
|
||||||
|
# Accumulate allowed score (inc negatives?)
|
||||||
|
hamavg += score
|
||||||
|
hamcount += 1
|
||||||
|
else:
|
||||||
|
spamavg += score
|
||||||
|
spamqueuedcount += 1
|
||||||
|
#spamassasin rejects
|
||||||
if parsed_data.get('spam-status') is not None and isinstance(parsed_data['spam-status'], str):
|
if parsed_data.get('spam-status') is not None and isinstance(parsed_data['spam-status'], str):
|
||||||
if parsed_data['spam-status'].lower().startswith('yes'):
|
if parsed_data['spam-status'].lower().startswith('yes'):
|
||||||
#Extract other parameters from this string
|
#Extract other parameters from this string
|
||||||
# example: Yes, score=10.3 required=4.0 autolearn=disable
|
# example: Yes, score=10.3 required=4.0 autolearn=disable
|
||||||
spam_pattern = r'score=([\d.]+)\s+required=([\d.]+)'
|
spam_pattern = r'score=(-?\d+\.\d+) required=(-?\d+\.\d+)'
|
||||||
match = re.search(spam_pattern, parsed_data['spam-status'])
|
match = re.search(spam_pattern, parsed_data['spam-status'])
|
||||||
if match:
|
if match:
|
||||||
score = float(match.group(1))
|
score = float(match.group(1))
|
||||||
required = float(match.group(2))
|
required = float(match.group(2))
|
||||||
#print(f"{parsed_data['spam-status']} / {score} {required}")
|
#print(f"{parsed_data['spam-status']} / {score} {required}")
|
||||||
|
rejectspamavg += score
|
||||||
|
rejectspamcount += 1
|
||||||
if score >= SARejectLevel:
|
if score >= SARejectLevel:
|
||||||
columnCounts_2d[hour][DelSpam] += 1
|
columnCounts_2d[hour][DelSpam] += 1
|
||||||
columnCounts_2d[ColTotals][DelSpam] += 1
|
columnCounts_2d[ColTotals][DelSpam] += 1
|
||||||
elif score >= required:
|
elif score >= required:
|
||||||
columnCounts_2d[hour][QuedSpam] += 1
|
columnCounts_2d[hour][QuedSpam] += 1
|
||||||
columnCounts_2d[ColTotals][QuedSpam] += 1
|
columnCounts_2d[ColTotals][QuedSpam] += 1
|
||||||
|
|
||||||
#Local send
|
#Local send
|
||||||
elif DomainName in parsed_data['sendurl']:
|
elif DomainName in parsed_data['sendurl']:
|
||||||
columnCounts_2d[hour][Local] += 1
|
columnCounts_2d[hour][Local] += 1
|
||||||
@ -778,8 +858,8 @@ if __name__ == "__main__":
|
|||||||
#my $logemail = $log_items[4];
|
#my $logemail = $log_items[4];
|
||||||
if DMARCDomain in parsed_data['from-email']: #(index($DMARC_Report_emails,$logemail)>=0) or
|
if DMARCDomain in parsed_data['from-email']: #(index($DMARC_Report_emails,$logemail)>=0) or
|
||||||
#$localsendtotal++;
|
#$localsendtotal++;
|
||||||
#$DMARCSendCount++;
|
DMARCSendCount += 1
|
||||||
localflag = 1;
|
#localflag = 1;
|
||||||
else:
|
else:
|
||||||
# ignore incoming localhost spoofs
|
# ignore incoming localhost spoofs
|
||||||
if not 'msg denied before queued' in parsed_data['error-msg']:
|
if not 'msg denied before queued' in parsed_data['error-msg']:
|
||||||
@ -843,12 +923,24 @@ if __name__ == "__main__":
|
|||||||
columnCounts_2d[ColTotals][PERCENT] = '100%'
|
columnCounts_2d[ColTotals][PERCENT] = '100%'
|
||||||
columnCounts_2d[ColPercent][TOTALS] = '100%'
|
columnCounts_2d[ColPercent][TOTALS] = '100%'
|
||||||
|
|
||||||
|
#other stats
|
||||||
|
emailperhour = (totalexamined / 24)
|
||||||
|
if not spamqueuedcount == 0:
|
||||||
|
spamavg = spamavg / spamqueuedcount
|
||||||
|
if not rejectspamcount == 0:
|
||||||
|
rejectspamavg = rejectspamavg / rejectspamcount
|
||||||
|
if not hamcount == 0:
|
||||||
|
hamavg = hamavg / hamcount
|
||||||
|
|
||||||
# Now scan for the other lines in the log of interest
|
# Now scan for the other lines in the log of interest
|
||||||
found_countries = defaultdict(int)
|
found_countries = defaultdict(int)
|
||||||
geoip_pattern = re.compile(r".*check_badcountries: GeoIP Country: (.*)")
|
geoip_pattern = re.compile(r".*check_badcountries: GeoIP Country: (.*)")
|
||||||
dmarc_pattern = re.compile(r".*dmarc: pass")
|
dmarc_pattern = re.compile(r".*dmarc: pass")
|
||||||
|
helo_pattern = re.compile(r"Accepted connection.*?from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) \/ ([\w.-]+)")
|
||||||
total_countries = 0
|
total_countries = 0
|
||||||
DMARCOkCount = 0
|
DMARCOkCount = 0
|
||||||
|
totalinternalsmtpsessions = 0
|
||||||
|
totalexternalsmtpsessions = 0
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
j = 0
|
j = 0
|
||||||
@ -860,6 +952,18 @@ if __name__ == "__main__":
|
|||||||
i += 1
|
i += 1
|
||||||
if isThonny:
|
if isThonny:
|
||||||
print_progress_bar(i, log_len, prefix='Scanning for sub tables:', suffix='Complete', length=50)
|
print_progress_bar(i, log_len, prefix='Scanning for sub tables:', suffix='Complete', length=50)
|
||||||
|
|
||||||
|
# Match initial connection message
|
||||||
|
match = helo_pattern.match(data[1])
|
||||||
|
if match:
|
||||||
|
ip = match.group(1)
|
||||||
|
fqdn = match.group(2)
|
||||||
|
if is_private_ip(ip):
|
||||||
|
totalinternalsmtpsessions += 1
|
||||||
|
else:
|
||||||
|
totalexternalsmtpsessions += 1
|
||||||
|
continue
|
||||||
|
|
||||||
#Pull out Geoip countries for analysis table
|
#Pull out Geoip countries for analysis table
|
||||||
if "check_badcountries: GeoIP Country" in data:
|
if "check_badcountries: GeoIP Country" in data:
|
||||||
j += 1
|
j += 1
|
||||||
@ -869,13 +973,14 @@ if __name__ == "__main__":
|
|||||||
found_countries[country] += 1
|
found_countries[country] += 1
|
||||||
total_countries += 1
|
total_countries += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
#Pull out DMARC approvals
|
#Pull out DMARC approvals
|
||||||
match = dmarc_pattern.match(data[1])
|
match = dmarc_pattern.match(data[1])
|
||||||
if match:
|
if match:
|
||||||
DMARCOkCount += 1
|
DMARCOkCount += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
#print(f"J:{j} I:{i}")
|
|
||||||
#Now apply the results to the chameleon template - main table
|
#Now apply the results to the chameleon template - main table
|
||||||
# Path to the template file
|
# Path to the template file
|
||||||
template_path = template_dir+'mailstats.html.pt'
|
template_path = template_dir+'mailstats.html.pt'
|
||||||
@ -894,7 +999,10 @@ if __name__ == "__main__":
|
|||||||
print(f"Chameleon render Exception {e}")
|
print(f"Chameleon render Exception {e}")
|
||||||
|
|
||||||
total_html = rendered_html
|
total_html = rendered_html
|
||||||
#Now apply the results to the chameleon template - subservient tables
|
# Add in the header information
|
||||||
|
rendered_html = get_heading()
|
||||||
|
total_html = insert_string_after(total_html,rendered_html, "<!---Add in header information here -->")
|
||||||
|
#add in the subservient tables..
|
||||||
#qpsmtd codes
|
#qpsmtd codes
|
||||||
qpsmtpd_headers = ["Code",'Count','Percent','Reason']
|
qpsmtpd_headers = ["Code",'Count','Percent','Reason']
|
||||||
qpsmtpd_title = 'Qpsmtpd codes league table:'
|
qpsmtpd_title = 'Qpsmtpd codes league table:'
|
||||||
|
Loading…
Reference in New Issue
Block a user