feat(audit): локальный журнал и push в auth-portal
Таблица audit_log, recordAudit на мутациях, GET /api/v1/audit и dual-write source_app=fw. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -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 }
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user