Compare commits

..

No commits in common. "513dca8a2d0e227b5221e76211a075795ba700b2" and "72a69d5d519182b76eb5dc5db3e96e88f17ad00a" have entirely different histories.

9 changed files with 45 additions and 253 deletions

View File

@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '39 12 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,49 +0,0 @@
# This workflow integrates a collection of open source static analysis tools
# with GitHub code scanning. For documentation, or to provide feedback, visit
# https://github.com/github/ossar-action
name: OSSAR
on:
push:
pull_request:
jobs:
OSSAR-Scan:
# OSSAR runs on windows-latest.
# ubuntu-latest and macos-latest support coming soon
runs-on: windows-latest
steps:
# Checkout your code repository to scan
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Ensure a compatible version of dotnet is installed.
# The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
# A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action.
# GitHub hosted runners already have a compatible version of dotnet installed and this step may be skipped.
# For self-hosted runners, ensure dotnet version 3.1.201 or later is installed by including this action:
# - name: Install .NET
# uses: actions/setup-dotnet@v1
# with:
# dotnet-version: '3.1.x'
# Run open source static analysis tools
- name: Run OSSAR
uses: github/ossar-action@v1
id: ossar
# Upload results to the Security tab
- name: Upload OSSAR results
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: ${{ steps.ossar.outputs.sarifFile }}

View File

@ -1,3 +1,3 @@
{ {
"python.pythonPath": "/home/dominik/src/github/ExOTA-Milter/venv/bin/python3" "python.pythonPath": "/home/dominik/src/git/ExOTA-Milter/venv/bin/python3"
} }

View File

@ -47,7 +47,6 @@ services:
MILTER_TRUSTED_AUTHSERVID: 'my-auth-serv-id' MILTER_TRUSTED_AUTHSERVID: 'my-auth-serv-id'
MILTER_X509_ENABLED: 'some_value' MILTER_X509_ENABLED: 'some_value'
MILTER_X509_TRUSTED_CN: 'mail.protection.outlook.com' MILTER_X509_TRUSTED_CN: 'mail.protection.outlook.com'
MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
MILTER_ADD_HEADER: 'some_value' MILTER_ADD_HEADER: 'some_value'
MILTER_AUTHSERVID: 'my-auth-serv-id' MILTER_AUTHSERVID: 'my-auth-serv-id'
volumes: volumes:

View File

@ -8,7 +8,7 @@ start
note left: From, Authentication-Results, X-MS-Exchange-CrossTenant-Id note left: From, Authentication-Results, X-MS-Exchange-CrossTenant-Id
:HDR: Recognising sender domain; :HDR: Recognising sender domain;
note left: Taken from RFC5322.From header and/or RFC5322.Resent-From header. RFC5321.mail (envelope) is NOT relevant! note left: Taken from RFC5322 From-header. RFC5321.mail (envelope) is NOT relevant!
:EOM: Looking up policy in backend; :EOM: Looking up policy in backend;
note left: Based on RFC5322.from domain note left: Based on RFC5322.from domain

View File

@ -37,8 +37,6 @@ g_milter_policy_file = '/data/policy.json'
g_milter_x509_enabled = False g_milter_x509_enabled = False
# ENV[MILTER_X509_TRUSTED_CN] # ENV[MILTER_X509_TRUSTED_CN]
g_milter_x509_trusted_cn = 'mail.protection.outlook.com' g_milter_x509_trusted_cn = 'mail.protection.outlook.com'
# ENV[MILTER_X509_IP_WHITELIST]
g_milter_x509_ip_whitelist = ['127.0.0.1','::1']
# ENV[MILTER_ADD_HEADER] # ENV[MILTER_ADD_HEADER]
g_milter_add_header = False g_milter_add_header = False
# ENV[MILTER_AUTHSERVID] # ENV[MILTER_AUTHSERVID]
@ -59,15 +57,9 @@ class ExOTAMilter(Milter.Base):
self.conn_reused = False self.conn_reused = False
self.hdr_from = None self.hdr_from = None
self.hdr_from_domain = None self.hdr_from_domain = None
self.hdr_resent_from = None
self.hdr_resent_from_domain = None
self.forwarded = False
self.hdr_tenant_id = None self.hdr_tenant_id = None
self.hdr_tenant_id_count = 0 self.hdr_tenant_id_count = 0
self.x509_whitelisted = False
self.dkim_valid = False self.dkim_valid = False
self.passed_dkim_results = []
self.dkim_aligned = False
self.xar_hdr_count = 0 self.xar_hdr_count = 0
# https://stackoverflow.com/a/2257449 # https://stackoverflow.com/a/2257449
self.mconn_id = g_milter_name + ': ' + ''.join( self.mconn_id = g_milter_name + ': ' + ''.join(
@ -84,7 +76,7 @@ class ExOTAMilter(Milter.Base):
if 'reason' in kwargs: if 'reason' in kwargs:
message = "{0} - reason: {1}".format(message, kwargs['reason']) message = "{0} - reason: {1}".format(message, kwargs['reason'])
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
": milter_action=reject message={0}".format(message) ": milter_action=reject"
) )
self.setreply('550','5.7.1', message) self.setreply('550','5.7.1', message)
return Milter.REJECT return Milter.REJECT
@ -147,23 +139,6 @@ class ExOTAMilter(Milter.Base):
) )
) )
# Parse RFC-5322-Resent-From header (Forwarded)
if(name.lower() == "Resent-From".lower()):
hdr_5322_resent_from = email.utils.parseaddr(hval)
self.hdr_resent_from = hdr_5322_resent_from[1].lower()
m = re.match(g_re_domain, self.hdr_resent_from)
if m is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
"Could not determine domain-part of 5322.resent_from=" + self.hdr_resent_from
)
else:
self.hdr_resent_from_domain = m.group(1).lower()
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: 5322.resentfrom={0}, 5322.resent_from_domain={1}".format(
self.hdr_resent_from, self.hdr_resent_from_domain
)
)
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header # Parse non-standardized X-MS-Exchange-CrossTenant-Id header
elif(name.lower() == "X-MS-Exchange-CrossTenant-Id".lower()): elif(name.lower() == "X-MS-Exchange-CrossTenant-Id".lower()):
self.hdr_tenant_id_count += 1 self.hdr_tenant_id_count += 1
@ -184,12 +159,6 @@ class ExOTAMilter(Milter.Base):
for ar_result in ar.results: for ar_result in ar.results:
if ar_result.method == 'dkim': if ar_result.method == 'dkim':
if ar_result.result == 'pass': if ar_result.result == 'pass':
self.passed_dkim_results.append({
"sdid": ar_result.header_d
})
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: DKIM passed SDID {0}".format(ar_result.header_d)
)
self.dkim_valid = True self.dkim_valid = True
else: else:
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) + logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
@ -214,15 +183,6 @@ class ExOTAMilter(Milter.Base):
# Check if client certificate CN matches trusted CN # Check if client certificate CN matches trusted CN
if g_milter_x509_enabled: if g_milter_x509_enabled:
for whitelisted_client_ip in g_milter_x509_ip_whitelist:
if self.client_ip == whitelisted_client_ip:
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: x509 CN check: client-IP '{0}' is whitelisted".format(
whitelisted_client_ip
)
)
self.x509_whitelisted = True
if not self.x509_whitelisted:
cert_subject = self.getsymval('{cert_subject}') cert_subject = self.getsymval('{cert_subject}')
if cert_subject is None: if cert_subject is None:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
@ -240,7 +200,7 @@ class ExOTAMilter(Milter.Base):
) )
else: else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Untrusted x509 client CN {0} - action=reject".format(cert_subject) "/EOM Untrusted x509 client CN {0} - action=reject".format(cert_subject)
) )
return self.smfir_reject( return self.smfir_reject(
queue_id = self.getsymval('i'), queue_id = self.getsymval('i'),
@ -249,7 +209,7 @@ class ExOTAMilter(Milter.Base):
if self.hdr_from is None: if self.hdr_from is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: exception: could not determine 5322.from header - action=reject" "/EOM exception: could not determine 5322.from header - action=reject"
) )
return self.smfir_reject( return self.smfir_reject(
queue_id = self.getsymval('i'), queue_id = self.getsymval('i'),
@ -261,48 +221,20 @@ class ExOTAMilter(Milter.Base):
try: try:
policy = g_policy_backend.get(self.hdr_from_domain) policy = g_policy_backend.get(self.hdr_from_domain)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) + logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy for 5322.from_domain={0} fetched from backend".format( "/EOM Policy for 5322.from_domain={0} fetched from backend".format(self.hdr_from_domain)
self.hdr_from_domain
)
) )
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e: except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.from: {0}".format(e.message) "/EOM {0}".format(e.message)
)
# Forwarded message? Maybe the Resent-From header domain matches.
if self.hdr_resent_from_domain is not None:
try:
policy = g_policy_backend.get(self.hdr_resent_from_domain)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy for 5322.resent_from_domain={0} fetched from backend".format(
self.hdr_resent_from_domain
)
)
self.forwarded = True
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Forwarded message -> Policy for 5322.resent_from_domain={0} found.".format(
self.hdr_resent_from_domain
)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.resent-from: {0}".format(e.message)
) )
return self.smfir_reject( return self.smfir_reject(
queue_id = self.getsymval('i'), queue_id = self.getsymval('i'),
reason = "No policy for 5322.resent_from_domain {0}".format( reason = "No policy for {0}".format(self.hdr_from_domain)
self.hdr_resent_from_domain
)
)
else:
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = "No policy for 5322.from_domain {0}".format(self.hdr_from_domain)
) )
if self.hdr_tenant_id is None: if self.hdr_tenant_id is None:
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject" "/EOM exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
) )
return self.smfir_reject( return self.smfir_reject(
queue_id = self.getsymval('i'), queue_id = self.getsymval('i'),
@ -336,23 +268,15 @@ class ExOTAMilter(Milter.Base):
) )
if self.dkim_valid: if self.dkim_valid:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Valid DKIM signatures found" "/EOM: Found valid DKIM authentication result for 5322.from_domain={0}".format(
self.hdr_from_domain
) )
for passed_dkim_result in self.passed_dkim_results:
if self.hdr_from_domain == passed_dkim_result['sdid']:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Found aligned DKIM signature for SDID: {0}".format(
passed_dkim_result['sdid']
)
)
self.dkim_aligned = True
if not self.dkim_aligned:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No aligned DKIM signatures found!"
) )
else: else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No valid DKIM authentication result found" "/EOM: No valid DKIM authentication result found for 5322.from_domain={0}".format(
self.hdr_from_domain
)
) )
return self.smfir_reject( return self.smfir_reject(
queue_id = self.getsymval('i'), queue_id = self.getsymval('i'),
@ -373,16 +297,12 @@ class ExOTAMilter(Milter.Base):
if g_milter_add_header: if g_milter_add_header:
try: try:
addhdr_value = str( self.addheader("X-ExOTA-Authentication-Results",
"{0};\n" + "{0};\n auth=pass header.d={1} dkim={2} x509_client_trust={3}".format(
" auth=pass 5322_from_domain={1} dkim={2} dkim_aligned={3} " +
"x509_client_trust={4} forwarded={5}"
).format(
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(), g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
self.dkim_aligned, g_milter_x509_enabled, self.forwarded g_milter_x509_enabled
)
) )
logging.debug(addhdr_value)
self.addheader("X-ExOTA-Authentication-Results", addhdr_value)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) + logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: AR-header added" "/EOM: AR-header added"
) )
@ -393,8 +313,8 @@ class ExOTAMilter(Milter.Base):
if g_milter_dkim_enabled: if g_milter_dkim_enabled:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) + logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized (dkim_enabled={0} dkim_aligned={1})".format( "/EOM: Tenant successfully authorized (dkim_enabled={0})".format(
policy.is_dkim_enabled(), self.dkim_aligned str(policy.is_dkim_enabled())
) )
) )
else: else:
@ -455,10 +375,6 @@ if __name__ == "__main__":
if 'MILTER_X509_TRUSTED_CN' in os.environ: if 'MILTER_X509_TRUSTED_CN' in os.environ:
g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN'] g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN']
logging.info("ENV[MILTER_X509_TRUSTED_CN]: {0}".format(g_milter_x509_trusted_cn)) logging.info("ENV[MILTER_X509_TRUSTED_CN]: {0}".format(g_milter_x509_trusted_cn))
if 'MILTER_X509_IP_WHITELIST' in os.environ:
g_milter_x509_ip_whitelist = "".join(os.environ['MILTER_X509_IP_WHITELIST'].split())
g_milter_x509_ip_whitelist = g_milter_x509_ip_whitelist.split(',')
logging.info("ENV[MILTER_X509_IP_WHITELIST]: {0}".format(g_milter_x509_ip_whitelist))
logging.info("ENV[MILTER_X509_ENABLED]: {0}".format(g_milter_x509_enabled)) logging.info("ENV[MILTER_X509_ENABLED]: {0}".format(g_milter_x509_enabled))
if 'MILTER_POLICY_SOURCE' in os.environ: if 'MILTER_POLICY_SOURCE' in os.environ:
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE'] g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']

View File

@ -89,7 +89,7 @@ class ExOTAPolicyBackendJSON(ExOTAPolicyBackend):
return ExOTAPolicy(self.policies[from_domain]) return ExOTAPolicy(self.policies[from_domain])
except KeyError as e: except KeyError as e:
raise ExOTAPolicyNotFoundException( raise ExOTAPolicyNotFoundException(
"Policy for domain={0} not found".format(from_domain) "Policy for from_domain={0} not found".format(from_domain)
) from e ) from e
except Exception as e: except Exception as e:
raise ExOTAPolicyException( raise ExOTAPolicyException(

View File

@ -19,7 +19,6 @@ export MILTER_DKIM_ENABLED=yepp
export MILTER_TRUSTED_AUTHSERVID=my-auth-serv-id export MILTER_TRUSTED_AUTHSERVID=my-auth-serv-id
export MILTER_X509_ENABLED=yepp export MILTER_X509_ENABLED=yepp
export MILTER_X509_TRUSTED_CN=mail.protection.outlook.com export MILTER_X509_TRUSTED_CN=mail.protection.outlook.com
export MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
export MILTER_ADD_HEADER=yepp export MILTER_ADD_HEADER=yepp
export MILTER_AUTHSERVID=my-auth-serv-id export MILTER_AUTHSERVID=my-auth-serv-id
``` ```

View File

@ -6,9 +6,6 @@ conn = mt.connect(socket)
if conn == nil then if conn == nil then
error "mt.connect() failed" error "mt.connect() failed"
end end
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(3) mt.set_timeout(3)
@ -21,7 +18,7 @@ if mt.getreply(conn) ~= SMFIR_CONTINUE then
end end
-- 5321.RCPT+MACROS -- 5321.RCPT+MACROS
mt.macro(conn, SMFIC_RCPT, "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx") mt.macro(conn, SMFIC_RCPT, '{client_addr}', "127.128.129.130", "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.com")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= nil then if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= nil then
error "mt.rcptto() failed" error "mt.rcptto() failed"
end end
@ -33,9 +30,6 @@ end
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@yad.onmicrosoft.com>') ~= nil then if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@yad.onmicrosoft.com>') ~= nil then
error "mt.header(From) failed" error "mt.header(From) failed"
end end
if mt.header(conn, "aaa-resent-fRoM", '"Blah Blubb" <blah@yad.onmicrosoft.COMa>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
error "mt.header(Subject) failed" error "mt.header(Subject) failed"
end end
@ -51,7 +45,7 @@ end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(Subject) failed" error "mt.header(Subject) failed"
end end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.comx header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.com header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Subject) failed" error "mt.header(Subject) failed"
end end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector2-asdf header.b=mmmjFpv8") ~= nil then if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector2-asdf header.b=mmmjFpv8") ~= nil then