feat(audit): локальный журнал и push в auth-portal
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m54s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

Таблица audit_log, recordAudit на мутациях, GET /api/v1/audit и dual-write source_app=fw.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 13:24:34 +07:00
co-authored by Cursor
parent 919c1d0f95
commit 39e4856caa
16 changed files with 744 additions and 2 deletions
+201 -1
View File
@@ -35,6 +35,7 @@ import {
} from '../services/install-links.js'
import { hashToken } from '../plugins/auth.js'
import type { AppConfig } from '../config.js'
import { auditMutation } from '../services/audit.js'
function mapAgent(
a: NonNullable<ReturnType<typeof repos.getAgent>>,
@@ -202,6 +203,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
})()
const row = repos.getInstallLink(app.db, linkId)!
auditMutation(app, config, req, {
action: 'agent.create',
targetType: 'app_resource',
targetId: agentId,
summary: `Создан агент (invite): ${name}`,
details: { agent_id: agentId, platform, install_link_id: linkId },
})
return reply.code(201).send(mapInstallLink(row, config.publicBaseUrl))
})
@@ -261,7 +269,7 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
const body = patchAgentBodySchema.parse(req.body)
const a = repos.getAgent(app.db, req.params.id)
if (!a) throw new AppError('NOT_FOUND', 'Agent not found', 404)
const updated = repos.updateAgent(app.db, a.id, {
const updated = repos.updateAgent(app.db, a.id, {
name: body.name,
policyMode: body.policy_mode,
settingsJson: body.settings
@@ -272,6 +280,17 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
? a.policyGeneration + 1
: a.policyGeneration,
})
auditMutation(app, config, req, {
action: 'agent.update',
targetType: 'app_resource',
targetId: a.id,
summary: `Обновлён агент: ${updated!.name}`,
details: {
agent_id: a.id,
policy_mode: body.policy_mode,
name: body.name,
},
})
return mapAgent(updated!)
})
@@ -283,6 +302,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
approvedAt: new Date().toISOString(),
})
repos.ensureSharedSetAssigned(app.db, a.id)
auditMutation(app, config, req, {
action: 'agent.approve',
targetType: 'app_resource',
targetId: a.id,
summary: `Агент одобрен: ${updated!.name}`,
details: { agent_id: a.id },
})
return mapAgent(updated!)
})
@@ -293,11 +319,30 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
status: 'revoked',
revokedAt: new Date().toISOString(),
})
auditMutation(app, config, req, {
action: 'agent.revoke',
severity: 'warning',
targetType: 'app_resource',
targetId: a.id,
summary: `Агент отозван: ${updated!.name}`,
details: { agent_id: a.id },
})
return mapAgent(updated!)
})
app.delete<{ Params: { id: string } }>('/agents/:id', async (req) => {
const a = repos.getAgent(app.db, req.params.id)
repos.deleteAgent(app.db, req.params.id)
if (a) {
auditMutation(app, config, req, {
action: 'agent.delete',
severity: 'warning',
targetType: 'app_resource',
targetId: a.id,
summary: `Агент удалён: ${a.name}`,
details: { agent_id: a.id },
})
}
return { ok: true }
})
@@ -312,6 +357,17 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
body.include_overrides ?? false,
)
if (!updated) throw new AppError('NOT_FOUND', 'Agent not found', 404)
auditMutation(app, config, req, {
action: 'agent.clone_rules',
targetType: 'app_resource',
targetId: updated.id,
summary: `Правила скопированы с ${req.params.sourceId} на ${updated.name}`,
details: {
agent_id: updated.id,
source_agent_id: req.params.sourceId,
include_overrides: body.include_overrides ?? false,
},
})
return mapAgent(updated)
},
)
@@ -347,6 +403,18 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
createdAt: new Date().toISOString(),
})
repos.bumpAgentGeneration(app.db, a.id)
auditMutation(app, config, req, {
action: 'override.create',
targetType: 'app_resource',
targetId: row!.id,
summary: `Override ${body.action} ${body.cidr} для ${a.name}`,
details: {
override_id: row!.id,
agent_id: a.id,
cidr: body.cidr,
action: body.action,
},
})
return {
id: row!.id,
agent_id: row!.agentId,
@@ -363,6 +431,17 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
async (req) => {
repos.deleteOverride(app.db, req.params.overrideId)
repos.bumpAgentGeneration(app.db, req.params.id)
auditMutation(app, config, req, {
action: 'override.delete',
severity: 'warning',
targetType: 'app_resource',
targetId: req.params.overrideId,
summary: `Override удалён у агента ${req.params.id}`,
details: {
override_id: req.params.overrideId,
agent_id: req.params.id,
},
})
return { ok: true }
},
)
@@ -411,6 +490,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
} 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,
@@ -438,6 +524,16 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
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(
@@ -457,6 +553,14 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
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(
@@ -469,14 +573,33 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
)
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 }
})
@@ -505,6 +628,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
auditMutation(app, config, req, {
action: 'policy_set.create',
targetType: 'app_resource',
targetId: row!.id,
summary: `Создан набор политик: ${row!.name}`,
details: { set_id: row!.id, policy_mode: row!.policyMode },
})
return mapPolicySet(row!, app.db)
})
@@ -547,14 +677,37 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
}
}
}
auditMutation(app, config, req, {
action: 'policy_set.update',
targetType: 'app_resource',
targetId: s.id,
summary: `Обновлён набор политик: ${updated!.name}`,
details: {
set_id: s.id,
enabled: body.enabled,
policy_mode: body.policy_mode,
name: body.name,
},
})
return mapPolicySet(updated!, app.db)
})
app.delete<{ Params: { id: string } }>('/policy-sets/:id', async (req) => {
const s = repos.getPolicySet(app.db, req.params.id)
try {
const agentIds = repos.listAgentIdsForSet(app.db, req.params.id)
repos.deletePolicySet(app.db, req.params.id)
for (const id of agentIds) repos.bumpAgentGeneration(app.db, id)
if (s) {
auditMutation(app, config, req, {
action: 'policy_set.delete',
severity: 'warning',
targetType: 'app_resource',
targetId: s.id,
summary: `Набор политик удалён: ${s.name}`,
details: { set_id: s.id, agents_affected: agentIds.length },
})
}
} catch (err) {
throw new AppError(
'VALIDATION_ERROR',
@@ -598,6 +751,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
400,
)
}
auditMutation(app, config, req, {
action: 'agent.policy_sets.update',
targetType: 'app_resource',
targetId: a.id,
summary: `Наборы политик агента ${a.name} обновлены`,
details: { agent_id: a.id, set_ids: body.set_ids },
})
return {
items: repos.listSetsForAgent(app.db, a.id).map((s) => ({
set_id: s.setId,
@@ -707,6 +867,18 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
}
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)
})
@@ -721,6 +893,19 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
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)
})
@@ -740,6 +925,13 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
)
}
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: repos
.listPolicyRules(app.db, s.id)
@@ -753,6 +945,14 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
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 }
})