-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
159 lines (125 loc) · 4 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { parse } from 'node-html-parser'
const SECURITY_PAGE = 'https://support.apple.com/en-us/HT201222'
const STRING = /exploited/
const serve = async (req: Request) => {
if (!req.body) return new Response('no body')
const data = await req.json()
// Something about us changed
if (data?.my_chat_member) {
const myInfo = data.my_chat_member
const chatId = myInfo.chat.id as number
const newStatus = myInfo.new_chat_member.status
const addedStatuses = ['creator', 'administrator', 'member']
const removedStatuses = ['restricted', 'left', 'kicked']
const kv = await Deno.openKv()
// We are added
if (addedStatuses.includes(newStatus)) {
console.log(`adding ${chatId} to chats`)
const chatsCache = await kv.get<string>(['chats'])
const chats = chatsCache.value ?? '[]'
const chatsParsed = JSON.parse(chats) as number[]
const newChats = [...chatsParsed, chatId]
await kv.set(['chats'], JSON.stringify(newChats))
} // We are removed
else if (removedStatuses.includes(newStatus)) {
console.log(`removing ${chatId} from chats`)
const chats = (await kv.get<string>(['chats'])).value
if (!chats) return new Response('no chats yet')
const chatsParsed = JSON.parse(chats) as number[]
const chatsFiltered = chatsParsed.filter(
(existingId) => existingId !== chatId,
)
await kv.set(['chats'], JSON.stringify(chatsFiltered))
}
}
return new Response('done')
}
const cron = async () => {
const TOKEN = Deno.env.get('TOKEN')
if (!TOKEN) {
console.error('Telegram bot token is missing')
return
}
const BOT = `https://api.telegram.org/bot${TOKEN}`
const kv = await Deno.openKv()
const chats = (await kv.get<string>(['chats'])).value
if (!chats) {
console.error('no chats yet, exiting')
return
}
const chatsParsed = JSON.parse(chats) as number[]
const sendMessage = async (
text: string,
options?: { [key: string]: unknown },
) => {
for (const chat of chatsParsed) {
const body = {
chat_id: chat,
text,
...options,
}
const req = await fetch(`${BOT}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!req.ok) {
console.log(await req.json())
}
}
}
const req = await fetch(SECURITY_PAGE)
const res = await req.text()
if (!req.ok) {
console.error('failed to load main support page')
console.log(res)
return
}
const parsed = parse(res)
const rows = parsed.querySelectorAll('tr').slice(1) // without heading row
// Rows: name with optional link - description - date
for (const row of rows.slice(0, 10)) {
const link = row.querySelector('a')
const [name, descr, date] = row
.querySelectorAll('td')
// biome-ignore lint: must have a child per html markup
.map((x) => x.firstChild!.innerText)
if (link) {
const href = link.attributes.href.toString()
const isProcessed = (await kv.get([href])).value
if (isProcessed) continue
// biome-ignore lint: trust me, this is nicer
const fullLink = 'https://support.apple.com' + href
const req = await fetch(fullLink)
const res = await req.text()
if (!req.ok) {
console.error('failed to load individual support page')
console.log(res)
return
}
const isExploited = STRING.test(res)
if (isExploited) {
await sendMessage(
[
'Update with a fix for an actively exploited vuln(s):',
name,
'', // spacer
'For:',
descr,
'', // spacer
'Released:',
date,
'', // spacer
`<a href="${fullLink}">Security Document</a>`,
].join('\n'),
{ parse_mode: 'HTML' },
)
}
await kv.set([href], 'done')
}
}
}
Deno.serve(serve)
Deno.cron('cron', '*/15 * * * *', cron)