feat(scan): add guided Linux agent setup in administration
Some checks failed
Build and Release FEDEO / verify (push) Successful in 23s
Build and Release FEDEO / build-and-release (push) Failing after 29s

This commit is contained in:
2026-09-17 18:56:30 +02:00
parent 1329f164d8
commit 68c0a224e5
15 changed files with 869 additions and 89 deletions

View File

@@ -90,10 +90,22 @@ export default async function instanceAgentRoutes(server: FastifyInstance) {
createdAt: instanceAgents.createdAt,
})
return {
agent,
token,
}
reply.header("Cache-Control", "no-store")
return { agent, token }
})
server.post<{ Params: { id: string } }>("/instance-agents/:id/token", async (req, reply) => {
if (!requireAdmin(req, reply)) return
const id = z.string().uuid().parse(req.params.id)
const token = createAgentToken()
const [agent] = await server.db
.update(instanceAgents)
.set({ tokenPrefix: token.slice(0, 24), tokenHash: hashToken(token), lastSeenAt: null, updatedAt: new Date() })
.where(eq(instanceAgents.id, id))
.returning({ id: instanceAgents.id })
if (!agent) return reply.code(404).send({ error: "Agent not found" })
reply.header("Cache-Control", "no-store")
return { token }
})
server.patch<{ Params: { id: string } }>("/instance-agents/:id", async (req, reply) => {

View File

@@ -0,0 +1,57 @@
import assert from "node:assert/strict"
import test from "node:test"
import { createHash } from "node:crypto"
import Fastify from "fastify"
import { PgDialect } from "drizzle-orm/pg-core"
import routes from "../src/routes/instanceAgents"
const id = "b48c0e33-1abd-4b11-89fa-1bb7b39a3057"
async function fixture(isAdmin: boolean) {
const updates: any[] = []
const app = Fastify()
app.addHook("onRequest", async (request) => { request.user = { is_admin: isAdmin } as any })
app.decorate("db", {
update: () => ({ set: (data: any) => ({ where: (condition: any) => ({ returning: async () => {
if (new PgDialect().sqlToQuery(condition).params[0] !== id) return []
updates.push(data)
return [{ id }]
} }) }) }),
})
await app.register(routes)
return { app, updates }
}
test("only administrators can rotate an agent token", async (t) => {
const f = await fixture(false)
t.after(() => f.app.close())
const response = await f.app.inject({ method: "POST", url: "/instance-agents/" + id + "/token" })
assert.equal(response.statusCode, 403)
assert.equal(f.updates.length, 0)
})
test("rotation returns a new token, stores only its hash and requires a new heartbeat", async (t) => {
const f = await fixture(true)
t.after(() => f.app.close())
const tokens = []
for (let i = 0; i < 2; i++) {
const response = await f.app.inject({ method: "POST", url: "/instance-agents/" + id + "/token" })
assert.equal(response.statusCode, 200)
assert.equal(response.headers["cache-control"], "no-store")
const { token } = response.json()
assert.match(token, /^fedeo_agent_[a-f0-9]{64}$/)
assert.equal(f.updates[i].tokenHash, createHash("sha256").update(token).digest("hex"))
assert.equal(f.updates[i].tokenPrefix, token.slice(0, 24))
assert.equal(f.updates[i].lastSeenAt, null)
assert.equal(f.updates[i].token, undefined)
tokens.push(token)
}
assert.notEqual(tokens[0], tokens[1])
})
test("unknown agents cannot receive setup credentials", async (t) => {
const f = await fixture(true)
t.after(() => f.app.close())
const response = await f.app.inject({ method: "POST", url: "/instance-agents/00000000-0000-4000-8000-000000000000/token" })
assert.equal(response.statusCode, 404)
assert.equal(f.updates.length, 0)
})