fix(api): fail-safe прод-старт, транзакции и санитизация install-скриптов

- прод-режим отказывается стартовать без AUTH_REQUIRED и реальных секретов
  (opt-out через EVOFW_ALLOW_UNSAFE)
- CORS: whitelist через CORS_ORIGINS вместо origin:true; CSP для раздаваемого SPA
- транзакции для setAgentPolicySets, reorderPolicyRules, replaceResolvedForRule,
  replaceIpListEntries
- install-скрипты: Zod-валидация имени ссылки, экранирование $ и контрольных
  символов в RouterOS-рендере
- constant-time сравнение enroll-seed
- опциональное шифрование токена EvoBGP в БД (EVOFW_SECRET_KEY, AES-256-GCM)
  и маскирование per-list api_token в ответах
- graceful shutdown (SIGTERM/SIGINT) + тесты
This commit is contained in:
Denozordec
2026-09-20 18:51:47 +07:00
parent 45812fef6a
commit 40030ce06c
23 changed files with 493 additions and 69 deletions
+15 -13
View File
@@ -60,17 +60,19 @@ export function countEntriesByListIds(
}
export function replaceIpListEntries(db: Db, listId: string, cidrs: string[]) {
db.delete(ipListEntries).where(eq(ipListEntries.listId, listId)).run()
if (cidrs.length === 0) return
const now = new Date().toISOString()
db.insert(ipListEntries)
.values(
cidrs.map((cidr) => ({
id: crypto.randomUUID(),
listId,
cidr,
createdAt: now,
})),
)
.run()
db.transaction((tx) => {
tx.delete(ipListEntries).where(eq(ipListEntries.listId, listId)).run()
if (cidrs.length === 0) return
const now = new Date().toISOString()
tx.insert(ipListEntries)
.values(
cidrs.map((cidr) => ({
id: crypto.randomUUID(),
listId,
cidr,
createdAt: now,
})),
)
.run()
})
}
+40 -34
View File
@@ -170,11 +170,13 @@ export function setAgentPolicySets(db: Db, agentId: string, setIds: string[]) {
}
}
db.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
setIds.forEach((setId, i) => {
db.insert(agentPolicySets)
.values({ agentId, setId, sort: i * 10 })
.run()
db.transaction((tx) => {
tx.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
setIds.forEach((setId, i) => {
tx.insert(agentPolicySets)
.values({ agentId, setId, sort: i * 10 })
.run()
})
})
bumpAgentGeneration(db, agentId)
}
@@ -271,20 +273,22 @@ export function reorderPolicyRules(
throw new Error('ordered_ids must list every rule in the set exactly once')
}
// Temporary priorities to avoid UNIQUE collisions
orderedIds.forEach((id, i) => {
db.update(policyRules)
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
.where(eq(policyRules.id, id))
.run()
})
orderedIds.forEach((id, i) => {
db.update(policyRules)
.set({
priority: (i + 1) * 10,
updatedAt: new Date().toISOString(),
})
.where(eq(policyRules.id, id))
.run()
db.transaction((tx) => {
orderedIds.forEach((id, i) => {
tx.update(policyRules)
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
.where(eq(policyRules.id, id))
.run()
})
orderedIds.forEach((id, i) => {
tx.update(policyRules)
.set({
priority: (i + 1) * 10,
updatedAt: new Date().toISOString(),
})
.where(eq(policyRules.id, id))
.run()
})
})
}
@@ -316,21 +320,23 @@ export function listResolvedForRule(db: Db, ruleId: string) {
}
export function replaceResolvedForRule(db: Db, ruleId: string, cidrs: string[]) {
db.delete(policyRuleResolved)
.where(eq(policyRuleResolved.ruleId, ruleId))
.run()
if (cidrs.length === 0) return
const now = new Date().toISOString()
db.insert(policyRuleResolved)
.values(
cidrs.map((cidr) => ({
id: crypto.randomUUID(),
ruleId,
cidr,
resolvedAt: now,
})),
)
.run()
db.transaction((tx) => {
tx.delete(policyRuleResolved)
.where(eq(policyRuleResolved.ruleId, ruleId))
.run()
if (cidrs.length === 0) return
const now = new Date().toISOString()
tx.insert(policyRuleResolved)
.values(
cidrs.map((cidr) => ({
id: crypto.randomUUID(),
ruleId,
cidr,
resolvedAt: now,
})),
)
.run()
})
}
export function listOverrides(db: Db, agentId: string) {
+15 -1
View File
@@ -492,8 +492,22 @@ export const dashboardStatsSchema = z.object({
lists_total: z.number().int(),
})
/**
* Install-link client name. The name is embedded into generated bash /
* RouterOS install scripts, so it is limited to safe printable characters.
*/
export const installLinkNameSchema = z
.string()
.trim()
.min(1)
.max(64)
.regex(
/^[\p{L}\p{N}][\p{L}\p{N} .:_-]*$/u,
'Имя может содержать буквы, цифры и символы . : _ - (без перевода строк и кавычек)',
)
export const createInstallLinkBodySchema = z.object({
name: z.string().min(1),
name: installLinkNameSchema,
platform: agentPlatformSchema.optional().default('linux'),
})