import colorsys, datetime, html, json, os, re, sys
# White ACKnowledge wordmark, 320x76 PNG (shown at 160x38). A data-URI image is
# turned into an inline attachment by exchange-send-mail and mail-sender; mail
# clients block SVG, hence the PNG.
LOGO_PNG_B64 = "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"
OUTER = "#55555a"
PAGE = "#1e1e1e"
HEADER = "#4a4a7c"
HEADER_RULE = "#5d5d8f"
PANEL = "#262626"
TEXT = "#e6e6e6"
MUTED = "#a9a9b8"
BLUE = "#4f9ae0"
BLUE_RULE = "#2f7fc8"
SIGNATURE = "#d8dcff"
WHITE = "#ffffff"
FONT = "'Exo 2','Segoe UI',Helvetica,Arial,sans-serif"
BODY_FONT = "'Segoe UI',Helvetica,Arial,sans-serif"
MONO = "Consolas,'SFMono-Regular',Menlo,'Courier New',monospace"
LEVELS = {
"critical": "#b3261e",
"high": "#c2410c",
"medium": "#7a3f12",
"low": "#2e6b3a",
"info": "#455a64",
"ok": "#1e7d4f",
}
LEVEL_ALIASES = {
"critique": "critical", "haute": "high", "élevée": "high", "elevee": "high", "élevé": "high",
"moyenne": "medium", "moyen": "medium", "faible": "low", "basse": "low",
"informational": "info", "information": "info",
}
LEVEL_LABELS = {
"fr": {"critical": "CRITIQUE", "high": "ÉLEVÉE", "medium": "MOYENNE", "low": "FAIBLE", "info": "INFO", "ok": "OK"},
"en": {"critical": "CRITICAL", "high": "HIGH", "medium": "MEDIUM", "low": "LOW", "info": "INFO", "ok": "OK"},
}
TEMPLATES = {
"sla_breach": {"color": "#a52a22", "icon": "⚠", "fr": "ALERTE SLA", "en": "SLA ALERT"},
"incident_notification": {"color": None, "icon": "●", "fr": "INCIDENT", "en": "INCIDENT"},
"incident_update": {"color": "#1f6fb2", "icon": "↻", "fr": "POINT DE SITUATION", "en": "STATUS UPDATE"},
"incident_closure": {"color": "#1e7d4f", "icon": "✓", "fr": "INCIDENT CLÔTURÉ", "en": "INCIDENT CLOSED"},
"action_required": {"color": "#b8620a", "icon": "⚑", "fr": "ACTION REQUISE", "en": "ACTION REQUIRED"},
"escalation": {"color": "#a8246f", "icon": "▲", "fr": "ESCALADE", "en": "ESCALATION"},
"threat_bulletin": {"color": "#6b3fa0", "icon": "◆", "fr": "BULLETIN DE VEILLE", "en": "THREAT BULLETIN"},
"info": {"color": "#55606e", "icon": "", "fr": "INFORMATION", "en": "INFORMATION"},
}
STRINGS = {
"fr": {
"months": None,
"at": "à",
"soc_footer": "Ce message est généré automatiquement par le bot {bot} — ACKnowledge SOC • Cybersécurité • Cloud • DevSecOps",
"soc_footer_text": "Ce message est généré automatiquement par le bot {bot} — ACKnowledge SOC • Cybersécurité • Cloud • DevSecOps",
"client_footer": "Ce message et ses éventuelles pièces jointes sont confidentiels et destinés exclusivement à leurs destinataires. Si vous l'avez reçu par erreur, merci d'en avertir l'expéditeur et de le supprimer.",
"tagline": "ACKnowledge SOC • Cybersécurité • Cloud • DevSecOps",
},
"en": {
"months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"at": "",
"soc_footer": "This message was generated automatically by the {bot} bot — ACKnowledge SOC • Cybersecurity • Cloud • DevSecOps",
"soc_footer_text": "This message was generated automatically by the {bot} bot — ACKnowledge SOC • Cybersecurity • Cloud • DevSecOps",
"client_footer": "This message and any attachments are confidential and intended solely for their addressees. If you received it in error, please notify the sender and delete it.",
"tagline": "ACKnowledge SOC • Cybersecurity • Cloud • DevSecOps",
},
}
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _s(v):
return "" if v is None else str(v).strip()
def _mix(a, b, t):
"""a blended toward b by t (0..1), as #rrggbb — solid colours, since Outlook drops rgba()."""
a = [int(a[i:i + 2], 16) for i in (1, 3, 5)]
b = [int(b[i:i + 2], 16) for i in (1, 3, 5)]
return "#" + "".join("%02x" % round(x + (y - x) * t) for x, y in zip(a, b))
def _vivid(c):
"""A light, saturated tint of c for text on a dark tinted box."""
h, _, sat = colorsys.rgb_to_hls(*(int(c[i:i + 2], 16) / 255 for i in (1, 3, 5)))
return "#" + "".join("%02x" % round(x * 255) for x in colorsys.hls_to_rgb(h, 0.64, max(sat, 0.75)))
def _level(name):
n = _s(name).lower()
n = LEVEL_ALIASES.get(n, n)
return n if n in LEVELS else None
def _pill(level, label):
return (
'%s' % (LEVELS[level], FONT, label)
)
# --- Markdown ---------------------------------------------------------------
# A small renderer for what a playbook writes in a mail: paragraphs, headings,
# emphasis, links, lists, quotes, rules, GFM tables, code, and {{level}} pills.
# Raw HTML is escaped, never passed through: the text often carries alert data.
_P = 'style="margin:0 0 14px;font-family:%s;font-size:15px;line-height:24px;color:%s"' % (BODY_FONT, TEXT)
_TABLE_SEP = re.compile(r"^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$")
_LIST = re.compile(r"^(\s*)([-*+]|\d{1,3}[.)])\s+(.*)$")
_HR = re.compile(r"^\s{0,3}([-*_])(\s*\1){2,}\s*$")
_HEADING = re.compile(r"^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$")
_FENCE = re.compile(r"^\s{0,3}(```|~~~)")
class Inline:
def __init__(self, lang):
self.lang = lang
def render(self, text):
keep = []
def stash(fragment):
keep.append(fragment)
return "\x00%d\x00" % (len(keep) - 1)
text = re.sub(
r"`([^`]+)`",
lambda m: stash(
'%s' % (MONO, html.escape(m.group(1)))
),
text,
)
text = html.escape(text, quote=True)
def link(label, url):
raw = html.unescape(url)
if not re.match(r"^(https?://|mailto:)", raw, re.I):
return label
return stash('%s' % (url, BLUE, label))
text = re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", lambda m: link(self._emphasis(m.group(1)), m.group(2)), text)
text = re.sub(r"<((?:https?://|mailto:)[^\s&]+)>", lambda m: link(m.group(1), m.group(1)), text)
text = re.sub(r"(?\1', text)
text = re.sub(r"(?\1', text)
text = re.sub(r"(?\1", text)
text = re.sub(r"(?\1", text)
text = re.sub(r"~~(?=\S)(.+?)(?<=\S)~~", r"\1", text)
return text
def _cells(line):
line = line.strip()
if line.startswith("|"):
line = line[1:]
if line.endswith("|") and not line.endswith("\\|"):
line = line[:-1]
return [c.strip().replace("\\|", "|") for c in re.split(r"(?")
or _LIST.match(line) or ("|" in line and i + 1 < len(lines) and _TABLE_SEP.match(lines[i + 1]))
)
def markdown(md, lang, accent):
lines = md.replace("\r\n", "\n").replace("\r", "\n").split("\n")
return _blocks(lines, Inline(lang), accent)
def _blocks(lines, inl, accent):
out = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
if not line.strip():
i += 1
continue
m = _FENCE.match(line)
if m:
fence = m.group(1)
body = []
i += 1
while i < n and not lines[i].strip().startswith(fence):
body.append(lines[i])
i += 1
i += 1
out.append(
'
' % (MONO, TEXT, html.escape("\n".join(body)))
)
continue
m = _HEADING.match(line)
if m:
level = len(m.group(1))
content = inl.render(m.group(2))
if level <= 2:
out.append(
'%s
'
% (BLUE_RULE, FONT, BLUE, content)
)
else:
out.append(
'%s
'
% (FONT, content)
)
i += 1
continue
if _HR.match(line):
out.append('
')
i += 1
continue
if "|" in line and i + 1 < n and _TABLE_SEP.match(lines[i + 1]):
head = _cells(line)
aligns = []
for c in _cells(lines[i + 1]):
aligns.append("center" if c.startswith(":") and c.endswith(":") else "right" if c.endswith(":") else "left")
i += 2
rows = []
while i < n and lines[i].strip() and "|" in lines[i]:
rows.append(_cells(lines[i]))
i += 1
out.append(_table(head, aligns, rows, inl))
continue
if line.lstrip().startswith(">"):
quoted = []
while i < n and lines[i].lstrip().startswith(">"):
quoted.append(re.sub(r"^\s*>\s?", "", lines[i]))
i += 1
out.append(
''
% (accent, _blocks(quoted, inl, accent))
)
continue
if _LIST.match(line):
html_list, i = _list(lines, i, inl, accent)
out.append(html_list)
continue
para = []
while i < n and lines[i].strip() and (not para or not _starts_block(lines, i)):
para.append(inl.render(lines[i].strip()))
i += 1
out.append("%s
" % (_P, "
".join(para)))
return "".join(out)
def _list(lines, i, inl, accent):
first = _LIST.match(lines[i])
indent = len(first.group(1))
ordered = first.group(2)[0].isdigit()
start = int(re.match(r"\d+", first.group(2)).group(0)) if ordered else 1
items = []
n = len(lines)
while i < n:
m = _LIST.match(lines[i])
if m and len(m.group(1)) == indent and m.group(2)[0].isdigit() == ordered:
items.append([m.group(3)])
i += 1
continue
line = lines[i]
if not line.strip():
# A blank line ends the list unless the list (or an item's body) goes on.
j = i + 1
if j < n and lines[j].strip() and (len(lines[j]) - len(lines[j].lstrip())) > indent:
items[-1].append("")
i += 1
continue
nxt = _LIST.match(lines[j]) if j < n else None
if nxt and len(nxt.group(1)) == indent:
i += 1
continue
break
if (len(line) - len(line.lstrip())) > indent:
items[-1].append(line[indent:])
i += 1
continue
if m and len(m.group(1)) < indent:
break
if not m and not _starts_block(lines, i):
items[-1].append(line) # lazy continuation
i += 1
continue
break
li = []
for body in items:
head, rest = body[0], body[1:]
inner = inl.render(head.strip())
if any(r.strip() for r in rest):
dedent = min((len(r) - len(r.lstrip()) for r in rest if r.strip()), default=0)
nested = [r[dedent:] for r in rest]
if not _LIST.match(next(r for r in nested if r.strip())):
inner += "
"
inner += _blocks(nested, inl, accent).replace("margin:0 0 14px", "margin:6px 0 0")
li.append('%s' % (BODY_FONT, TEXT, inner))
tag = "ol" if ordered else "ul"
attr = ' start="%d"' % start if ordered and start != 1 else ""
return '<%s%s style="margin:0 0 14px;padding:0 0 0 24px;color:%s">%s%s>' % (tag, attr, TEXT, "".join(li), tag), i
def _table(head, aligns, rows, inl):
width = len(head)
th = []
for k, cell in enumerate(head):
radius = "border-radius:%s;" % ("9px 0 0 0" if k == 0 else "0 9px 0 0") if k in (0, width - 1) else ""
th.append(
'%s | '
% (aligns[k] if k < len(aligns) else "left", PANEL, PANEL, radius, FONT,
aligns[k] if k < len(aligns) else "left", inl.render(cell))
)
body = []
for r in rows:
r = (r + [""] * width)[:width]
tds = []
for k, cell in enumerate(r):
align = aligns[k] if k < len(aligns) else "left"
tds.append(
'%s | ' % (align, BODY_FONT, TEXT, align, inl.render(cell))
)
body.append("%s
" % "".join(tds))
return (
'" % ("".join(th), "".join(body))
)
def markdown_text(md, lang):
"""The plain-text part: the markdown as written, with pills spelled out."""
def pill(m):
level = _level(m.group(1))
if not level:
return m.group(0)
return "[" + (m.group(2) or m.group(1).upper()) + "]"
return re.sub(r"\{\{\s*([^}:]+?)\s*(?::\s*([^}]*?)\s*)?\}\}", pill, md.strip())
def _plain(md):
"""A one-line excerpt for the hidden preheader."""
t = re.sub(r"```.*?```", " ", md, flags=re.S)
t = re.sub(r"\{\{\s*[^}:]+?\s*(?::\s*([^}]*?)\s*)?\}\}", lambda m: m.group(1) or "", t)
t = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", t)
t = re.sub(r"^\s*(#{1,6}|>|[-*+]|\d+[.)])\s+", "", t, flags=re.M)
t = re.sub(r"[*_`~|]", "", t)
t = re.sub(r"^\s*:?-{2,}.*$", "", t, flags=re.M)
return re.sub(r"\s+", " ", t).strip()[:140]
# --- Mail -------------------------------------------------------------------
def _when(raw, lang):
if raw:
try:
d = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
raise Exception("date must be ISO 8601 (e.g. 2026-07-16T12:54:00Z), got " + json.dumps(raw))
if d.tzinfo is None:
d = d.replace(tzinfo=datetime.timezone.utc)
d = d.astimezone(datetime.timezone.utc)
else:
d = datetime.datetime.now(datetime.timezone.utc)
if lang == "fr":
return d.strftime("%d/%m/%Y à %H:%M UTC")
return "%d %s %d, %s UTC" % (d.day, STRINGS["en"]["months"][d.month - 1], d.year, d.strftime("%H:%M"))
def _choice(inputs, key, allowed, default):
v = _s(inputs.get(key)).lower() or default
if v not in allowed:
raise Exception("%s must be one of %s, got %s" % (key, ", ".join(allowed), json.dumps(v)))
return v
def render(cfg, inputs):
template = _choice(inputs, "template", list(TEMPLATES), "info")
lang = _choice(inputs, "lang", ["fr", "en"], "fr")
audience = _choice(inputs, "audience", ["soc", "client"], "soc")
subject = _s(inputs.get("subject"))
description = str(inputs.get("description") or "").strip()
if not subject:
raise Exception("subject is required")
if not description:
raise Exception("description is required")
severity = None
if _s(inputs.get("severity")):
severity = _level(inputs.get("severity"))
if not severity or severity == "ok":
raise Exception("severity must be one of critical, high, medium, low, info, got " + json.dumps(_s(inputs.get("severity"))))
case_ref = _s(inputs.get("case_ref"))
button_label = _s(inputs.get("button_label"))
button_url = _s(inputs.get("button_url"))
if button_url and not re.match(r"^https?://", button_url, re.I):
raise Exception("button_url must start with http:// or https://")
if button_label and not button_url:
raise Exception("button_url is required with button_label")
callout_text = str(inputs.get("callout_text") or "").strip()
bot = _s(cfg.get("bot_name")) or "Riposte"
t = TEMPLATES[template]
badge = t[lang]
accent = t["color"]
if template == "incident_notification":
accent = LEVELS[severity] if severity else LEVELS["info"]
if severity:
badge = "%s %s" % (badge, LEVEL_LABELS[lang][severity]) if lang == "fr" else "%s %s" % (LEVEL_LABELS[lang][severity], badge)
badge_full = (t["icon"] + " " + badge).strip()
callout_title = _s(inputs.get("callout_title")) or badge_full
inl = Inline(lang)
esc = html.escape
when = _when(_s(inputs.get("date")), lang)
meta = [esc(when)]
if case_ref:
meta.append('%s' % (MONO, esc(case_ref)))
if severity and template != "incident_notification":
meta.append(_pill(severity, LEVEL_LABELS[lang][severity]))
meta_html = ' · ' % MUTED
meta_html = meta_html.join(meta)
callout = ""
if callout_text:
callout = (
'"
% (_mix(accent, "#565656", 0.8), _mix(accent, "#565656", 0.8), accent, FONT, _vivid(accent),
esc(callout_title), markdown(callout_text, lang, accent).replace("margin:0 0 14px", "margin:0 0 6px"))
)
button = ""
if button_url:
button = (
''
% (BLUE_RULE, BLUE_RULE, esc(button_url, quote=True), FONT, esc(button_label or button_url))
)
if audience == "client":
footer = esc(STRINGS[lang]["client_footer"]) + "
" + esc(STRINGS[lang]["tagline"])
footer_text = STRINGS[lang]["client_footer"] + "\n" + STRINGS[lang]["tagline"]
else:
footer = STRINGS[lang]["soc_footer"].format(blue=BLUE, bot=esc(bot))
footer_text = STRINGS[lang]["soc_footer_text"].format(bot=bot)
logo = ""
if LOGO_PNG_B64 and not LOGO_PNG_B64.startswith("__"):
logo = ('
' % LOGO_PNG_B64)
preheader = esc(_plain(callout_text + " " + description) if callout_text else _plain(description))
page = """
{title}
{preheader}
|
ACKnowledge
SECURITY OPERATIONS CENTER | SOC 24/7
|
{badge}
|
{title}
{meta}
|
{callout}{description}{button}
| {logo} |
SOC | Security Operation Center ACKnowledge
|
|
| {footer} |
|
""".format(
lang=lang, title=esc(subject), outer=OUTER, page=PAGE, header=HEADER, header_rule=HEADER_RULE,
font=FONT, body_font=BODY_FONT, blue=BLUE, blue_rule=BLUE_RULE, signature=SIGNATURE, muted=MUTED,
accent=accent, badge=esc(badge_full), meta=meta_html, preheader=preheader,
callout=callout, description=markdown(description, lang, accent), button=button,
logo=logo, footer=footer,
)
head = [("[%s] " % badge) + subject]
head.append(" · ".join([when] + ([case_ref] if case_ref else []) + ([LEVEL_LABELS[lang][severity]] if severity and template != "incident_notification" else [])))
text = ["\n".join(head)]
if callout_text:
text.append(callout_title + "\n" + markdown_text(callout_text, lang))
text.append(markdown_text(description, lang))
if button_url:
text.append((button_label + ": " if button_label else "") + button_url)
text.append("--\nSOC | Security Operation Center\nACKnowledge\nw: www.acknowledge.fr\ne: soc@acknowledge.fr")
text.append(footer_text)
full_subject = subject
if case_ref and case_ref not in subject:
full_subject = "[%s] %s" % (case_ref, subject)
return {
"subject": full_subject,
"html_body": page,
"text_body": "\n\n".join(text) + "\n",
"template": template,
"lang": lang,
"audience": audience,
"html_bytes": len(page.encode("utf-8")),
}
def main():
try:
print(json.dumps(render(_cfg(), _inputs()), ensure_ascii=False))
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
if __name__ == "__main__":
main()