- retention: ежедневный cron (03:17) + запуск на старте удаляет сырые
agent_stats_samples старше STATS_RETENTION_DAYS (по умолчанию 30);
lifetime-итоги на агенте и агрегаты сохраняются
- N+1 на поллинг-пути агента: evaluateAgentPolicy теперь делает по одному
батч-запросу на записи списков / резолвы hostname / имена списков вместо
запроса на каждое правило; mapPolicyRules и port-rules GET — аналогично
- пагинация: GET /agents, /lists, /rules, /install-links принимают
limit/offset и возвращают total; без параметров — прежнее поведение
- openapi.yaml: approve-bulk, PUT /policy-sets/{id}/agents, параметры
пагинации (redocly lint OK); тесты (47 passed)
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
import type { FastifyPluginAsync } from 'fastify'
|
|
import { repos } from '@evofw/db'
|
|
import {
|
|
createPolicyRuleBodySchema,
|
|
patchPolicyRuleBodySchema,
|
|
reorderPolicyRulesBodySchema,
|
|
} from '@evofw/shared'
|
|
import { AppError } from '../plugins/error-handler.js'
|
|
import {
|
|
resolveAndStoreHostnameRule,
|
|
resolveHostnameToCidrs,
|
|
} from '../services/policy/resolve-hostname.js'
|
|
import type { AppConfig } from '../config.js'
|
|
import { auditMutation } from '../services/audit.js'
|
|
import { mapPolicyRule, mapPolicyRules } from '../services/row-mappers.js'
|
|
import { applyPagination } from '../services/pagination.js'
|
|
|
|
export const rulesRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
|
|
app,
|
|
opts,
|
|
) => {
|
|
const { config } = opts
|
|
|
|
app.get<{ Params: { id: string } }>(
|
|
'/policy-sets/:id/rules',
|
|
async (req) => {
|
|
const s = repos.getPolicySet(app.db, req.params.id)
|
|
if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404)
|
|
return {
|
|
items: mapPolicyRules(repos.listPolicyRules(app.db, s.id), app.db),
|
|
}
|
|
},
|
|
)
|
|
|
|
app.get<{
|
|
Querystring: { set_id?: string; agent_id?: string; limit?: string; offset?: string }
|
|
}>('/rules', async (req) => {
|
|
if (req.query.agent_id) {
|
|
return {
|
|
items: mapPolicyRules(
|
|
repos.listPolicyRulesForAgent(app.db, req.query.agent_id),
|
|
app.db,
|
|
),
|
|
}
|
|
}
|
|
const paged = applyPagination(
|
|
mapPolicyRules(repos.listPolicyRules(app.db, req.query.set_id), app.db),
|
|
req.query,
|
|
)
|
|
return { items: paged.items, total: paged.total }
|
|
})
|
|
|
|
app.post('/rules', async (req) => {
|
|
const body = createPolicyRuleBodySchema.parse(req.body)
|
|
const set = repos.getPolicySet(app.db, body.set_id)
|
|
if (!set) throw new AppError('NOT_FOUND', 'Policy set not found', 404)
|
|
|
|
const hostname = body.hostname?.trim() || null
|
|
const cidr = body.cidr?.trim() || null
|
|
const listId = body.list_id?.trim() || null
|
|
|
|
if (hostname) {
|
|
try {
|
|
await resolveHostnameToCidrs(hostname)
|
|
} catch (err) {
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
}
|
|
|
|
if (listId && !repos.getIpList(app.db, listId)) {
|
|
throw new AppError('NOT_FOUND', 'IP list not found', 404)
|
|
}
|
|
|
|
const priority =
|
|
body.priority ?? repos.nextRulePriority(app.db, body.set_id)
|
|
|
|
const id = crypto.randomUUID()
|
|
const row = repos.insertPolicyRule(app.db, {
|
|
id,
|
|
setId: body.set_id,
|
|
priority,
|
|
action: body.action,
|
|
enabled: body.enabled === false ? 0 : 1,
|
|
listId,
|
|
cidr,
|
|
hostname,
|
|
comment: body.comment ?? null,
|
|
createdByUserId: req.authUser?.id,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
|
|
if (hostname) {
|
|
try {
|
|
await resolveAndStoreHostnameRule(app.db, id, hostname)
|
|
} catch (err) {
|
|
repos.deletePolicyRule(app.db, id)
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
}
|
|
|
|
repos.bumpAgentsForSet(app.db, body.set_id)
|
|
auditMutation(app, config, req, {
|
|
action: 'rule.create',
|
|
targetType: 'app_resource',
|
|
targetId: row!.id,
|
|
summary: `Создано правило ${body.action} в наборе ${set.name}`,
|
|
details: {
|
|
rule_id: row!.id,
|
|
set_id: body.set_id,
|
|
action: body.action,
|
|
priority,
|
|
},
|
|
})
|
|
return mapPolicyRule(row!, app.db)
|
|
})
|
|
|
|
app.patch<{ Params: { id: string } }>('/rules/:id', async (req) => {
|
|
const body = patchPolicyRuleBodySchema.parse(req.body)
|
|
const rule = repos.getPolicyRule(app.db, req.params.id)
|
|
if (!rule) throw new AppError('NOT_FOUND', 'Rule not found', 404)
|
|
const updated = repos.updatePolicyRule(app.db, rule.id, {
|
|
enabled: body.enabled === undefined ? undefined : body.enabled ? 1 : 0,
|
|
action: body.action,
|
|
comment: body.comment,
|
|
priority: body.priority,
|
|
})
|
|
repos.bumpAgentsForSet(app.db, rule.setId)
|
|
auditMutation(app, config, req, {
|
|
action: 'rule.update',
|
|
targetType: 'app_resource',
|
|
targetId: rule.id,
|
|
summary: `Обновлено правило ${rule.id}`,
|
|
details: {
|
|
rule_id: rule.id,
|
|
set_id: rule.setId,
|
|
enabled: body.enabled,
|
|
action: body.action,
|
|
priority: body.priority,
|
|
},
|
|
})
|
|
return mapPolicyRule(updated!, app.db)
|
|
})
|
|
|
|
app.put<{ Params: { id: string } }>(
|
|
'/policy-sets/:id/rules/reorder',
|
|
async (req) => {
|
|
const body = reorderPolicyRulesBodySchema.parse(req.body)
|
|
const s = repos.getPolicySet(app.db, req.params.id)
|
|
if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404)
|
|
try {
|
|
repos.reorderPolicyRules(app.db, s.id, body.ordered_ids)
|
|
} catch (err) {
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
repos.bumpAgentsForSet(app.db, s.id)
|
|
auditMutation(app, config, req, {
|
|
action: 'rule.reorder',
|
|
targetType: 'app_resource',
|
|
targetId: s.id,
|
|
summary: `Порядок правил изменён в наборе ${s.name}`,
|
|
details: { set_id: s.id, ordered_ids: body.ordered_ids },
|
|
})
|
|
return {
|
|
items: mapPolicyRules(
|
|
repos.listPolicyRules(app.db, s.id),
|
|
app.db,
|
|
),
|
|
}
|
|
},
|
|
)
|
|
|
|
app.delete<{ Params: { id: string } }>('/rules/:id', async (req) => {
|
|
const rule = repos.getPolicyRule(app.db, req.params.id)
|
|
if (!rule) throw new AppError('NOT_FOUND', 'Rule not found', 404)
|
|
repos.deletePolicyRule(app.db, req.params.id)
|
|
repos.bumpAgentsForSet(app.db, rule.setId)
|
|
auditMutation(app, config, req, {
|
|
action: 'rule.delete',
|
|
severity: 'warning',
|
|
targetType: 'app_resource',
|
|
targetId: rule.id,
|
|
summary: `Правило удалено из набора ${rule.setId}`,
|
|
details: { rule_id: rule.id, set_id: rule.setId },
|
|
})
|
|
return { ok: true }
|
|
})
|
|
}
|