- Added a `maybe_self_update` function in `evofw-firewall.sh` to allow agents to pull the latest version of the sync script from the server, enhancing the agent's ability to stay updated. - Updated the `/v1/agent/sync-script` endpoint to return ETag and script SHA256 headers, enabling efficient caching and conditional requests. - Modified the agent policy response to include `script_sha256`, providing visibility into the current version of the sync script. - Enhanced tests to verify the self-update functionality and ensure correct behavior of the sync script endpoint. These changes improve the maintainability and reliability of Linux agents by enabling automatic updates of critical scripts.
331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import { describe, it, expect, afterAll } from 'vitest'
|
|
import { buildApp } from '../app.js'
|
|
import type { AppConfig } from '../config.js'
|
|
|
|
const testConfig: AppConfig = {
|
|
databaseUrl: 'sqlite::memory:',
|
|
jwtSecret: 'test',
|
|
jwtTtlHours: 24,
|
|
serverPort: 8080,
|
|
staticDir: null,
|
|
logLevel: 'error',
|
|
authRequired: false,
|
|
authIssuer: 'https://auth.test',
|
|
authPortalUrl: 'http://localhost:5175',
|
|
publicBaseUrl: 'https://fw.example.com',
|
|
enrollSeed: 'test-seed',
|
|
}
|
|
|
|
describe('install-links', () => {
|
|
const appPromise = buildApp({ memory: true, config: testConfig })
|
|
|
|
afterAll(async () => {
|
|
const app = await appPromise
|
|
await app.close()
|
|
})
|
|
|
|
it('creates invited agent and serves scripts by id and slug', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'web-01', platform: 'linux' },
|
|
})
|
|
expect(created.statusCode).toBe(201)
|
|
const body = created.json() as {
|
|
id: string
|
|
slug: string
|
|
agent_id: string
|
|
curl: { by_id: string; by_slug: string }
|
|
}
|
|
expect(body.id).toBeTruthy()
|
|
expect(body.slug).toBeTruthy()
|
|
expect(body.agent_id).toBeTruthy()
|
|
expect(body.curl.by_id).toContain(`/agent-install/${body.id}`)
|
|
|
|
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
|
|
expect(agents.statusCode).toBe(200)
|
|
const list = agents.json() as {
|
|
items: {
|
|
id: string
|
|
status: string
|
|
install_curl?: string | null
|
|
}[]
|
|
}
|
|
const invited = list.items.find((a) => a.id === body.agent_id)
|
|
expect(invited?.status).toBe('invited')
|
|
expect(invited?.install_curl).toContain(body.slug)
|
|
|
|
const byId = await app.inject({
|
|
method: 'GET',
|
|
url: `/agent-install/${body.id}`,
|
|
})
|
|
expect(byId.statusCode).toBe(200)
|
|
expect(byId.headers['content-type']).toContain('text/x-shellscript')
|
|
expect(byId.body).toContain("EVOFW_CLIENT_NAME='web-01'")
|
|
expect(byId.body).toContain(`EVOFW_INSTALL_LINK_ID='${body.id}'`)
|
|
|
|
const bySlug = await app.inject({
|
|
method: 'GET',
|
|
url: `/${body.slug}`,
|
|
})
|
|
expect(bySlug.statusCode).toBe(200)
|
|
expect(bySlug.body).toContain("EVOFW_CP_URL='https://fw.example.com'")
|
|
})
|
|
|
|
it('enroll with install_link_id updates invited agent to pending', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'web-02', platform: 'linux' },
|
|
})
|
|
const link = created.json() as { id: string; agent_id: string }
|
|
|
|
const enroll = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/agent/enroll',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-evofw-seed': 'test-seed',
|
|
},
|
|
payload: {
|
|
name: 'web-02',
|
|
hostname: 'host-02',
|
|
platform: 'linux',
|
|
token: 'evofw_test_token_1234567890abcd',
|
|
install_link_id: link.id,
|
|
},
|
|
})
|
|
expect(enroll.statusCode).toBe(201)
|
|
const enrolled = enroll.json() as { id: string; status: string }
|
|
expect(enrolled.id).toBe(link.agent_id)
|
|
expect(enrolled.status).toBe('pending')
|
|
|
|
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
|
|
const list = agents.json() as { items: { id: string; status: string }[] }
|
|
const row = list.items.find((a) => a.id === link.agent_id)
|
|
expect(row?.status).toBe('pending')
|
|
})
|
|
|
|
it('mikrotik install link serves RSC and fetch/import one-liner', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'mt-01', platform: 'mikrotik' },
|
|
})
|
|
expect(created.statusCode).toBe(201)
|
|
const body = created.json() as {
|
|
id: string
|
|
slug: string
|
|
agent_id: string
|
|
curl: { by_id: string; by_slug: string }
|
|
}
|
|
expect(body.curl.by_id).toContain('/tool fetch url=')
|
|
expect(body.curl.by_id).toContain('/import file-name=evofw-install.rsc')
|
|
expect(body.curl.by_id).not.toContain('| bash')
|
|
|
|
const byId = await app.inject({
|
|
method: 'GET',
|
|
url: `/agent-install/${body.id}`,
|
|
})
|
|
expect(byId.statusCode).toBe(200)
|
|
expect(byId.headers['content-type']).toContain('text/plain')
|
|
expect(byId.body).toContain(':global EvofwCpUrl "https://fw.example.com"')
|
|
expect(byId.body).toContain(`:global EvofwInstallLinkId "${body.id}"`)
|
|
expect(byId.body).toContain('evofw-deny-drop-input')
|
|
expect(byId.body).toContain('/v1/agent/policy')
|
|
expect(byId.body).toContain(':deserialize')
|
|
expect(byId.body).toContain('evofw-sync-body')
|
|
expect(byId.body).toContain('remove [find name="evofw-env"]')
|
|
})
|
|
|
|
it('approved agent can fetch empty policy without rule sets', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'empty-policy', platform: 'linux' },
|
|
})
|
|
const link = created.json() as { id: string; agent_id: string }
|
|
const token = 'evofw_empty_policy_token_abcdefgh'
|
|
|
|
const enroll = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/agent/enroll',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-evofw-seed': 'test-seed',
|
|
},
|
|
payload: {
|
|
name: 'empty-policy',
|
|
platform: 'linux',
|
|
token,
|
|
install_link_id: link.id,
|
|
},
|
|
})
|
|
expect(enroll.statusCode).toBe(201)
|
|
|
|
await app.inject({
|
|
method: 'POST',
|
|
url: `/api/v1/agents/${link.agent_id}/approve`,
|
|
})
|
|
|
|
const policy = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/policy',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
})
|
|
expect(policy.statusCode).toBe(200)
|
|
const body = policy.json() as {
|
|
deny_cidrs: string[]
|
|
allow_cidrs: string[]
|
|
default_action: string
|
|
policy_mode: string
|
|
apply_version: number
|
|
hash: string
|
|
script_sha256: string
|
|
}
|
|
expect(body.deny_cidrs).toEqual([])
|
|
expect(body.allow_cidrs).toEqual([])
|
|
expect(body.default_action).toBe('accept')
|
|
expect(body.policy_mode).toBe('blacklist')
|
|
expect(body.apply_version).toBe(3)
|
|
expect(body.hash).toMatch(/^sha256:/)
|
|
expect(body.script_sha256).toMatch(/^[a-f0-9]{64}$/)
|
|
|
|
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
|
|
const row = (
|
|
agents.json() as { items: { id: string; last_seen_at: string | null }[] }
|
|
).items.find((a) => a.id === link.agent_id)
|
|
expect(row?.last_seen_at).toBeTruthy()
|
|
})
|
|
|
|
it('approved agent can fetch policy.rsc with address-list commands', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'mt-policy', platform: 'mikrotik' },
|
|
})
|
|
const link = created.json() as { id: string; agent_id: string }
|
|
const token = 'evofw_mt_policy_token_abcdefghij'
|
|
|
|
const enroll = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/agent/enroll',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-evofw-seed': 'test-seed',
|
|
},
|
|
payload: {
|
|
name: 'mt-policy',
|
|
platform: 'mikrotik',
|
|
token,
|
|
install_link_id: link.id,
|
|
},
|
|
})
|
|
expect(enroll.statusCode).toBe(201)
|
|
|
|
await app.inject({
|
|
method: 'POST',
|
|
url: `/api/v1/agents/${link.agent_id}/approve`,
|
|
})
|
|
|
|
// add a deny override so policy has a CIDR
|
|
await app.inject({
|
|
method: 'POST',
|
|
url: `/api/v1/agents/${link.agent_id}/overrides`,
|
|
payload: { action: 'deny', cidr: '203.0.113.0/24' },
|
|
})
|
|
|
|
const rsc = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/policy.rsc',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
})
|
|
expect(rsc.statusCode).toBe(200)
|
|
expect(rsc.headers['content-type']).toContain('text/plain')
|
|
expect(rsc.body).toContain('address-list')
|
|
expect(rsc.body).toContain('EVOFW_DENY')
|
|
expect(rsc.body).toContain('203.0.113.0/24')
|
|
})
|
|
|
|
it('sync-script serves ETag and 304 on If-None-Match', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const first = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/sync-script',
|
|
})
|
|
expect(first.statusCode).toBe(200)
|
|
expect(first.body.startsWith('#!')).toBe(true)
|
|
expect(first.body).toContain('maybe_self_update')
|
|
const etag = String(first.headers.etag ?? '')
|
|
const sha = String(first.headers['x-evofw-script-sha256'] ?? '')
|
|
expect(etag).toMatch(/^"[a-f0-9]{64}"$/)
|
|
expect(sha).toBe(etag.replaceAll('"', ''))
|
|
|
|
const cached = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/sync-script',
|
|
headers: { 'if-none-match': etag },
|
|
})
|
|
expect(cached.statusCode).toBe(304)
|
|
|
|
const miss = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/sync-script',
|
|
headers: { 'if-none-match': '"deadbeef"' },
|
|
})
|
|
expect(miss.statusCode).toBe(200)
|
|
expect(miss.body).toBe(first.body)
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'script-sha-policy', platform: 'linux' },
|
|
})
|
|
const link = created.json() as { id: string; agent_id: string }
|
|
const token = 'evofw_script_sha_token_abcdefghij'
|
|
const enroll = await app.inject({
|
|
method: 'POST',
|
|
url: '/v1/agent/enroll',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-evofw-seed': 'test-seed',
|
|
},
|
|
payload: {
|
|
name: 'script-sha-policy',
|
|
platform: 'linux',
|
|
token,
|
|
install_link_id: link.id,
|
|
},
|
|
})
|
|
expect(enroll.statusCode).toBe(201)
|
|
await app.inject({
|
|
method: 'POST',
|
|
url: `/api/v1/agents/${link.agent_id}/approve`,
|
|
})
|
|
const policy = await app.inject({
|
|
method: 'GET',
|
|
url: '/v1/agent/policy',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
})
|
|
expect(policy.statusCode).toBe(200)
|
|
expect((policy.json() as { script_sha256: string }).script_sha256).toBe(sha)
|
|
})
|
|
})
|