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>
334 lines
11 KiB
TypeScript
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,
|
|
}
|