feat(api, web): implement policy mode management for agents and rules
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m46s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added support for policy modes ('blacklist' and 'whitelist') in agent and policy set management.
- Updated API endpoints to handle policy mode during agent assignment and rule operations.
- Enhanced the web UI to display and manage policy modes for agents and rules, ensuring all assigned sets share a consistent mode.
- Introduced new validation to enforce single policy mode across assigned sets for agents.
- Improved error handling for policy mode conflicts and updated documentation accordingly.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 02:16:51 +07:00
co-authored by Cursor
parent d5784b9f35
commit 90d50c2556
15 changed files with 1261 additions and 266 deletions
@@ -0,0 +1,15 @@
-- Mode on policy sets + per-rule enabled
ALTER TABLE policy_sets ADD COLUMN policy_mode TEXT NOT NULL DEFAULT 'blacklist';
ALTER TABLE policy_rules ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
-- Backfill set mode from agents that use the set (prefer whitelist if any agent has it)
UPDATE policy_sets
SET policy_mode = 'whitelist'
WHERE id IN (
SELECT DISTINCT aps.set_id
FROM agent_policy_sets aps
INNER JOIN agents a ON a.id = aps.agent_id
WHERE a.policy_mode = 'whitelist'
);
+73 -1
View File
@@ -206,6 +206,7 @@ export function listSetsForAgent(db: Db, agentId: string) {
name: policySets.name,
description: policySets.description,
enabled: policySets.enabled,
policyMode: policySets.policyMode,
})
.from(agentPolicySets)
.innerJoin(policySets, eq(agentPolicySets.setId, policySets.id))
@@ -214,8 +215,24 @@ export function listSetsForAgent(db: Db, agentId: string) {
.all()
}
/** Replace agent↔set assignments; set_ids order = sort. */
/** Replace agent↔set assignments; set_ids order = sort. All sets must share policy_mode. */
export function setAgentPolicySets(db: Db, agentId: string, setIds: string[]) {
if (setIds.length > 0) {
const modes = new Set<string>()
for (const setId of setIds) {
const s = getPolicySet(db, setId)
if (!s) throw new Error(`policy set not found: ${setId}`)
modes.add(s.policyMode === 'whitelist' ? 'whitelist' : 'blacklist')
}
if (modes.size > 1) {
throw new Error(
'все наборы агента должны иметь один режим (blacklist или whitelist)',
)
}
const mode = [...modes][0] ?? 'blacklist'
updateAgent(db, agentId, { policyMode: mode })
}
db.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
setIds.forEach((setId, i) => {
db.insert(agentPolicySets)
@@ -270,6 +287,7 @@ export function listPolicyRulesForAgent(db: Db, agentId: string) {
.all()
return rules
.filter((r) => r.enabled !== 0)
.map((r) => ({
...r,
_setSort: sortBySet.get(r.setId) ?? 0,
@@ -289,6 +307,57 @@ export function insertPolicyRule(
return getPolicyRule(db, row.id)
}
export function updatePolicyRule(
db: Db,
id: string,
patch: Partial<typeof policyRules.$inferInsert>,
) {
db.update(policyRules)
.set({ ...patch, updatedAt: new Date().toISOString() })
.where(eq(policyRules.id, id))
.run()
return getPolicyRule(db, id)
}
/** Renumber priorities 10, 20, … in given order. */
export function reorderPolicyRules(
db: Db,
setId: string,
orderedIds: string[],
) {
const existing = listPolicyRules(db, setId)
const existingIds = new Set(existing.map((r) => r.id))
if (
orderedIds.length !== existing.length ||
orderedIds.some((id) => !existingIds.has(id))
) {
throw new Error('ordered_ids must list every rule in the set exactly once')
}
// Temporary priorities to avoid UNIQUE collisions
orderedIds.forEach((id, i) => {
db.update(policyRules)
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
.where(eq(policyRules.id, id))
.run()
})
orderedIds.forEach((id, i) => {
db.update(policyRules)
.set({
priority: (i + 1) * 10,
updatedAt: new Date().toISOString(),
})
.where(eq(policyRules.id, id))
.run()
})
}
export function nextRulePriority(db: Db, setId: string): number {
const rows = listPolicyRules(db, setId)
if (rows.length === 0) return 10
const max = Math.max(...rows.map((r) => r.priority))
return Math.min(10000, max + 10)
}
export function deletePolicyRule(db: Db, id: string) {
db.delete(policyRules).where(eq(policyRules.id, id)).run()
}
@@ -535,6 +604,9 @@ export const repos = {
listPolicyRulesForAgent,
getPolicyRule,
insertPolicyRule,
updatePolicyRule,
reorderPolicyRules,
nextRulePriority,
deletePolicyRule,
listHostnameRules,
listResolvedForRule,
+2
View File
@@ -84,6 +84,7 @@ export const policySets = sqliteTable('policy_sets', {
name: text('name').notNull(),
description: text('description'),
enabled: integer('enabled').notNull().default(1),
policyMode: text('policy_mode').notNull().default('blacklist'), // blacklist | whitelist
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -118,6 +119,7 @@ export const policyRules = sqliteTable(
.references(() => policySets.id, { onDelete: 'cascade' }),
priority: integer('priority').notNull(),
action: text('action').notNull(), // allow | deny
enabled: integer('enabled').notNull().default(1),
listId: text('list_id').references(() => ipLists.id, { onDelete: 'cascade' }),
cidr: text('cidr'),
hostname: text('hostname'),