aws-email-check: only report ban-risk mails (suspension/abuse/suspicious activity)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:42:51 +08:00
co-authored by Claude Sonnet 5.5
parent 5db6f351cd
commit ce0eaa9e95
3 changed files with 256 additions and 113 deletions
+6 -2
View File
@@ -1,6 +1,6 @@
--- ---
name: aws-email-check name: aws-email-check
description: 读取当天新到的 AWS 账号风控邮件,过滤登录验证码和资源访问请求验证成功通知、去重并按中文字段顺序汇报已提取的风险与处理信息。适用于定时检查配置的 IMAP 邮箱。 description: 读取当天新到的 AWS 账号风控邮件,过滤登录验证码、资源访问请求验证成功及域名到期续费提醒,去重并按中文字段顺序汇报风险与处理信息。适用于定时检查配置的 IMAP 邮箱。
--- ---
# AWS 账号风控邮件检查 # AWS 账号风控邮件检查
@@ -11,10 +11,14 @@ description: 读取当天新到的 AWS 账号风控邮件,过滤登录验证
脚本仅检查 `timezone` 所设时区当天到达邮箱的邮件,以 IMAP `INTERNALDATE` 为准,而不是转发邮件引用的原始日期;使用邮箱 UIDVALIDITY + UID 及 Message-ID 持久去重。每小时重复调用同一命令即可。状态记录的是已扫描/已生成报告,不代表 AstrBot 消息已送达;有新报告时,脚本将其原文随去重状态保存,最多保留最近 48 次非空检查。普通检查不会自动重发,也不会用“无新增”覆盖历史。仅在确认某次发送失败且消息未送达时,可人工运行 `python scripts/check_mail.py --config config.json --list-report-batches` 定位批次 ID,再运行 `python scripts/check_mail.py --config config.json --replay-id BATCH_ID` 重放;最近一次也可用 `--replay-last`。维护命令不连接邮箱、不改变状态;重放报告的 stdout 应原样发给用户。若送达状态不确定,不得盲目重发。此机制不保证端到端 exactly-once 投递。 脚本仅检查 `timezone` 所设时区当天到达邮箱的邮件,以 IMAP `INTERNALDATE` 为准,而不是转发邮件引用的原始日期;使用邮箱 UIDVALIDITY + UID 及 Message-ID 持久去重。每小时重复调用同一命令即可。状态记录的是已扫描/已生成报告,不代表 AstrBot 消息已送达;有新报告时,脚本将其原文随去重状态保存,最多保留最近 48 次非空检查。普通检查不会自动重发,也不会用“无新增”覆盖历史。仅在确认某次发送失败且消息未送达时,可人工运行 `python scripts/check_mail.py --config config.json --list-report-batches` 定位批次 ID,再运行 `python scripts/check_mail.py --config config.json --replay-id BATCH_ID` 重放;最近一次也可用 `--replay-last`。维护命令不连接邮箱、不改变状态;重放报告的 stdout 应原样发给用户。若送达状态不确定,不得盲目重发。此机制不保证端到端 exactly-once 投递。
**只提醒可能导致封号的邮件**:必须有明确的封禁信号才生成提醒——账号受限/暂停/关闭、资源滥用或钓鱼报告、可疑活动、访问密钥泄露、要求提交身份核验材料。仅含 verify、验证、risk 等泛词的普通通知(如身份验证通知、证书/凭据到期)一律不提醒。
登录验证码、OTP 和一次性密码邮件不生成提醒,包括转发邮件、中英文及 HTML 邮件;异常活动提示、建议更换密码或启用 MFA 等验证码安全套话不会将其变成风控提醒。明确说明账号受限、暂停、关闭,或要求提交身份核验材料的真实风控通知仍保留,不能仅凭“验证”、verify 或 MFA 字样过滤。过滤邮件仍记录扫描去重,但不保存到报告历史。 登录验证码、OTP 和一次性密码邮件不生成提醒,包括转发邮件、中英文及 HTML 邮件;异常活动提示、建议更换密码或启用 MFA 等验证码安全套话不会将其变成风控提醒。明确说明账号受限、暂停、关闭,或要求提交身份核验材料的真实风控通知仍保留,不能仅凭“验证”、verify 或 MFA 字样过滤。过滤邮件仍记录扫描去重,但不保存到报告历史。
“您访问 AWS 资源的请求已得到验证 / Your Request For Accessing AWS Resources Has Been Validated”这类资源访问请求验证成功通知也不生成提醒,支持中英文、转发及 HTML 邮件。同一邮件仍有明确的账号限制、暂停、关闭、访问密钥泄露、资源滥用或核验材料提交要求时保留提醒;不能泛化过滤所有包含 validated 或“验证”的邮件。 “您访问 AWS 资源的请求已得到验证 / Your Request For Accessing AWS Resources Has Been Validated”这类资源访问请求验证成功通知也不生成提醒,支持中英文、转发及 HTML 邮件。同一邮件仍有明确的账号限制、暂停、关闭、访问密钥泄露、资源滥用或核验材料提交要求时保留提醒;不能泛化过滤所有包含 validated 或“验证”的邮件。
域名注册到期、续费提醒及续费失败通知不生成提醒,包括 Route 53 的 `[Domain expiring] Registration for … expires on …`,支持中英文、转发和 HTML 邮件,不限定域名或日期。只识别以域名注册或续费为主体的提醒,提到域名的证书、凭据过期邮件不属于此类。域名自动续费关闭、续费失败或域名因到期暂停属于业务状态;同一邮件包含明确的 AWS 账号限制、暂停、关闭、密钥泄露、资源滥用或身份核验材料提交要求时仍保留提醒。不能仅凭 domain、expires 或“续费”字样过滤;过滤邮件仍扫描去重,不保存到报告历史。
输出每封邮件按以下字段顺序排列,多封之间空一行。只展示有可信非空提取结果的字段;账号 ID、警告信息、涉及资源、要求操作、截止时间等任何缺失字段均整行省略,不输出空标签或 `未提取`,不猜测补齐。没有符合条件的新邮件时只输出固定的“无”行,不列出被过滤邮件或过滤详情。不输出任何访问密钥、授权码、密码或令牌。 输出每封邮件按以下字段顺序排列,多封之间空一行。只展示有可信非空提取结果的字段;账号 ID、警告信息、涉及资源、要求操作、截止时间等任何缺失字段均整行省略,不输出空标签或 `未提取`,不猜测补齐。没有符合条件的新邮件时只输出固定的“无”行,不列出被过滤邮件或过滤详情。不输出任何访问密钥、授权码、密码或令牌。
```text ```text
@@ -33,4 +37,4 @@ AWS账号ID:...
今日无新增AWS账号风控邮件。 今日无新增AWS账号风控邮件。
``` ```
当前工作区有 `参考邮件` 目录时,本地样本验证可用 `python scripts/check_mail.py --eml-dir 参考邮件 --date 2026-09-28`;`过滤邮件` 中的验证码和资源访问请求验证成功样本也可用同一命令指定目录和对应日期验证。本地模式不读取或改写生产去重状态;这两个目录含真实邮件,不纳入版本控制。离线回归测试使用 `python -m unittest discover -s tests -v`,真实样本目录缺失时相应样本测试跳过,合成邮件测试仍完整运行。 当前工作区有 `参考邮件` 目录时,本地样本验证可用 `python scripts/check_mail.py --eml-dir 参考邮件 --date 2026-09-28`;`过滤邮件` 中的验证码、资源访问请求验证成功和域名到期样本也可用同一命令指定目录和对应日期验证,新增域名到期样本的邮件日期为 `2026-10-10`。本地模式不读取或改写生产去重状态;这两个目录含真实邮件,不纳入版本控制。离线回归测试使用 `python -m unittest discover -s tests -v`,真实样本目录缺失时相应样本测试跳过,合成邮件测试仍完整运行。
+56 -1
View File
@@ -39,6 +39,31 @@ ACCESS_VALIDATED = re.compile(
r"(?:successfully\s+)?validated\b|" r"(?:successfully\s+)?validated\b|"
r"您访问\s*AWS\s*资源的请求已(?:得到验证|通过验证|验证成功)", re.I r"您访问\s*AWS\s*资源的请求已(?:得到验证|通过验证|验证成功)", re.I
) )
DOMAIN_EXPIRATION_NOTICE = re.compile(
r"\[\s*domain\s+expiring\s*\]|\bdomain\s+expiring\s+soon\b|"
r"(?:^|[\n.!?;,::,。])\s*registration\s+for\s+"
r"[a-z0-9.-]+\.[a-z]{2,}\s+expires?\s+on\b|"
r"\bdomain\s+(?:registration\s+)?(?:expiration\s+(?:notice|reminder|date)|"
r"renewal\s+(?:reminder|notice|failed|failure))\b|"
r"\bdomain\s+registration\s+(?:(?:for\s+)?[a-z0-9.-]+\.[a-z]{2,}\s+)?"
r"(?:is\s+scheduled\s+to\s+expire|will\s+expire|expires?\s+on)\b|"
r"(?:^|[\n.!?;,::,。])\s*(?:your|the|this)\s+domain(?:\s+name)?\s+"
r"(?:[a-z0-9.-]+\.[a-z]{2,}\s+)?"
r"(?:is\s+scheduled\s+to\s+expire|will\s+expire|expires?\s+on)\b|"
r"\bdomain\s+renewal\b[^\n]{0,60}\b(?:failed|unsuccessful)\b|"
r"域名(?:\s*[a-z0-9.-]+)?\s*(?:(?:的)?注册(?:有效期|期限)?)?\s*"
r"(?:即将到期|到期提醒|到期通知|过期提醒|续费提醒|续费通知|续费失败|"
r"将于\s*\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?\s*到期)", re.I
)
DOMAIN_BUSINESS_STATE = re.compile(
r"(?P<prefix>^|[\n.!?。!?;;,,]|\b(?:and|but|while|before|after)\s+|而)"
r"\s*(?:(?:your|the|this)\s+|您的|该)?"
r"(?:\b(?:domain(?:\s+(?:registration|[a-z0-9.-]+\.[a-z]{2,}))?|"
r"(?:automatic(?:\s+annual)?\s+)?renewal)\s+"
r"(?:(?:has\s+been|is|was|will\s+be)\s+)?"
r"(?:suspended|restricted|disabled|terminated|closed)\b|"
r"域名(?:注册|解析)?(?:已被|已|被)?(?:暂停|受限|停用|关闭))", re.I | re.M
)
RISK_ALERT_SUBJECT = re.compile( RISK_ALERT_SUBJECT = re.compile(
r"suspicious activity|security (?:alert|warning)|abuse report|phishing|compromis|" r"suspicious activity|security (?:alert|warning)|abuse report|phishing|compromis|"
r"可疑活动|异常活动|滥用|钓鱼|泄露|安全警告", re.I r"可疑活动|异常活动|滥用|钓鱼|泄露|安全警告", re.I
@@ -144,6 +169,25 @@ def is_access_validated_notice(subject, body):
) )
def has_domain_notice_risk(text):
# A domain's business state must not become an account restriction merely
# because the same clause also mentions the AWS account used for billing.
return has_explicit_risk(
DOMAIN_BUSINESS_STATE.sub(r"\g<prefix>", text), ACCESS_NOTICE_RISK_EVIDENCE
)
def is_domain_expiration_notice(subject, body):
"""Ignore domain-expiration business notices, while retaining actual risks."""
combined = subject + "\n" + body
return bool(
DOMAIN_EXPIRATION_NOTICE.search(combined)
and not (has_domain_notice_risk(combined)
or EXPLICIT_CLOSURE_SUBJECT.search(subject)
or RISK_ALERT_SUBJECT.search(subject))
)
class PlainHTML(HTMLParser): class PlainHTML(HTMLParser):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -268,12 +312,23 @@ def extract(message, received):
subject = header_value(message, "Subject") subject = header_value(message, "Subject")
body = body_text(message) body = body_text(message)
combined = subject + "\n" + body combined = subject + "\n" + body
if is_verification_code(subject, body) or is_access_validated_notice(subject, body): if (is_verification_code(subject, body)
or is_access_validated_notice(subject, body)
or is_domain_expiration_notice(subject, body)):
return None return None
if not RISK_TERMS.search(combined) or not ( if not RISK_TERMS.search(combined) or not (
WARNING_TERMS.search(combined) or EXPLICIT_CLOSURE_SUBJECT.search(subject) WARNING_TERMS.search(combined) or EXPLICIT_CLOSURE_SUBJECT.search(subject)
or (DOMAIN_EXPIRATION_NOTICE.search(combined)
and has_domain_notice_risk(combined))
): ):
return None return None
# Allowlist: generic words like "verification" or "risk" are not enough.
# Only mail that signals a possible ban (restriction, suspension, abuse,
# suspicious activity, compromised keys, document requests) is reported.
if not (has_explicit_risk(combined, ACCESS_NOTICE_RISK_EVIDENCE)
or EXPLICIT_CLOSURE_SUBJECT.search(subject)
or RISK_ALERT_SUBJECT.search(subject)):
return None
# Only explicit 12-digit IDs with account context qualify. Case IDs and dates do not. # Only explicit 12-digit IDs with account context qualify. Case IDs and dates do not.
identifier = account_id(combined) identifier = account_id(combined)
warning = lines_matching(body, re.compile( warning = lines_matching(body, re.compile(
+194 -110
View File
@@ -64,16 +64,11 @@ class ExtractionTests(unittest.TestCase):
self.assertEqual(record["action"], check_mail.EMPTY) self.assertEqual(record["action"], check_mail.EMPTY)
self.assertEqual(record["deadline"], check_mail.EMPTY) self.assertEqual(record["deadline"], check_mail.EMPTY)
def test_identity_verification_footer_does_not_imply_compromise(self): def test_identity_verification_footer_is_not_ban_alert(self):
record = synthetic( self.assertIsNone(synthetic(
"验证您的身份", "验证您的身份",
"AWS 身份验证通知。为保护您的安全,如果您发现未授权访问,请联系支持团队。", "AWS 身份验证通知。为保护您的安全,如果您发现未授权访问,请联系支持团队。",
) ))
self.assertEqual(record["type"], "身份验证通知")
self.assertEqual(record["warning"], check_mail.EMPTY)
self.assertEqual(record["action"], check_mail.EMPTY)
self.assertEqual(record["account"], check_mail.EMPTY)
self.assertEqual(record["deadline"], check_mail.EMPTY)
def test_specific_subject_outweighs_quoted_phishing_report(self): def test_specific_subject_outweighs_quoted_phishing_report(self):
record = synthetic( record = synthetic(
@@ -83,91 +78,180 @@ class ExtractionTests(unittest.TestCase):
) )
self.assertEqual(record["type"], "账户验证或暂停") self.assertEqual(record["type"], "账户验证或暂停")
def test_local_forwarded_login_code_is_filtered(self): def test_local_forwarded_login_code_is_filtered(self):
self.assertIsNone(sample("*FW__*.eml", "过滤邮件")) self.assertIsNone(sample("*FW__*.eml", "过滤邮件"))
def test_local_forwarded_access_validated_notice_is_filtered(self): def test_local_forwarded_access_validated_notice_is_filtered(self):
self.assertIsNone(sample("*Resources_Has_Been_Validated.eml", "过滤邮件")) self.assertIsNone(sample("*Resources_Has_Been_Validated.eml", "过滤邮件"))
def test_access_validated_notices_are_filtered(self): def test_local_forwarded_domain_expiration_notice_is_filtered(self):
cases = [ self.assertIsNone(sample("*Domain_expiring*.eml", "过滤邮件"))
("您访问 AWS 资源的请求已得到验证", "您的 AWS 资源访问请求已完成核验,现在可以访问资源。", False),
("AWS: Your Request For Accessing AWS Resources Has Been Validated", def test_domain_expiration_notices_are_filtered(self):
"The request is complete. You may access AWS resources.", False), cases = [
("Fw: 您访问 AWS 资源的请求已得到验证 / " ("Fw:[Domain expiring] Registration for example.net expires on 2027-02-11",
"Your Request For Accessing AWS Resources Has Been Validated", "Dear AWS customer, you have disabled automatic annual renewal for this domain.", False),
"AWS 资源访问现已恢复。如果您有任何问题,请联系支持。", False), ("FWD: [DOMAIN EXPIRING] Registration for other.org expires on 2028-04-09",
("Fwd: AWS notification", "Forwarded message:\nSubject: Your Request For Accessing " "AWS domain registration verification: automatic renewal is disabled.", False),
"AWS Resources Has Been Validated\nYou may access resources again.", False), ("AWS notification", "Forwarded message:\nSubject: Registration for example.net "
("FW: AWS notification", "<div>Your Request For Accessing</div><p>AWS Resources " "expires on 2027-02-11\nAutomatic renewal is disabled.", False),
"Has Been Successfully Validated</p><p>You may access AWS resources.</p>", True), ("FW: AWS notification", "<div>Domain Expiring Soon</div><p>Your domain is "
("Fw: AWS 通知", "<p>您访问 AWS 资源的请求已得到验证</p><p>现在可以访问资源。</p>", True), "scheduled to expire. Automatic annual renewal is disabled.</p>", True),
] ("转发:AWS 域名到期提醒", "您的域名即将到期,自动续费已停用。", False),
for subject, body, rich in cases: ("AWS 通知", "<p>域名注册将于 2027-02-11 到期</p><p>自动续费已停用。</p>", True),
with self.subTest(subject=subject, rich=rich): ]
self.assertIsNone(synthetic(subject, body, rich)) for subject, body, rich in cases:
with self.subTest(subject=subject, rich=rich):
def test_success_notice_does_not_hide_actual_risk_or_required_documents(self): self.assertIsNone(synthetic(subject, body, rich))
success = "Your Request For Accessing AWS Resources Has Been Validated"
risks = [ def test_domain_renewal_business_states_are_filtered(self):
"Your AWS account has been suspended.", cases = [
"Your AWS account is restricted.", ("AWS domain renewal failed", "Automatic renewal is disabled. "
"Your AWS account is disabled.", "The domain has been suspended because its registration expired."),
"Your AWS account has been terminated.", ("AWS domain renewal reminder", "Your domain is disabled after expiration. "
"Your AWS account is closed.", "Payment verification failed; please renew its registration."),
"Your access key has been compromised.", ("AWS 域名续费失败", "域名已暂停解析,域名注册已关闭。请重新验证续费付款信息。"),
"Your AWS resource has been implicated in hosting a phishing website.", ("AWS domain renewal failed", "We could not charge your AWS account "
"We detected potentially unwanted activity in your AWS account.", "before the domain was suspended due to registration expiry."),
"Please upload your identity documents.", ("AWS 域名到期提醒", "您的 AWS 账户未扣款而域名已暂停。"),
"If you fail to respond, your account will be suspended.", ("[Domain expiring] AWS registration notice", "If your AWS account is suspended, "
"您的 AWS 账户已受限。", "please contact support. Automatic annual renewal is disabled."),
"您的账户已暂停。请提交身份证明材料。", ]
] for subject, body in cases:
for body in risks: with self.subTest(subject=subject):
with self.subTest(body=body): self.assertIsNone(synthetic(subject, body))
self.assertIsNotNone(synthetic(success, body))
for subject in ("AWS Abuse Report", "AWS Suspicious Activity", "AWS Account Permanent Closure Confirmation"): def test_domain_expiration_does_not_hide_actual_account_or_resource_risk(self):
with self.subTest(subject=subject): subject = "[Domain expiring] Registration for example.net expires on 2027-02-11"
self.assertIsNotNone(synthetic(subject, success)) risks = [
"Your AWS account has been suspended.",
def test_other_validation_notices_remain_reported(self): "Your AWS account has been suspended and your domain is disabled.",
cases = [ "Your AWS account associated with this domain is suspended.",
("AWS Identity Validated", "Contact support regarding your identity verification."), "Your AWS account for this domain is suspended.",
("AWS 验证您的身份", "请上传身份证核验材料。"), "Your AWS account is restricted.",
("Your Request For Accessing AWS Resources Has Not Been Validated", "Your AWS account is disabled.",
"We could not validate important details. Please upload proof of identity."), "Your AWS account has been terminated.",
("AWS notification", "Your Request For Accessing AWS Resources Has Been Validated? " "Your AWS account is closed.",
"Your account has been placed on hold."), "Your access key has been compromised. AWS security notification.",
] "Your AWS resource has been implicated in hosting a phishing website.",
for subject, body in cases: "We detected potentially unwanted activity in your AWS account.",
with self.subTest(subject=subject): "AWS verification: Please upload your identity documents.",
self.assertIsNotNone(synthetic(subject, body)) "AWS: If you fail to respond, your account will be suspended.",
"您的 AWS 账户已受限。",
def test_access_success_keeps_explicit_resource_risk_actions(self): "您的 AWS 账户已暂停。请提交身份证明材料。",
subject = "Your Request For Accessing AWS Resources Has Been Validated" "AWS: Please delete malicious content hosted on your resource.",
cases = [ ]
"Please take action immediately to remove phishing content from your AWS resource.", for body in risks:
"Please delete malicious content hosted on your AWS resource.", with self.subTest(body=body):
"You must stop abusive traffic from your AWS resource.", self.assertIsNotNone(synthetic(subject, body))
"请立即删除您的 AWS 资源上的钓鱼内容。", for risk_subject in ("AWS Abuse Report", "AWS Suspicious Activity",
"必须停止 AWS 资源上的滥用行为。", "AWS Account Permanent Closure Confirmation"):
] with self.subTest(subject=risk_subject):
for body in cases: self.assertIsNotNone(synthetic(risk_subject, subject))
with self.subTest(body=body):
self.assertIsNotNone(synthetic(subject, body)) def test_generic_verification_and_expiry_notices_are_not_ban_alerts(self):
cases = [
def test_access_success_security_tips_are_still_filtered(self): ("AWS identity verification", "Your verification session expires on 2027-02-11."),
subject = "Your Request For Accessing AWS Resources Has Been Validated" ("AWS domain security verification", "Verify your domain ownership."),
cases = [ ("AWS renewal verification", "Please verify your billing information."),
"If you find phishing content, please remove phishing content from your AWS resource.", ("AWS 域名验证", "请验证域名所有权。"),
"If you have questions, please take action to contact AWS support.", ("AWS certificate verification required", "Your SSL certificate for domain example.com "
"如果您发现钓鱼内容,请立即删除 AWS 资源上的钓鱼内容。", "expires on 2026-10-17. Please renew the certificate."),
"如果您有任何问题,请联系支持团队。", ("AWS credentials verification required", "Your credentials for domain example.com "
"Please contact AWS support if you need help accessing resources.", "expire on 2026-10-17. Please rotate credentials."),
] ("AWS certificate verification required", "Your domain certificate expires on "
for body in cases: "2026-10-17. Please renew the certificate."),
with self.subTest(body=body): ("AWS certificate verification required", "Your certificate for your domain "
self.assertIsNone(synthetic(subject, body)) "example.com expires on 2026-10-17. Please renew the certificate."),
("AWS certificate verification", "Registration for certificate expires on "
"2026-10-17. Please renew the certificate."),
("AWS certificate verification", "Your certificate registration for example.com "
"expires on 2026-10-17. Please renew the certificate."),
("AWS 证书风险通知", "域名 example.com 的 SSL 证书将于 2026-10-17 到期,请更新证书。"),
("AWS 凭据风险通知", "域名 example.com 的登录凭据将于 2026-10-17 到期,请更新凭据。"),
]
for subject, body in cases:
with self.subTest(subject=subject):
self.assertIsNone(synthetic(subject, body))
def test_access_validated_notices_are_filtered(self):
cases = [
("您访问 AWS 资源的请求已得到验证", "您的 AWS 资源访问请求已完成核验,现在可以访问资源。", False),
("AWS: Your Request For Accessing AWS Resources Has Been Validated",
"The request is complete. You may access AWS resources.", False),
("Fw: 您访问 AWS 资源的请求已得到验证 / "
"Your Request For Accessing AWS Resources Has Been Validated",
"AWS 资源访问现已恢复。如果您有任何问题,请联系支持。", False),
("Fwd: AWS notification", "Forwarded message:\nSubject: Your Request For Accessing "
"AWS Resources Has Been Validated\nYou may access resources again.", False),
("FW: AWS notification", "<div>Your Request For Accessing</div><p>AWS Resources "
"Has Been Successfully Validated</p><p>You may access AWS resources.</p>", True),
("Fw: AWS 通知", "<p>您访问 AWS 资源的请求已得到验证</p><p>现在可以访问资源。</p>", True),
]
for subject, body, rich in cases:
with self.subTest(subject=subject, rich=rich):
self.assertIsNone(synthetic(subject, body, rich))
def test_success_notice_does_not_hide_actual_risk_or_required_documents(self):
success = "Your Request For Accessing AWS Resources Has Been Validated"
risks = [
"Your AWS account has been suspended.",
"Your AWS account is restricted.",
"Your AWS account is disabled.",
"Your AWS account has been terminated.",
"Your AWS account is closed.",
"Your access key has been compromised.",
"Your AWS resource has been implicated in hosting a phishing website.",
"We detected potentially unwanted activity in your AWS account.",
"Please upload your identity documents.",
"If you fail to respond, your account will be suspended.",
"您的 AWS 账户已受限。",
"您的账户已暂停。请提交身份证明材料。",
]
for body in risks:
with self.subTest(body=body):
self.assertIsNotNone(synthetic(success, body))
for subject in ("AWS Abuse Report", "AWS Suspicious Activity", "AWS Account Permanent Closure Confirmation"):
with self.subTest(subject=subject):
self.assertIsNotNone(synthetic(subject, success))
def test_other_validation_notices_remain_reported(self):
cases = [
("AWS 验证您的身份", "请上传身份证核验材料。"),
("Your Request For Accessing AWS Resources Has Not Been Validated",
"We could not validate important details. Please upload proof of identity."),
("AWS notification", "Your Request For Accessing AWS Resources Has Been Validated? "
"Your account has been placed on hold."),
]
for subject, body in cases:
with self.subTest(subject=subject):
self.assertIsNotNone(synthetic(subject, body))
def test_access_success_keeps_explicit_resource_risk_actions(self):
subject = "Your Request For Accessing AWS Resources Has Been Validated"
cases = [
"Please take action immediately to remove phishing content from your AWS resource.",
"Please delete malicious content hosted on your AWS resource.",
"You must stop abusive traffic from your AWS resource.",
"请立即删除您的 AWS 资源上的钓鱼内容。",
"必须停止 AWS 资源上的滥用行为。",
]
for body in cases:
with self.subTest(body=body):
self.assertIsNotNone(synthetic(subject, body))
def test_access_success_security_tips_are_still_filtered(self):
subject = "Your Request For Accessing AWS Resources Has Been Validated"
cases = [
"If you find phishing content, please remove phishing content from your AWS resource.",
"If you have questions, please take action to contact AWS support.",
"如果您发现钓鱼内容,请立即删除 AWS 资源上的钓鱼内容。",
"如果您有任何问题,请联系支持团队。",
"Please contact AWS support if you need help accessing resources.",
]
for body in cases:
with self.subTest(body=body):
self.assertIsNone(synthetic(subject, body))
def test_login_codes_with_security_boilerplate_are_filtered(self): def test_login_codes_with_security_boilerplate_are_filtered(self):
cases = [ cases = [
@@ -212,12 +296,12 @@ class ExtractionTests(unittest.TestCase):
cases = [ cases = [
("AWS verification code", "Your verification code is 987654. Use this code to sign in.\n" ("AWS verification code", "Your verification code is 987654. Use this code to sign in.\n"
"If you think your access key has been compromised, change your password and enable MFA."), "If you think your access key has been compromised, change your password and enable MFA."),
("AWS 验证码", "您的验证码是 987654,用于登录。\n如果您的账户已暂停,请联系支持。"), ("AWS 验证码", "您的验证码是 987654,用于登录。\n如果您的账户已暂停,请联系支持。"),
("AWS verification code", "Your verification code is 987654. Use this code to sign in. " ("AWS verification code", "Your verification code is 987654. Use this code to sign in. "
"If your account is suspended, please submit identity documents to support."), "If your account is suspended, please submit identity documents to support."),
("AWS verification code", "Your verification code is 987654. Use this code to sign in. " ("AWS verification code", "Your verification code is 987654. Use this code to sign in. "
"If you need help, please provide identity documents to support."), "If you need help, please provide identity documents to support."),
("AWS 验证码", "验证码 987654,用于登录。如果您需要帮助,请提交身份证材料给支持团队。"), ("AWS 验证码", "验证码 987654,用于登录。如果您需要帮助,请提交身份证材料给支持团队。"),
] ]
for subject, body in cases: for subject, body in cases:
with self.subTest(subject=subject): with self.subTest(subject=subject):
@@ -239,12 +323,12 @@ class ExtractionTests(unittest.TestCase):
("AWS verification code", english_code + ("AWS verification code", english_code +
"If you do not upload documents by 2026-10-10, your account will be suspended."), "If you do not upload documents by 2026-10-10, your account will be suspended."),
("AWS 验证码", "验证码 987654,用于登录。如果您未能在截止日期回复您的账号将被暂停。"), ("AWS 验证码", "验证码 987654,用于登录。如果您未能在截止日期回复您的账号将被暂停。"),
("AWS verification code", english_code + ("AWS verification code", english_code +
"Your access key has been compromised. If your account is suspended, contact support."), "Your access key has been compromised. If your account is suspended, contact support."),
("AWS verification code", english_code + ("AWS verification code", english_code +
"If you need help, please provide identity documents to support, your account has been suspended."), "If you need help, please provide identity documents to support, your account has been suspended."),
("AWS verification code", english_code + ("AWS verification code", english_code +
"If you need help, please provide identity documents to support. Please upload proof of identity."), "If you need help, please provide identity documents to support. Please upload proof of identity."),
] ]
for subject, body in cases: for subject, body in cases:
with self.subTest(subject=subject, body=body): with self.subTest(subject=subject, body=body):
@@ -266,13 +350,13 @@ class ExtractionTests(unittest.TestCase):
self.assertEqual(report.splitlines(), ["【AWS账号风控邮件】", "风险类型:账户验证或暂停", self.assertEqual(report.splitlines(), ["【AWS账号风控邮件】", "风险类型:账户验证或暂停",
"截止时间:2026-10-10"]) "截止时间:2026-10-10"])
def test_complete_report_keeps_field_order(self): def test_complete_report_keeps_field_order(self):
record = synthetic("AWS Account on Hold: Response Required", record = synthetic("AWS Account on Hold: Response Required",
"AWS Account ID: 123456789012\nYour account has been placed on hold.\n" "AWS Account ID: 123456789012\nYour account has been placed on hold.\n"
"Resource i-12345678\nPlease reply before 2026-10-10.") "Resource i-12345678\nPlease reply before 2026-10-10.")
self.assertEqual(record["account"], "123456789012") self.assertEqual(record["account"], "123456789012")
self.assertEqual(record["resources"], "i-12345678") self.assertEqual(record["resources"], "i-12345678")
report = check_mail.format_report(record, ZoneInfo("Asia/Shanghai")) report = check_mail.format_report(record, ZoneInfo("Asia/Shanghai"))
self.assertEqual([line.split(":", 1)[0] for line in report.splitlines()], self.assertEqual([line.split(":", 1)[0] for line in report.splitlines()],
["【AWS账号风控邮件】", "收件时间", "邮件主题", "AWS账号ID", "风险类型", ["【AWS账号风控邮件】", "收件时间", "邮件主题", "AWS账号ID", "风险类型",
"警告信息", "涉及资源", "要求操作", "截止时间"]) "警告信息", "涉及资源", "要求操作", "截止时间"])