Skip to content

Commit 8e3cfe5

Browse files
committed
add pixel key revoke cog
1 parent d6eefb8 commit 8e3cfe5

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

‎bot/exts/pixels.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import re
2+
import textwrap
3+
from pathlib import Path
4+
5+
from discord import Message
6+
from discord.ext import commands
7+
from pydis_core.utils.logging import get_logger
8+
9+
from bot.bot import SirRobin
10+
11+
log = get_logger(__name__)
12+
13+
PIXELS_API_KEY_REGEX = re.compile(r"owl-[A-Za-z0-9\-\_]{86}")
14+
PIXELS_JWT_REGEX = re.compile(r"e[yw][A-Za-z0-9-_]+\.(?:e[yw][A-Za-z0-9-_]+)?\.[A-Za-z0-9-_]{2,}")
15+
16+
REVOKED_MESSAGE = textwrap.dedent("""
17+
:warning: Your message contained a valid Pixels API key or access token, which has been revoked.
18+
19+
Visit [`/auth/authorize`](https://pixels.pythondiscord.com/auth/authorize) to generate a new API key.
20+
""")
21+
22+
PIXELS_API_TOKEN_PATH = Path("/var/run/secrets/pixels-api/token")
23+
24+
class Pixels(commands.Cog):
25+
"""Utilities for working with Pixels API."""
26+
27+
def __init__(self, bot: SirRobin):
28+
self.bot = bot
29+
30+
def get_request_headers(self) -> dict[str, str]:
31+
"""Get the Pixels API request headers from the projected service account file."""
32+
with open(PIXELS_API_TOKEN_PATH) as f:
33+
token = f.read().strip()
34+
35+
return {"Authorization": f"Token {token}"}
36+
37+
@commands.Cog.listener()
38+
async def on_message(self, message: Message) -> None:
39+
"""Check for exposed Pixels API keys and JWTs in messages."""
40+
if message.author.bot:
41+
return
42+
43+
found_jwts = PIXELS_JWT_REGEX.findall(message.content)
44+
found_api_keys = PIXELS_API_KEY_REGEX.findall(message.content)
45+
any_invalidated = False
46+
47+
if found_jwts or found_api_keys:
48+
headers = self.get_request_headers()
49+
log.info(f"Potential Pixels credentials in message {message.channel}/{message.id} by {message.author}.")
50+
# Call https://pixels.pythondiscord.com/api/invalidate_credential for each credential found
51+
52+
for jwt in found_jwts:
53+
async with self.bot.http_session.post(
54+
"https://pixels.pythondiscord.com/api/invalidate_credential",
55+
headers=headers,
56+
json={"jwt": jwt},
57+
) as resp:
58+
if resp.status == 200:
59+
log.info(f"Successfully invalidated Pixels JWT from {message.channel}/{message.id}")
60+
any_invalidated = True
61+
else:
62+
if resp.status in (404, 400):
63+
# False positive, the JWT is already invalid or didn't exist
64+
continue
65+
66+
log.error(f"Failed to invalidate Pixels JWT: {jwt}. Status code: {resp.status}")
67+
68+
for api_key in found_api_keys:
69+
async with self.bot.http_session.post(
70+
"https://pixels.pythondiscord.com/api/invalidate_credential",
71+
headers=headers,
72+
json={"api_key": api_key},
73+
) as resp:
74+
if resp.status == 200:
75+
log.info(f"Successfully invalidated Pixels key from {message.channel}/{message.id}")
76+
any_invalidated = True
77+
else:
78+
if resp.status in (404, 400):
79+
# False positive, the API key is already invalid or didn't exist
80+
continue
81+
82+
log.error(f"Failed to invalidate Pixels API key: {api_key}. Status code: {resp.status}")
83+
84+
if any_invalidated:
85+
await message.channel.send(REVOKED_MESSAGE)
86+
await message.delete()
87+
log.info(f"Deleted message {message.channel}/{message.id} containing exposed Pixels credentials.")
88+
89+
90+
async def setup(bot: SirRobin) -> None:
91+
"""Load the Pixels cog."""
92+
if not PIXELS_API_TOKEN_PATH.exists():
93+
log.warning(f"Pixels API token file {PIXELS_API_TOKEN_PATH} does not exist. Skipping Pixels cog setup.")
94+
return
95+
96+
await bot.add_cog(Pixels(bot))

0 commit comments

Comments
 (0)