snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
0
zerver/webhooks/thinkst/__init__.py
Normal file
0
zerver/webhooks/thinkst/__init__.py
Normal file
31
zerver/webhooks/thinkst/doc.md
Normal file
31
zerver/webhooks/thinkst/doc.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Zulip Thinkst integration
|
||||
|
||||
Get your Thinkst Canary and Canarytoken alerts in Zulip!
|
||||
|
||||
This integration works with Canarytokens from Thinkst's paid product.
|
||||
For [canarytokens.org][canarytokens], see the
|
||||
[Canarytokens](/integrations/canarytoken) integration!
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. {!create-an-incoming-webhook.md!}
|
||||
|
||||
1. {!generate-webhook-url-basic.md!}
|
||||
|
||||
1. Go to your Thinkst Canary settings, and click on **Webhooks** in the
|
||||
left sidebar. Select the **Generic** tab, and click **Add Generic
|
||||
Webhook**.
|
||||
|
||||
1. Enter the generated URL above. Click **Save**.
|
||||
|
||||
{end_tabs}
|
||||
|
||||
{!congrats.md!}
|
||||
|
||||

|
||||
|
||||
### Related documentation
|
||||
|
||||
{!webhooks-url-specification.md!}
|
||||
|
||||
[canarytokens]: https://canarytokens.org
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"ReverseDNS": "attacker.local",
|
||||
"CanaryName": "foo-bar",
|
||||
"Description": "Consolidated Network Port Scan",
|
||||
"CanaryPort": "443",
|
||||
"Timestamp": "2020-07-20 16:18:40 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "A portscan has been done on several of your canaries by the host 1.1.1.1.",
|
||||
"IncidentHash": "3f25fec9e18c7673dcc468800b7af0a6",
|
||||
"CanaryLocation": "dining room",
|
||||
"SourceIP": "1.1.1.1",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Incident",
|
||||
"Consolidated Network Port Scan"
|
||||
],
|
||||
[
|
||||
"Source",
|
||||
"1.1.1.1"
|
||||
],
|
||||
[
|
||||
"Targets",
|
||||
"1.0.0.1, 1.0.0.2"
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 8 incidents from 1.0.0.1 previously."
|
||||
]
|
||||
],
|
||||
"CanaryID": "00000000aa8a310e"
|
||||
}
|
||||
27
zerver/webhooks/thinkst/fixtures/canary_dummy.json
Normal file
27
zerver/webhooks/thinkst/fixtures/canary_dummy.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"ReverseDNS": "attacker-ip.local",
|
||||
"CanaryName": "0000000testnode",
|
||||
"Description": "Dummy Incident",
|
||||
"Timestamp": "2020-06-09 13:59:38 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "This is a dummy incident.",
|
||||
"IncidentHash": "aa875f255f94e3ffe40dc85cf1a8b1e0",
|
||||
"CanaryPort": 8080,
|
||||
"SourceIP": "2.2.2.2",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Field1",
|
||||
"VALUE1"
|
||||
],
|
||||
[
|
||||
"Field2",
|
||||
"VALUE2"
|
||||
],
|
||||
[
|
||||
"Field3",
|
||||
"VALUE3"
|
||||
]
|
||||
],
|
||||
"CanaryID": "0000000testnode"
|
||||
}
|
||||
32
zerver/webhooks/thinkst/fixtures/canary_file_access.json
Normal file
32
zerver/webhooks/thinkst/fixtures/canary_file_access.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"ReverseDNS": "",
|
||||
"CanaryName": "bar-foo",
|
||||
"Description": "Shared File (Re)Opened",
|
||||
"CanaryPort": 445,
|
||||
"Timestamp": "2020-07-20 16:27:20 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "Shared File (Re)Opened has been detected against one of your Canaries (bar-foo) at 1.1.1.1.",
|
||||
"IncidentHash": "a7bb317ba2072415462233cef3bc615a",
|
||||
"CanaryLocation": "dining room",
|
||||
"SourceIP": "1.1.1.1",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"User",
|
||||
"guest"
|
||||
],
|
||||
[
|
||||
"Filename",
|
||||
"secret/bar.doc"
|
||||
],
|
||||
[
|
||||
"Additional Information",
|
||||
"This file 'secret/bar.doc' was previously opened by the host 'zulip-dev' on 2020-07-20 16:18:56.\n\nIt was also opened 2 times before by the same host, on 2020-06-10 14:33:50, 2020-06-18 19:02:33."
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 20 incidents from 1.1.1.1 previously."
|
||||
]
|
||||
],
|
||||
"CanaryID": "00000000aa8a310e"
|
||||
}
|
||||
20
zerver/webhooks/thinkst/fixtures/canary_host_port_scan.json
Normal file
20
zerver/webhooks/thinkst/fixtures/canary_host_port_scan.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"ReverseDNS": "attacker.local",
|
||||
"CanaryName": "foo-bar",
|
||||
"Description": "Host Port Scan",
|
||||
"CanaryPort": "1723",
|
||||
"Timestamp": "2020-07-20 16:26:30 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "Host Port Scan has been detected against one of your Canaries (foo-bar) at 1.1.1.1.",
|
||||
"IncidentHash": "9060473d2da98afe494adad648495620",
|
||||
"CanaryLocation": "dining room",
|
||||
"SourceIP": "1.1.1.1",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Partial Ports",
|
||||
"443, 554, 80, 1723, 22"
|
||||
]
|
||||
],
|
||||
"CanaryID": "00024808d6c2f7e5"
|
||||
}
|
||||
32
zerver/webhooks/thinkst/fixtures/canary_http_login.json
Normal file
32
zerver/webhooks/thinkst/fixtures/canary_http_login.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"ReverseDNS": "",
|
||||
"CanaryName": "foo-bar",
|
||||
"Description": "HTTP Login Attempt",
|
||||
"CanaryPort": 80,
|
||||
"Timestamp": "2020-07-20 14:55:45 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "HTTP Login Attempt has been detected against one of your Canaries (foo-bar) at 1.1.1.1.",
|
||||
"IncidentHash": "1ef86f5cf4090a5252c6f453c5cd46bd",
|
||||
"CanaryLocation": "dining room",
|
||||
"SourceIP": "1.1.1.1",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Username",
|
||||
"regular"
|
||||
],
|
||||
[
|
||||
"Password",
|
||||
"*******"
|
||||
],
|
||||
[
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15"
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 14 incidents from 1.1.1.1 previously."
|
||||
]
|
||||
],
|
||||
"CanaryID": "00000000d9febd04"
|
||||
}
|
||||
28
zerver/webhooks/thinkst/fixtures/canary_ssh_login.json
Normal file
28
zerver/webhooks/thinkst/fixtures/canary_ssh_login.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"ReverseDNS": "attacker.local",
|
||||
"CanaryName": "foo-bar",
|
||||
"Description": "SSH Login Attempt",
|
||||
"CanaryPort": 22,
|
||||
"Timestamp": "2020-07-20 16:26:22 (UTC)",
|
||||
"CanaryIP": "1.1.1.1",
|
||||
"AlertType": "CanaryIncident",
|
||||
"Intro": "SSH Login Attempt has been detected against one of your Canaries (foo-bar) at 1.1.1.1.",
|
||||
"IncidentHash": "4ef51a936d05e0a6b4846378c8c38d2c",
|
||||
"CanaryLocation": "dining room",
|
||||
"SourceIP": "1.1.1.1",
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Username",
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"Key",
|
||||
"*******"
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 9 incidents from 1.1.1.1 previously."
|
||||
]
|
||||
],
|
||||
"CanaryID": "00024808d6c2f7e5"
|
||||
}
|
||||
40
zerver/webhooks/thinkst/fixtures/canarytoken_msword.json
Normal file
40
zerver/webhooks/thinkst/fixtures/canarytoken_msword.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"Token": "dbwx4d68flwh2u5zku56nogu6",
|
||||
"Intro": "A MS Word .docx Document Canarytoken has been triggered over doc-msword by the source IP 1.1.1.1.",
|
||||
"Description": "MS Word .docx Document",
|
||||
"Triggered": 5,
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Accept",
|
||||
"*/*"
|
||||
],
|
||||
[
|
||||
"Accept-Encoding",
|
||||
"gzip, deflate"
|
||||
],
|
||||
[
|
||||
"Accept-Language",
|
||||
"en-gb"
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 21 incidents from 1.1.1.1 previously."
|
||||
],
|
||||
[
|
||||
"Connection",
|
||||
"keep-alive"
|
||||
],
|
||||
[
|
||||
"Dst Port",
|
||||
80
|
||||
],
|
||||
[
|
||||
"User-Agent",
|
||||
"Mozilla/4.0 (compatible; ms-office; MSOffice 16)"
|
||||
]
|
||||
],
|
||||
"Timestamp": "2020-07-20 14:40:15 (UTC)",
|
||||
"Reminder": "test document",
|
||||
"IncidentHash": "db6f9b5528c6c6c385cb3bb63f5949c8",
|
||||
"AlertType": "CanarytokenIncident"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"Token": "ew5n8gqtb82m4uegrttarn2zu",
|
||||
"Intro": "A Remote Web Image Canarytoken has been triggered over web-image by the source IP 1.1.1.1.",
|
||||
"Description": "Remote Web Image",
|
||||
"Triggered": 37,
|
||||
"AdditionalDetails": [
|
||||
[
|
||||
"Accept",
|
||||
"image/webp,*/*"
|
||||
],
|
||||
[
|
||||
"Accept-Encoding",
|
||||
"gzip, deflate"
|
||||
],
|
||||
[
|
||||
"Accept-Language",
|
||||
"en-US,en;q=0.5"
|
||||
],
|
||||
[
|
||||
"Background Context",
|
||||
"You have had 12 incidents from 1.1.1.1 previously."
|
||||
],
|
||||
[
|
||||
"Cache-Control",
|
||||
"max-age=0"
|
||||
],
|
||||
[
|
||||
"Connection",
|
||||
"keep-alive"
|
||||
],
|
||||
[
|
||||
"Dnt",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"Dst Port",
|
||||
80
|
||||
],
|
||||
[
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0"
|
||||
]
|
||||
],
|
||||
"Timestamp": "2020-07-20 16:33:12 (UTC)",
|
||||
"Reminder": "test image",
|
||||
"IncidentHash": "533395067f6d655cd19384bc6991cc0f",
|
||||
"AlertType": "CanarytokenIncident"
|
||||
}
|
||||
256
zerver/webhooks/thinkst/tests.py
Normal file
256
zerver/webhooks/thinkst/tests.py
Normal file
@@ -0,0 +1,256 @@
|
||||
from zerver.lib.test_classes import WebhookTestCase
|
||||
|
||||
|
||||
class ThinkstHookTests(WebhookTestCase):
|
||||
def test_canary_dummy(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *0000000testnode* has been triggered!**\n\n"
|
||||
"This is a dummy incident.\n\n"
|
||||
"**Incident ID:** `aa875f255f94e3ffe40dc85cf1a8b1e0`\n"
|
||||
"**Kind:** Dummy Incident\n"
|
||||
"**Timestamp:** <time:2020-06-09T13:59:38+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Source IP:** `2.2.2.2`\n"
|
||||
"**Reverse DNS:** `attacker-ip.local`\n"
|
||||
"**Field1:** VALUE1\n"
|
||||
"**Field2:** VALUE2\n"
|
||||
"**Field3:** VALUE3"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_dummy",
|
||||
"canary alert - 0000000testnode",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_consolidated_port_scan(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *foo-bar* has been triggered!**\n\n"
|
||||
"A portscan has been done on several of your canaries by the host "
|
||||
"1.1.1.1.\n\n"
|
||||
"**Incident ID:** `3f25fec9e18c7673dcc468800b7af0a6`\n"
|
||||
"**Kind:** Consolidated Network Port Scan\n"
|
||||
"**Timestamp:** <time:2020-07-20T16:18:40+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Canary location:** dining room\n"
|
||||
"**Source IP:** `1.1.1.1`\n"
|
||||
"**Reverse DNS:** `attacker.local`\n"
|
||||
"**Incident:** Consolidated Network Port Scan\n"
|
||||
"**Source:** 1.1.1.1\n"
|
||||
"**Targets:** 1.0.0.1, 1.0.0.2\n"
|
||||
"**Background Context:** You have had 8 incidents from 1.0.0.1 previously."
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_consolidated_port_scan",
|
||||
"canary alert - foo-bar",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_file_access(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *bar-foo* has been triggered!**\n\n"
|
||||
"Shared File (Re)Opened has been detected against one of your Canaries "
|
||||
"(bar-foo) at 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `a7bb317ba2072415462233cef3bc615a`\n"
|
||||
"**Kind:** Shared File (Re)Opened\n"
|
||||
"**Timestamp:** <time:2020-07-20T16:27:20+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Canary location:** dining room\n"
|
||||
"**Source IP:** `1.1.1.1`\n"
|
||||
"**User:** guest\n"
|
||||
"**Filename:** secret/bar.doc\n"
|
||||
"**Additional Information:** This file 'secret/bar.doc' was "
|
||||
"previously opened by the host 'zulip-dev' on 2020-07-20 16:18:56.\n\n"
|
||||
"It was also opened 2 times before by the same host, on 2020-06-10 "
|
||||
"14:33:50, 2020-06-18 19:02:33.\n"
|
||||
"**Background Context:** You have had 20 incidents from 1.1.1.1 previously."
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_file_access",
|
||||
"canary alert - bar-foo",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_host_port_scan(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *foo-bar* has been triggered!**\n\n"
|
||||
"Host Port Scan has been detected against one of your Canaries "
|
||||
"(foo-bar) at 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `9060473d2da98afe494adad648495620`\n"
|
||||
"**Kind:** Host Port Scan\n"
|
||||
"**Timestamp:** <time:2020-07-20T16:26:30+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Canary location:** dining room\n"
|
||||
"**Source IP:** `1.1.1.1`\n"
|
||||
"**Reverse DNS:** `attacker.local`\n"
|
||||
"**Partial Ports:** 443, 554, 80, 1723, 22"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_host_port_scan",
|
||||
"canary alert - foo-bar",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_http_login(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *foo-bar* has been triggered!**\n\n"
|
||||
"HTTP Login Attempt has been detected against one of your Canaries "
|
||||
"(foo-bar) at 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `1ef86f5cf4090a5252c6f453c5cd46bd`\n"
|
||||
"**Kind:** HTTP Login Attempt\n"
|
||||
"**Timestamp:** <time:2020-07-20T14:55:45+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Canary location:** dining room\n"
|
||||
"**Source IP:** `1.1.1.1`\n"
|
||||
"**Username:** regular\n"
|
||||
"**Password:** `*******`\n"
|
||||
"**User-Agent:** Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15\n"
|
||||
"**Background Context:** You have had 14 incidents from 1.1.1.1 previously."
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_http_login",
|
||||
"canary alert - foo-bar",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_ssh_login(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canary *foo-bar* has been triggered!**\n\n"
|
||||
"SSH Login Attempt has been detected against one of your Canaries "
|
||||
"(foo-bar) at 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `4ef51a936d05e0a6b4846378c8c38d2c`\n"
|
||||
"**Kind:** SSH Login Attempt\n"
|
||||
"**Timestamp:** <time:2020-07-20T16:26:22+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Canary location:** dining room\n"
|
||||
"**Source IP:** `1.1.1.1`\n"
|
||||
"**Reverse DNS:** `attacker.local`\n"
|
||||
"**Username:** foo\n"
|
||||
"**Key:** `*******`\n"
|
||||
"**Background Context:** You have had 9 incidents from 1.1.1.1 previously."
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_ssh_login",
|
||||
"canary alert - foo-bar",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canary_with_specific_topic(self) -> None:
|
||||
self.url = self.build_webhook_url(topic="foo")
|
||||
expected_message = (
|
||||
"**:alert: Canary *0000000testnode* has been triggered!**\n\n"
|
||||
"This is a dummy incident.\n\n"
|
||||
"**Incident ID:** `aa875f255f94e3ffe40dc85cf1a8b1e0`\n"
|
||||
"**Kind:** Dummy Incident\n"
|
||||
"**Timestamp:** <time:2020-06-09T13:59:38+00:00>\n"
|
||||
"**Canary IP:** `1.1.1.1`\n"
|
||||
"**Source IP:** `2.2.2.2`\n"
|
||||
"**Reverse DNS:** `attacker-ip.local`\n"
|
||||
"**Field1:** VALUE1\n"
|
||||
"**Field2:** VALUE2\n"
|
||||
"**Field3:** VALUE3"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canary_dummy",
|
||||
"foo",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canarytoken_msword(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canarytoken *test document* has been triggered!**\n\n"
|
||||
"A MS Word .docx Document Canarytoken has been triggered over doc-msword "
|
||||
"by the source IP 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `db6f9b5528c6c6c385cb3bb63f5949c8`\n"
|
||||
"**Token:** `dbwx4d68flwh2u5zku56nogu6`\n"
|
||||
"**Kind:** MS Word .docx Document\n"
|
||||
"**Timestamp:** <time:2020-07-20T14:40:15+00:00>\n"
|
||||
"**Triggered:** 5 times\n"
|
||||
"**Accept:** `*/*`\n"
|
||||
"**Accept-Encoding:** gzip, deflate\n"
|
||||
"**Accept-Language:** en-gb\n"
|
||||
"**Background Context:** You have had 21 incidents from 1.1.1.1 "
|
||||
"previously.\n"
|
||||
"**Connection:** keep-alive\n"
|
||||
"**Dst Port:** 80\n"
|
||||
"**User-Agent:** Mozilla/4.0 (compatible; ms-office; MSOffice 16)"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canarytoken_msword",
|
||||
"canarytoken alert - test document",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canarytoken_remote_image(self) -> None:
|
||||
expected_message = (
|
||||
"**:alert: Canarytoken *test image* has been triggered!**\n\n"
|
||||
"A Remote Web Image Canarytoken has been triggered over web-image by the "
|
||||
"source IP 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `533395067f6d655cd19384bc6991cc0f`\n"
|
||||
"**Token:** `ew5n8gqtb82m4uegrttarn2zu`\n"
|
||||
"**Kind:** Remote Web Image\n"
|
||||
"**Timestamp:** <time:2020-07-20T16:33:12+00:00>\n"
|
||||
"**Triggered:** 37 times\n"
|
||||
"**Accept:** `image/webp,*/*`\n"
|
||||
"**Accept-Encoding:** gzip, deflate\n"
|
||||
"**Accept-Language:** en-US,en;q=0.5\n"
|
||||
"**Background Context:** You have had 12 incidents from 1.1.1.1 "
|
||||
"previously.\n"
|
||||
"**Cache-Control:** max-age=0\n"
|
||||
"**Connection:** keep-alive\n"
|
||||
"**Dnt:** 1\n"
|
||||
"**Dst Port:** 80\n"
|
||||
"**User-Agent:** Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 "
|
||||
"Firefox/78.0"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canarytoken_remote_image",
|
||||
"canarytoken alert - test image",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_canarytoken_with_specific_topic(self) -> None:
|
||||
self.url = self.build_webhook_url(topic="foo")
|
||||
expected_message = (
|
||||
"**:alert: Canarytoken *test document* has been triggered!**\n\n"
|
||||
"A MS Word .docx Document Canarytoken has been triggered over doc-msword "
|
||||
"by the source IP 1.1.1.1.\n\n"
|
||||
"**Incident ID:** `db6f9b5528c6c6c385cb3bb63f5949c8`\n"
|
||||
"**Token:** `dbwx4d68flwh2u5zku56nogu6`\n"
|
||||
"**Kind:** MS Word .docx Document\n"
|
||||
"**Timestamp:** <time:2020-07-20T14:40:15+00:00>\n"
|
||||
"**Triggered:** 5 times\n"
|
||||
"**Accept:** `*/*`\n"
|
||||
"**Accept-Encoding:** gzip, deflate\n"
|
||||
"**Accept-Language:** en-gb\n"
|
||||
"**Background Context:** You have had 21 incidents from 1.1.1.1 "
|
||||
"previously.\n"
|
||||
"**Connection:** keep-alive\n"
|
||||
"**Dst Port:** 80\n"
|
||||
"**User-Agent:** Mozilla/4.0 (compatible; ms-office; MSOffice 16)"
|
||||
)
|
||||
|
||||
self.check_webhook(
|
||||
"canarytoken_msword",
|
||||
"foo",
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
152
zerver/webhooks/thinkst/view.py
Normal file
152
zerver/webhooks/thinkst/view.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Webhooks for external integrations.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from zerver.decorator import webhook_view
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.timestamp import datetime_to_global_time
|
||||
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
||||
from zerver.lib.validator import WildValue, check_int, check_string, check_union
|
||||
from zerver.lib.webhooks.common import OptionalUserSpecifiedTopicStr, check_send_webhook_message
|
||||
from zerver.models import UserProfile
|
||||
|
||||
|
||||
def is_canarytoken(message: WildValue) -> bool:
|
||||
"""
|
||||
Requests sent from Thinkst canaries are either from canarytokens or
|
||||
canaries, which can be differentiated by the value of the `AlertType`
|
||||
field.
|
||||
"""
|
||||
return message["AlertType"].tame(check_string) == "CanarytokenIncident"
|
||||
|
||||
|
||||
def canary_name(message: WildValue) -> str:
|
||||
"""
|
||||
Returns the name of the canary or canarytoken.
|
||||
"""
|
||||
if is_canarytoken(message):
|
||||
return message["Reminder"].tame(check_string)
|
||||
else:
|
||||
return message["CanaryName"].tame(check_string)
|
||||
|
||||
|
||||
def canary_kind(message: WildValue) -> str:
|
||||
"""
|
||||
Returns a description of the kind of request - canary or canarytoken.
|
||||
"""
|
||||
if is_canarytoken(message):
|
||||
return "canarytoken"
|
||||
else:
|
||||
return "canary"
|
||||
|
||||
|
||||
def source_ip_and_reverse_dns(message: WildValue) -> tuple[str | None, str | None]:
|
||||
"""
|
||||
Extract the source IP and reverse DNS information from a canary request.
|
||||
"""
|
||||
reverse_dns, source_ip = (None, None)
|
||||
|
||||
if "SourceIP" in message:
|
||||
source_ip = message["SourceIP"].tame(check_string)
|
||||
# `ReverseDNS` can sometimes exist and still be empty.
|
||||
if "ReverseDNS" in message and message["ReverseDNS"].tame(check_string) != "":
|
||||
reverse_dns = message["ReverseDNS"].tame(check_string)
|
||||
|
||||
return (source_ip, reverse_dns)
|
||||
|
||||
|
||||
def body(message: WildValue) -> str:
|
||||
"""
|
||||
Construct the response to a canary or canarytoken request.
|
||||
"""
|
||||
|
||||
title = canary_kind(message).title()
|
||||
name = canary_name(message)
|
||||
body = f"**:alert: {title} *{name}* has been triggered!**\n\n{message['Intro'].tame(check_string)}\n\n"
|
||||
|
||||
if "IncidentHash" in message:
|
||||
body += f"**Incident ID:** `{message['IncidentHash'].tame(check_string)}`\n"
|
||||
|
||||
if "Token" in message:
|
||||
body += f"**Token:** `{message['Token'].tame(check_string)}`\n"
|
||||
|
||||
if "Description" in message:
|
||||
body += f"**Kind:** {message['Description'].tame(check_string)}\n"
|
||||
|
||||
if "Timestamp" in message:
|
||||
timestamp = message["Timestamp"].tame(check_string)
|
||||
# Thinkst datetime format: https://help.canary.tools/hc/en-gb/articles/360012727777-How-do-I-configure-notifications-for-a-Generic-Webhook#h_01K71QZ806C5D49RYB6RBXSZ1B
|
||||
# "YYYY-MM-DD HH:MM:SS (UTC)"
|
||||
formatted_timestamp = timestamp.replace(" (UTC)", "Z")
|
||||
dt = datetime.fromisoformat(formatted_timestamp)
|
||||
global_time = datetime_to_global_time(dt)
|
||||
body += f"**Timestamp:** {global_time}\n"
|
||||
|
||||
if "CanaryIP" in message:
|
||||
body += f"**Canary IP:** `{message['CanaryIP'].tame(check_string)}`\n"
|
||||
|
||||
if "CanaryLocation" in message:
|
||||
body += f"**Canary location:** {message['CanaryLocation'].tame(check_string)}\n"
|
||||
|
||||
if "Triggered" in message:
|
||||
unit = "times" if message["Triggered"].tame(check_int) > 1 else "time"
|
||||
body += f"**Triggered:** {message['Triggered'].tame(check_int)} {unit}\n"
|
||||
|
||||
source_ip, reverse_dns = source_ip_and_reverse_dns(message)
|
||||
if source_ip:
|
||||
body += f"**Source IP:** `{source_ip}`\n"
|
||||
if reverse_dns:
|
||||
body += f"**Reverse DNS:** `{reverse_dns}`\n"
|
||||
|
||||
if "AdditionalDetails" in message:
|
||||
for detail in message["AdditionalDetails"]:
|
||||
key = detail[0].tame(check_string)
|
||||
value = detail[1].tame(check_union([check_string, check_int]))
|
||||
if isinstance(value, str) and "*" in value:
|
||||
# Thinkst sends passwords as a series of stars which can mess with
|
||||
# formatting, so wrap these in backticks.
|
||||
body += f"**{key}:** `{value}`\n"
|
||||
else:
|
||||
body += f"**{key}:** {value}\n"
|
||||
|
||||
return body
|
||||
|
||||
|
||||
@webhook_view("Thinkst")
|
||||
@typed_endpoint
|
||||
def api_thinkst_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
message: JsonBodyPayload[WildValue],
|
||||
user_specified_topic: OptionalUserSpecifiedTopicStr = None,
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
Construct a response to a webhook event from a Thinkst canary or canarytoken.
|
||||
|
||||
Thinkst offers public canarytokens with canarytokens.org and with their canary
|
||||
product, but the schema returned by these identically named services are
|
||||
completely different - canarytokens from canarytokens.org are handled by a
|
||||
different Zulip integration.
|
||||
|
||||
Thinkst's documentation for the schema is linked below, but in practice the JSON
|
||||
received doesn't always conform.
|
||||
|
||||
https://help.canary.tools/hc/en-gb/articles/360002426577-How-do-I-configure-notifications-for-a-Generic-Webhook-
|
||||
"""
|
||||
|
||||
response = body(message)
|
||||
|
||||
topic_name = None
|
||||
if user_specified_topic is not None:
|
||||
topic_name = user_specified_topic
|
||||
else:
|
||||
name = canary_name(message)
|
||||
kind = canary_kind(message)
|
||||
|
||||
topic_name = f"{kind} alert - {name}"
|
||||
|
||||
check_send_webhook_message(request, user_profile, topic_name, response)
|
||||
return json_success(request)
|
||||
Reference in New Issue
Block a user