- 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)
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import type { FastifyPluginAsync } from 'fastify'
|
|
import { repos } from '@evofw/db'
|
|
import {
|
|
createIpListBodySchema,
|
|
listEntriesBodySchema,
|
|
deleteListEntryBodySchema,
|
|
isManualListType,
|
|
} from '@evofw/shared'
|
|
import { AppError } from '../plugins/error-handler.js'
|
|
import { refreshIpList } from '../services/lists/refresh.js'
|
|
import {
|
|
addListEntries,
|
|
deleteListEntry,
|
|
mapListDetail,
|
|
} from '../services/lists/entries.js'
|
|
import type { AppConfig } from '../config.js'
|
|
import { auditMutation } from '../services/audit.js'
|
|
import { maskListConfig, sealListConfig } from '../services/secret-cipher.js'
|
|
import { applyPagination } from '../services/pagination.js'
|
|
|
|
export const listsRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
|
|
app,
|
|
opts,
|
|
) => {
|
|
const { config } = opts
|
|
|
|
app.get<{ Querystring: { limit?: string; offset?: string } }>(
|
|
'/lists',
|
|
async (req) => {
|
|
const lists = repos.listIpLists(app.db)
|
|
const counts = repos.countEntriesByListIds(
|
|
app.db,
|
|
lists.map((l) => l.id),
|
|
)
|
|
const items = lists.map((l) => ({
|
|
id: l.id,
|
|
name: l.name,
|
|
type: l.type,
|
|
config_json: maskListConfig(l.configJson),
|
|
content_hash: l.contentHash,
|
|
refreshed_at: l.refreshedAt,
|
|
last_error: l.lastError,
|
|
entry_count: counts.get(l.id) ?? 0,
|
|
created_at: l.createdAt,
|
|
updated_at: l.updatedAt,
|
|
}))
|
|
const paged = applyPagination(items, req.query)
|
|
return { items: paged.items, total: paged.total }
|
|
},
|
|
)
|
|
|
|
app.post('/lists', async (req) => {
|
|
const body = createIpListBodySchema.parse(req.body)
|
|
const type =
|
|
body.type === 'domains' ? 'static' : body.type
|
|
const id = crypto.randomUUID()
|
|
const list = repos.insertIpList(app.db, {
|
|
id,
|
|
name: body.name,
|
|
type,
|
|
configJson: sealListConfig({ ...(body.config ?? {}) }),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
if (body.entries?.length && isManualListType(type)) {
|
|
try {
|
|
await addListEntries(app.db, id, { values: body.entries })
|
|
} catch (err) {
|
|
repos.deleteIpList(app.db, id)
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
} else if (!isManualListType(type)) {
|
|
await refreshIpList(app.db, id)
|
|
}
|
|
auditMutation(app, config, req, {
|
|
action: 'list.create',
|
|
targetType: 'app_resource',
|
|
targetId: list!.id,
|
|
summary: `Создан список: ${list!.name}`,
|
|
details: { list_id: list!.id, type: list!.type },
|
|
})
|
|
return {
|
|
id: list!.id,
|
|
name: list!.name,
|
|
type: list!.type,
|
|
config_json: maskListConfig(list!.configJson),
|
|
created_at: list!.createdAt,
|
|
updated_at: list!.updatedAt,
|
|
}
|
|
})
|
|
|
|
app.get<{ Params: { id: string } }>('/lists/:id', async (req) => {
|
|
const detail = mapListDetail(app.db, req.params.id)
|
|
if (!detail) throw new AppError('NOT_FOUND', 'List not found', 404)
|
|
return detail
|
|
})
|
|
|
|
app.post<{ Params: { id: string } }>(
|
|
'/lists/:id/entries',
|
|
async (req) => {
|
|
const l = repos.getIpList(app.db, req.params.id)
|
|
if (!l) throw new AppError('NOT_FOUND', 'List not found', 404)
|
|
const body = listEntriesBodySchema.parse(req.body)
|
|
try {
|
|
const result = await addListEntries(app.db, l.id, {
|
|
values: body.values,
|
|
items: body.items,
|
|
})
|
|
auditMutation(app, config, req, {
|
|
action: 'list.entries.add',
|
|
targetType: 'app_resource',
|
|
targetId: l.id,
|
|
summary: `Добавлены записи в список: ${l.name}`,
|
|
details: {
|
|
list_id: l.id,
|
|
entry_count: result.entries.length,
|
|
},
|
|
})
|
|
return mapListDetail(app.db, l.id) ?? result
|
|
} catch (err) {
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
|
|
app.delete<{ Params: { id: string } }>(
|
|
'/lists/:id/entries',
|
|
async (req) => {
|
|
const l = repos.getIpList(app.db, req.params.id)
|
|
if (!l) throw new AppError('NOT_FOUND', 'List not found', 404)
|
|
const body = deleteListEntryBodySchema.parse(req.body)
|
|
try {
|
|
await deleteListEntry(app.db, l.id, body.value)
|
|
auditMutation(app, config, req, {
|
|
action: 'list.entries.delete',
|
|
severity: 'warning',
|
|
targetType: 'app_resource',
|
|
targetId: l.id,
|
|
summary: `Удалена запись из списка: ${l.name}`,
|
|
details: { list_id: l.id, value: body.value },
|
|
})
|
|
return mapListDetail(app.db, l.id)
|
|
} catch (err) {
|
|
throw new AppError(
|
|
'VALIDATION_ERROR',
|
|
err instanceof Error ? err.message : String(err),
|
|
400,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
|
|
app.post<{ Params: { id: string } }>('/lists/:id/refresh', async (req) => {
|
|
const l = repos.getIpList(app.db, req.params.id)
|
|
await refreshIpList(app.db, req.params.id)
|
|
const detail = mapListDetail(app.db, req.params.id)
|
|
if (!detail) throw new AppError('NOT_FOUND', 'List not found', 404)
|
|
auditMutation(app, config, req, {
|
|
action: 'list.refresh',
|
|
targetType: 'app_resource',
|
|
targetId: req.params.id,
|
|
summary: `Обновлён список: ${l?.name ?? req.params.id}`,
|
|
details: { list_id: req.params.id },
|
|
})
|
|
return detail
|
|
})
|
|
|
|
app.delete<{ Params: { id: string } }>('/lists/:id', async (req) => {
|
|
const l = repos.getIpList(app.db, req.params.id)
|
|
repos.deleteIpList(app.db, req.params.id)
|
|
if (l) {
|
|
auditMutation(app, config, req, {
|
|
action: 'list.delete',
|
|
severity: 'warning',
|
|
targetType: 'app_resource',
|
|
targetId: l.id,
|
|
summary: `Список удалён: ${l.name}`,
|
|
details: { list_id: l.id },
|
|
})
|
|
}
|
|
return { ok: true }
|
|
})
|
|
}
|