Files
EvoFirewall/packages/db/src/schema.ts
T
DenozordecandCursor c5069fbdaf
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m59s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped
feat(api, web): add Linux nft destination port hits for blocked IPs
Track tcp/udp dports via deny_port_hits, expose aggregate and per-IP ports in UI; install-link re-run refreshes nft rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 00:33:03 +07:00

334 lines
11 KiB
TypeScript

import { sqliteTable, text, integer, uniqueIndex, index } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull().default(''),
updatedAt: text('updated_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
})
export const agents = sqliteTable(
'agents',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
hostname: text('hostname'),
platform: text('platform').notNull().default('linux'), // linux | mikrotik
tokenPrefix: text('token_prefix').notNull(),
tokenHash: text('token_hash').notNull(),
status: text('status').notNull().default('pending'), // invited | pending | approved | revoked
/** Packet default when not in deny/allow sets: accept | drop */
defaultAction: text('default_action').notNull().default('accept'),
policyGeneration: integer('policy_generation').notNull().default(1),
lastSeenAt: text('last_seen_at'),
lastSeenIp: text('last_seen_ip'),
lastApplyAt: text('last_apply_at'),
lastApplyStatus: text('last_apply_status'),
lastApplyError: text('last_apply_error'),
lastApplyPrefixCount: integer('last_apply_prefix_count').default(0),
lastApplyPacketsDropped: integer('last_apply_packets_dropped').notNull().default(0),
lastApplyPacketsAccepted: integer('last_apply_packets_accepted').notNull().default(0),
/** Lifetime counters until UI/API reset (accumulate across applies). */
totalPacketsDropped: integer('total_packets_dropped').notNull().default(0),
totalPacketsAccepted: integer('total_packets_accepted').notNull().default(0),
lastApplyKernelMethod: text('last_apply_kernel_method'),
clientVersion: text('client_version'),
settingsJson: text('settings_json').notNull().default('{}'),
createdByUserId: text('created_by_user_id'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
approvedAt: text('approved_at'),
revokedAt: text('revoked_at'),
},
(t) => ({
tokenHashIdx: uniqueIndex('idx_agents_token_hash').on(t.tokenHash),
statusIdx: index('idx_agents_status').on(t.status),
}),
)
export const ipLists = sqliteTable('ip_lists', {
id: text('id').primaryKey(),
name: text('name').notNull(),
type: text('type').notNull(), // static | json_url | domains | evobgp_community
configJson: text('config_json').notNull().default('{}'),
contentHash: text('content_hash'),
refreshedAt: text('refreshed_at'),
lastError: text('last_error'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
})
export const ipListEntries = sqliteTable(
'ip_list_entries',
{
id: text('id').primaryKey(),
listId: text('list_id')
.notNull()
.references(() => ipLists.id, { onDelete: 'cascade' }),
cidr: text('cidr').notNull(),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
listCidr: uniqueIndex('idx_ip_list_entries_list_cidr').on(t.listId, t.cidr),
}),
)
/** Named reusable policy sets (M:N with agents). */
export const policySets = sqliteTable('policy_sets', {
id: text('id').primaryKey(),
name: text('name').notNull(),
description: text('description'),
enabled: integer('enabled').notNull().default(1),
/** Legacy unused; sets no longer carry exclusive mode. */
policyMode: text('policy_mode').notNull().default('blacklist'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
})
export const agentPolicySets = sqliteTable(
'agent_policy_sets',
{
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
setId: text('set_id')
.notNull()
.references(() => policySets.id, { onDelete: 'cascade' }),
sort: integer('sort').notNull().default(0),
},
(t) => ({
pk: uniqueIndex('idx_agent_policy_sets_pk').on(t.agentId, t.setId),
setIdx: index('idx_agent_policy_sets_set').on(t.setId),
}),
)
export const policyRules = sqliteTable(
'policy_rules',
{
id: text('id').primaryKey(),
setId: text('set_id')
.notNull()
.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'),
comment: text('comment'),
createdByUserId: text('created_by_user_id'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
setPriority: uniqueIndex('idx_policy_rules_set_priority').on(t.setId, t.priority),
}),
)
/** DNS resolve cache for hostname rules. */
export const policyRuleResolved = sqliteTable(
'policy_rule_resolved',
{
id: text('id').primaryKey(),
ruleId: text('rule_id')
.notNull()
.references(() => policyRules.id, { onDelete: 'cascade' }),
cidr: text('cidr').notNull(),
resolvedAt: text('resolved_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
ruleCidr: uniqueIndex('idx_policy_rule_resolved_rule_cidr').on(t.ruleId, t.cidr),
ruleIdx: index('idx_policy_rule_resolved_rule').on(t.ruleId),
}),
)
export const ipOverrides = sqliteTable(
'ip_overrides',
{
id: text('id').primaryKey(),
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
cidr: text('cidr').notNull(),
action: text('action').notNull(), // allow | deny
comment: text('comment'),
createdByUserId: text('created_by_user_id'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
agentCidr: uniqueIndex('idx_ip_overrides_agent_cidr').on(t.agentId, t.cidr),
}),
)
export const agentStatsSamples = sqliteTable(
'agent_stats_samples',
{
id: text('id').primaryKey(),
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
packetsDropped: integer('packets_dropped').notNull().default(0),
packetsAccepted: integer('packets_accepted').notNull().default(0),
prefixCount: integer('prefix_count').notNull().default(0),
kernelMethod: text('kernel_method'),
recordedAt: text('recorded_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
agentTime: index('idx_agent_stats_agent_time').on(t.agentId, t.recordedAt),
}),
)
/** Per-IP/CIDR drop counters reported by Linux agents (nft/ipset element counters). */
export const agentIpBlockStats = sqliteTable(
'agent_ip_block_stats',
{
id: text('id').primaryKey(),
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
ip: text('ip').notNull(),
packets: integer('packets').notNull().default(0),
lastReportedPackets: integer('last_reported_packets').notNull().default(0),
firstSeenAt: text('first_seen_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
lastSeenAt: text('last_seen_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
agentIp: uniqueIndex('idx_agent_ip_block_stats_agent_ip').on(t.agentId, t.ip),
agentPackets: index('idx_agent_ip_block_stats_agent_packets').on(
t.agentId,
t.packets,
),
}),
)
/** Per-(ip, proto, dport) deny hits from Linux nft dynamic concat set. */
export const agentPortBlockStats = sqliteTable(
'agent_port_block_stats',
{
id: text('id').primaryKey(),
agentId: text('agent_id')
.notNull()
.references(() => agents.id, { onDelete: 'cascade' }),
ip: text('ip').notNull(),
port: integer('port').notNull(),
protocol: text('protocol').notNull(), // tcp | udp
packets: integer('packets').notNull().default(0),
lastReportedPackets: integer('last_reported_packets').notNull().default(0),
firstSeenAt: text('first_seen_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
lastSeenAt: text('last_seen_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
agentIpPortProto: uniqueIndex(
'idx_agent_port_block_stats_agent_ip_port_proto',
).on(t.agentId, t.ip, t.port, t.protocol),
agentPackets: index('idx_agent_port_block_stats_agent_packets').on(
t.agentId,
t.packets,
),
agentPortProto: index('idx_agent_port_block_stats_agent_port_proto').on(
t.agentId,
t.port,
t.protocol,
),
}),
)
/** Short install invite links (`/agent-install/:id` and `/:slug`). */
export const agentInstallLinks = sqliteTable(
'agent_install_links',
{
id: text('id').primaryKey(),
slug: text('slug').notNull(),
clientName: text('client_name').notNull(),
platform: text('platform').notNull().default('linux'), // linux | mikrotik
agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
revokedAt: text('revoked_at'),
lastUsedAt: text('last_used_at'),
useCount: integer('use_count').notNull().default(0),
},
(t) => ({
slugIdx: uniqueIndex('idx_agent_install_links_slug').on(t.slug),
agentIdx: index('idx_agent_install_links_agent').on(t.agentId),
}),
)
export const auditLog = sqliteTable(
'audit_log',
{
id: text('id').primaryKey(),
eventId: text('event_id'),
sourceApp: text('source_app').notNull().default('fw'),
action: text('action').notNull(),
severity: text('severity').notNull().default('info'),
actorUserId: text('actor_user_id'),
actorEmail: text('actor_email'),
actorName: text('actor_name'),
targetType: text('target_type'),
targetId: text('target_id'),
summary: text('summary').notNull(),
detailsJson: text('details_json'),
ip: text('ip'),
createdAt: text('created_at')
.notNull()
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
},
(t) => ({
eventIdIdx: uniqueIndex('idx_audit_log_event_id').on(t.eventId),
createdAtIdx: index('idx_audit_log_created_at').on(t.createdAt),
actionIdx: index('idx_audit_log_action').on(t.action),
}),
)
export const SHARED_POLICY_SET_ID = 'set-shared-default'
export const schema = {
settings,
agents,
ipLists,
ipListEntries,
policySets,
agentPolicySets,
policyRules,
policyRuleResolved,
ipOverrides,
agentStatsSamples,
agentIpBlockStats,
agentPortBlockStats,
agentInstallLinks,
auditLog,
}