Skip to content

implemented Microsoft Teams delivery channel - #557

Merged
AbdulmalikAlayande merged 2 commits into
TegoLabs:mainfrom
idrisososanwo:teams-delivery-channel
Jul 31, 2026
Merged

AbdulmalikAlayande merged 2 commits into
TegoLabs:mainfrom
idrisososanwo:teams-delivery-channel

Conversation

@idrisososanwo

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a Microsoft Teams alert channel with Adaptive Card support, webhook validation, and customizable alert templates. It also registers the channel as a built-in alert target and includes comprehensive unit tests.

Closes #311

Why?

This enables alert notifications to be delivered directly to Microsoft Teams channels using incoming webhooks, providing rich, structured notifications for operational events while ensuring webhook security and consistent alert formatting.

Does this touch secret-key handling or transaction submission?

  • Yes — see notes above
  • No

Checklist

  • Tests pass (npm test)
  • Type check passes (npx tsc --noEmit)
  • Lint passes (npm run lint)
  • Tests cover the new functionality (TDD preferred — see CONTRIBUTING.md)
  • No unnecessary dependencies added
  • Commit messages follow conventional format
  • No console.log in core logic
  • ADR added if this is a significant design decision (see docs/adr)
  • E2E sandbox tested, if this touches RPC or daemon behavior (see docs/e2e-sandbox.md)

Additional Notes

Changes Made

  • Implemented a new Teams alert channel that conforms to the AlertChannel interface.
  • Added validation to ensure webhook URLs are restricted to valid *.webhook.office.com endpoints.
  • Implemented Microsoft Teams Adaptive Card payload generation with:
    • Severity-based styling (attention, warning, good)
    • Event summary and title
    • Event details displayed using FactSet
    • Footer guidance for recipients
  • Added support for custom Handlebars templates through renderAlertTemplate("teams", event).
  • Registered the Teams channel in the built-in alert registry with lazy loading and target validation.
  • Added comprehensive TDD-based unit tests covering:
    • Webhook URL validation
    • Adaptive Card payload generation
    • Fact field rendering for different event types
    • HTTP failure and error handling
    • Built-in channel registration

Verification

  • tests/alerts/teams.test.ts: 13/13 tests passed
  • tests/alerts/builtins.test.ts: 16/16 tests passed
  • Entire test suite: 78 test files and 1,010 unit tests passed successfully.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@idrisososanwo Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added Microsoft Teams as a built-in alert delivery channel.
    • Send alerts to Teams through validated Incoming Webhook URLs.
    • Alerts are formatted as Adaptive Cards with event-specific details.
    • Supports custom Teams alert templates and clear handling of delivery errors.

Walkthrough

Adds a Microsoft Teams alert channel that validates webhook URLs, generates Adaptive Card payloads for alert events, supports custom templates, posts through fetch, and integrates with the built-in channel registry and tests.

Changes

Teams alert delivery

Layer / File(s) Summary
Adaptive Card payload and webhook validation
src/alerts/teams.ts, tests/alerts/teams.test.ts
Builds event-specific Adaptive Card titles and facts, validates Teams webhook hosts, and tests payload structure and validation failures.
Webhook request workflow
src/alerts/teams.ts, tests/alerts/teams.test.ts
Supports optional rendered templates, sends JSON POST requests with timeout handling, and reports HTTP and network errors.
Built-in channel registration and coverage
src/alerts/builtins.ts, tests/alerts/builtins.test.ts
Registers the teams channel with URL targeting and verifies delegation, target parsing, registry counts, and missing-target errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AlertDispatcher
  participant sendTeamsAlert
  participant renderAlertTemplate
  participant TeamsWebhook
  AlertDispatcher->>sendTeamsAlert: webhook URL and AlertEvent
  sendTeamsAlert->>renderAlertTemplate: render teams template
  sendTeamsAlert->>TeamsWebhook: POST JSON payload
  TeamsWebhook-->>sendTeamsAlert: HTTP response
  sendTeamsAlert-->>AlertDispatcher: completion or error
Loading

Possibly related PRs

Suggested reviewers: abdulmalikalayande

Poem

A rabbit hops where alerts now gleam,
Adaptive cards cross Teams’ stream.
Webhooks checked, then messages fly,
Six bright channels reach the sky.
“Nibble-tested!” the bunny sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the core change: adding a Microsoft Teams delivery channel.
Description check ✅ Passed The description clearly describes the Teams channel, validation, templates, registration, and tests.
Linked Issues check ✅ Passed The changes satisfy #311 with a Teams sender, Adaptive Card payloads, webhook validation, status errors, built-in registration, and tests.
Out of Scope Changes check ✅ Passed The summarized changes stay focused on the Teams alert channel and its tests, with no clear unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/alerts/teams.ts`:
- Around line 171-195: Update validateWebhookUrl to require the parsed URL to
use the https: scheme and to validate the hostname as a genuine
*.webhook.office.com endpoint, using an exact hostname or strict suffix check
rather than includes. Preserve the existing required, parse-error, and
invalid-URL error behavior.
- Around line 249-262: Update the error-detail extraction in the Teams webhook
response handling to support both a string-valued `body.error` and the existing
nested `body.error.message` object shape, preserving `body.message` precedence
and including whichever diagnostic text is available in the thrown error.
- Around line 26-41: Update buildTitle so resource_alert events receive a
resource-specific title rather than the TTL label, while preserving the existing
TTL title for threshold_crossed events and the current resolved/state_changed
behavior.

In `@tests/alerts/teams.test.ts`:
- Around line 58-77: Extend the “Webhook URL validation” tests with a
`validateWebhookUrl` regression case through `sendTeamsAlert` using the
lookalike host `x.webhook.office.com.evil.com`, and assert it rejects without
calling `mockFetch`. Add an `alert_resolved` fixture to the payload-structure
tests and verify `buildAdaptiveCard` handles it without crashing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a226aa7b-1b80-4986-9535-25660674d656

📥 Commits

Reviewing files that changed from the base of the PR and between 35d9237 and efc2d09.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • src/alerts/builtins.ts
  • src/alerts/teams.ts
  • tests/alerts/builtins.test.ts
  • tests/alerts/teams.test.ts
📜 Review details
🔇 Additional comments (5)
src/alerts/teams.ts (2)

1-10: LGTM!

Also applies to: 197-248, 263-265


64-130: 🩺 Stability & Availability

No change needed for alert_resolved card rendering. alert_resolved events are TTLAlertEvent, and both entry and threshold fields are present in that shape and populated by buildAlertEvent, so the generic fact branch is safe.

			> Likely an incorrect or invalid review comment.
tests/alerts/teams.test.ts (1)

1-56: LGTM!

Also applies to: 79-191, 193-209

src/alerts/builtins.ts (1)

67-78: LGTM!

tests/alerts/builtins.test.ts (1)

10-10: LGTM!

Also applies to: 32-34, 47-55, 70-70, 100-104, 121-123

Comment thread src/alerts/teams.ts
Comment on lines +26 to +41
function buildTitle(event: AlertEvent): string {
const icon = severityEmoji(event);
const contractDisplay = event.contractName ?? event.contractId;

if (event.type === "alert_resolved") {
return `${icon} Alert Resolved — ${contractDisplay}`;
}

if (event.type === "state_changed") {
const diffLabel = event.diff.diffType.charAt(0).toUpperCase() + event.diff.diffType.slice(1);
return `${icon} State ${diffLabel} — ${contractDisplay}`;
}

const level = event.severity === "critical" ? "CRITICAL" : "Warning";
return `${icon} TTL ${level} — ${contractDisplay}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

buildTitle mislabels resource_alert events as "TTL".

The default branch (Lines 39-40) is reached by both threshold_crossed and resource_alert events, always emitting TTL ${level}. A CPU/Memory resource alert (confirmed by the resource_alert test fixture in tests/alerts/teams.test.ts Lines 134-148, which has no entry/threshold fields) will render a title like ⚠️ TTL Warning — resource-contract, which is misleading for on-call responders triaging the alert.

🐛 Proposed fix
     if (event.type === "state_changed") {
         const diffLabel = event.diff.diffType.charAt(0).toUpperCase() + event.diff.diffType.slice(1);
         return `${icon} State ${diffLabel} — ${contractDisplay}`;
     }
 
+    if (event.type === "resource_alert") {
+        const resourceLabel = event.resource.type === "cpu" ? "CPU" : "Memory";
+        const level = event.severity === "critical" ? "CRITICAL" : "Warning";
+        return `${icon} ${resourceLabel} ${level} — ${contractDisplay}`;
+    }
+
     const level = event.severity === "critical" ? "CRITICAL" : "Warning";
     return `${icon} TTL ${level} — ${contractDisplay}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function buildTitle(event: AlertEvent): string {
const icon = severityEmoji(event);
const contractDisplay = event.contractName ?? event.contractId;
if (event.type === "alert_resolved") {
return `${icon} Alert Resolved — ${contractDisplay}`;
}
if (event.type === "state_changed") {
const diffLabel = event.diff.diffType.charAt(0).toUpperCase() + event.diff.diffType.slice(1);
return `${icon} State ${diffLabel} — ${contractDisplay}`;
}
const level = event.severity === "critical" ? "CRITICAL" : "Warning";
return `${icon} TTL ${level} — ${contractDisplay}`;
}
function buildTitle(event: AlertEvent): string {
const icon = severityEmoji(event);
const contractDisplay = event.contractName ?? event.contractId;
if (event.type === "alert_resolved") {
return `${icon} Alert Resolved — ${contractDisplay}`;
}
if (event.type === "state_changed") {
const diffLabel = event.diff.diffType.charAt(0).toUpperCase() + event.diff.diffType.slice(1);
return `${icon} State ${diffLabel} — ${contractDisplay}`;
}
if (event.type === "resource_alert") {
const resourceLabel = event.resource.type === "cpu" ? "CPU" : "Memory";
const level = event.severity === "critical" ? "CRITICAL" : "Warning";
return `${icon} ${resourceLabel} ${level} — ${contractDisplay}`;
}
const level = event.severity === "critical" ? "CRITICAL" : "Warning";
return `${icon} TTL ${level} — ${contractDisplay}`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/alerts/teams.ts` around lines 26 - 41, Update buildTitle so
resource_alert events receive a resource-specific title rather than the TTL
label, while preserving the existing TTL title for threshold_crossed events and
the current resolved/state_changed behavior.

Comment thread src/alerts/teams.ts
Comment on lines +171 to +195
function validateWebhookUrl(webhookUrl: string): void {
if (!webhookUrl) {
throw new Error(
"Teams webhook URL is required. " +
"Pass the full URL from your Microsoft Teams channel's Incoming Webhook settings.",
);
}

let parsed: URL;
try {
parsed = new URL(webhookUrl);
} catch {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}

if (!parsed.hostname.includes("webhook.office.com")) {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Hostname validation is bypassable — substring match instead of suffix match; no scheme check.

parsed.hostname.includes("webhook.office.com") accepts any hostname containing that substring anywhere, e.g. x.webhook.office.com.attacker.com, which is not a real Microsoft Teams endpoint. The PR objective requires validating hostnames as genuine *.webhook.office.com endpoints; a substring check doesn't enforce that. There's also no check that the scheme is https:, despite the error message advertising https://....

🛡️ Proposed fix
-    if (!parsed.hostname.includes("webhook.office.com")) {
+    const hostname = parsed.hostname.toLowerCase();
+    const isTeamsHost = hostname === "webhook.office.com" || hostname.endsWith(".webhook.office.com");
+    if (parsed.protocol !== "https:" || !isTeamsHost) {
         throw new Error(
             `Invalid Teams webhook URL: "${webhookUrl}". ` +
             "Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
         );
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function validateWebhookUrl(webhookUrl: string): void {
if (!webhookUrl) {
throw new Error(
"Teams webhook URL is required. " +
"Pass the full URL from your Microsoft Teams channel's Incoming Webhook settings.",
);
}
let parsed: URL;
try {
parsed = new URL(webhookUrl);
} catch {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}
if (!parsed.hostname.includes("webhook.office.com")) {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}
}
function validateWebhookUrl(webhookUrl: string): void {
if (!webhookUrl) {
throw new Error(
"Teams webhook URL is required. " +
"Pass the full URL from your Microsoft Teams channel's Incoming Webhook settings.",
);
}
let parsed: URL;
try {
parsed = new URL(webhookUrl);
} catch {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}
const hostname = parsed.hostname.toLowerCase();
const isTeamsHost = hostname === "webhook.office.com" || hostname.endsWith(".webhook.office.com");
if (parsed.protocol !== "https:" || !isTeamsHost) {
throw new Error(
`Invalid Teams webhook URL: "${webhookUrl}". ` +
"Expected a URL like https://<tenant>.webhook.office.com/webhookb2/...",
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/alerts/teams.ts` around lines 171 - 195, Update validateWebhookUrl to
require the parsed URL to use the https: scheme and to validate the hostname as
a genuine *.webhook.office.com endpoint, using an exact hostname or strict
suffix check rather than includes. Preserve the existing required, parse-error,
and invalid-URL error behavior.

Comment thread src/alerts/teams.ts
Comment on lines +249 to +262
if (!response.ok) {
let detail = "";
try {
const body = (await response.json()) as { message?: string; error?: { message?: string } };
if (body.message) {
detail = `: ${body.message}`;
} else if (body.error?.message) {
detail = `: ${body.error.message}`;
}
} catch {
// body not JSON — ignore
}
throw new Error(`Teams webhook request failed: HTTP ${response.status}${detail}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Error detail extraction misses the common { error: "<string>" } shape.

body.error?.message only handles a nested { error: { message } } object. Many webhook/API error bodies (including the test helper's { error: message } at tests/alerts/teams.test.ts Lines 41-46) use a plain string for error, so detail silently stays empty and useful diagnostic text is dropped from the thrown error.

🔧 Proposed fix
-            const body = (await response.json()) as { message?: string; error?: { message?: string } };
+            const body = (await response.json()) as { message?: string; error?: string | { message?: string } };
             if (body.message) {
                 detail = `: ${body.message}`;
+            } else if (typeof body.error === "string") {
+                detail = `: ${body.error}`;
             } else if (body.error?.message) {
                 detail = `: ${body.error.message}`;
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response.ok) {
let detail = "";
try {
const body = (await response.json()) as { message?: string; error?: { message?: string } };
if (body.message) {
detail = `: ${body.message}`;
} else if (body.error?.message) {
detail = `: ${body.error.message}`;
}
} catch {
// body not JSON — ignore
}
throw new Error(`Teams webhook request failed: HTTP ${response.status}${detail}`);
}
if (!response.ok) {
let detail = "";
try {
const body = (await response.json()) as { message?: string; error?: string | { message?: string } };
if (body.message) {
detail = `: ${body.message}`;
} else if (typeof body.error === "string") {
detail = `: ${body.error}`;
} else if (body.error?.message) {
detail = `: ${body.error.message}`;
}
} catch {
// body not JSON — ignore
}
throw new Error(`Teams webhook request failed: HTTP ${response.status}${detail}`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/alerts/teams.ts` around lines 249 - 262, Update the error-detail
extraction in the Teams webhook response handling to support both a
string-valued `body.error` and the existing nested `body.error.message` object
shape, preserving `body.message` precedence and including whichever diagnostic
text is available in the thrown error.

Comment on lines +58 to +77
describe("Webhook URL validation", () => {
it("throws a clear error when URL is empty", async () => {
await expect(sendTeamsAlert("", makeAlertEvent())).rejects.toThrow(/Teams webhook URL is required/);
});

it("throws when URL is not a valid URL string", async () => {
await expect(sendTeamsAlert("invalid-url", makeAlertEvent())).rejects.toThrow(/Invalid Teams webhook URL/);
});

it("throws when hostname does not match Teams webhook domain", async () => {
await expect(sendTeamsAlert("https://discord.com/api/webhooks/123/abc", makeAlertEvent())).rejects.toThrow(
/Invalid Teams webhook URL/,
);
});

it("does not call fetch when URL validation fails", async () => {
await expect(sendTeamsAlert("https://example.com/hook", makeAlertEvent())).rejects.toThrow();
expect(mockFetch).not.toHaveBeenCalled();
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for the hostname bypass and alert_resolved coverage.

Once validateWebhookUrl is tightened (see src/alerts/teams.ts Lines 171-195), add a case here asserting a lookalike host like https://x.webhook.office.com.evil.com/webhookb2/... is rejected. Also consider adding an alert_resolved fixture to the payload-structure suite to guard against the potential crash flagged in buildAdaptiveCard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/alerts/teams.test.ts` around lines 58 - 77, Extend the “Webhook URL
validation” tests with a `validateWebhookUrl` regression case through
`sendTeamsAlert` using the lookalike host `x.webhook.office.com.evil.com`, and
assert it rejects without calling `mockFetch`. Add an `alert_resolved` fixture
to the payload-structure tests and verify `buildAdaptiveCard` handles it without
crashing.

@gitguardian

gitguardian Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

AbdulmalikAlayande added a commit that referenced this pull request Jul 31, 2026
… + completed)

Adds src/alerts/teams.ts (Adaptive Card payload, severity coloring per
event type) and its tests, per #311.

Two things fixed before merging:
- CodeRabbit correctly flagged validateWebhookUrl()'s hostname check:
  `hostname.includes("webhook.office.com")` accepts any hostname that
  merely contains that substring, e.g. an attacker-controlled
  "x.webhook.office.com.evil.com" would pass, silently sending real
  alert content (contract IDs, TTL data) to an attacker-controlled
  server. Changed to an exact/suffix match on the real hostname, plus
  an explicit https-only check. Added regression tests for both.
- The PR never registered the channel in builtins.ts, so `--type teams`
  was unreachable from the CLI. Completed the registration
  (targetOption: "url", matching webhook/discord's pattern) and the
  matching builtins.test.ts coverage.
@AbdulmalikAlayande
AbdulmalikAlayande merged commit f902655 into TegoLabs:main Jul 31, 2026
2 checks passed
@AbdulmalikAlayande

Copy link
Copy Markdown
Collaborator

Merged as cc5f4c0 on main — but with two fixes first, want to be upfront about both:

  1. Real security issue (CodeRabbit caught it, confirmed and fixed): validateWebhookUrl()'s hostname check used hostname.includes("webhook.office.com"), which accepts any hostname merely containing that substring — e.g. x.webhook.office.com.evil.com would pass validation, meaning real alert content (contract IDs, TTL data) could get silently sent to an attacker-controlled server. Changed to an exact/suffix hostname match plus an explicit https-only check, and added two regression tests for it.
  2. The channel was never registered in builtins.ts, so --type teams was unreachable from the CLI despite the sender being fully implemented. Completed that registration (targetOption: "url") and the matching builtins.test.ts coverage.

The Adaptive Card payload building and severity-color logic itself is well done. Verified locally: lint, typecheck, full suite (1146/1146), build, and audit all clean. Closing #311 as shipped.

AbdulmalikAlayande added a commit that referenced this pull request Jul 31, 2026
…rimmed to scope)

Adds src/alerts/googlechat.ts and registers it in builtins.ts, per #313.
Registration and the sender itself were already correct.

Trimmed from the original PR before merging: a bundled Grafana/Prometheus
observability stack (devops/grafana/*, devops/prometheus/*, docker-compose.
observability.yml, docs/observability.md) - unrelated to this issue, shared
branch lineage with several other open PRs (#594, #595, #596) that also
carry the identical bundle. Left tests/docker/docker-compose.test.ts
untouched by reverting to main's version.

Updated tests/alerts/builtins.test.ts for the 10th channel (the PR's
branch predated matrix/teams/email, so its own copy of this file didn't
know about them).

Note for a follow-up: src/alerts/discord.ts has the same hostname-
validation weakness fixed in #557 (`hostname.includes("discord")`,
even looser than Teams's check) - pre-existing, not part of this PR,
flagging separately.
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
… + completed)

Adds src/alerts/teams.ts (Adaptive Card payload, severity coloring per
event type) and its tests, per #311.

Two things fixed before merging:
- CodeRabbit correctly flagged validateWebhookUrl()'s hostname check:
  `hostname.includes("webhook.office.com")` accepts any hostname that
  merely contains that substring, e.g. an attacker-controlled
  "x.webhook.office.com.evil.com" would pass, silently sending real
  alert content (contract IDs, TTL data) to an attacker-controlled
  server. Changed to an exact/suffix match on the real hostname, plus
  an explicit https-only check. Added regression tests for both.
- The PR never registered the channel in builtins.ts, so `--type teams`
  was unreachable from the CLI. Completed the registration
  (targetOption: "url", matching webhook/discord's pattern) and the
  matching builtins.test.ts coverage.
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
…rimmed to scope)

Adds src/alerts/googlechat.ts and registers it in builtins.ts, per #313.
Registration and the sender itself were already correct.

Trimmed from the original PR before merging: a bundled Grafana/Prometheus
observability stack (devops/grafana/*, devops/prometheus/*, docker-compose.
observability.yml, docs/observability.md) - unrelated to this issue, shared
branch lineage with several other open PRs (#594, #595, #596) that also
carry the identical bundle. Left tests/docker/docker-compose.test.ts
untouched by reverting to main's version.

Updated tests/alerts/builtins.test.ts for the 10th channel (the PR's
branch predated matrix/teams/email, so its own copy of this file didn't
know about them).

Note for a follow-up: src/alerts/discord.ts has the same hostname-
validation weakness fixed in #557 (`hostname.includes("discord")`,
even looser than Teams's check) - pre-existing, not part of this PR,
flagging separately.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(alerts): implement Microsoft Teams delivery channel

2 participants