Add USB storage device control feature
- Add USB policy fields to machines schema (policy, status, error) - Create usbPolicyEvents table for audit logging - Implement Convex mutations/queries for USB policy management - Add REST API endpoints for desktop agent communication - Create Rust usb_control module for Windows registry manipulation - Integrate USB policy check in agent heartbeat loop - Add USB policy control component in admin device overview - Add localhost:3001 to auth trustedOrigins for dev 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0e9310d6e4
commit
49aa143a80
11 changed files with 1116 additions and 1 deletions
|
|
@ -637,6 +637,11 @@ export default defineSchema({
|
|||
updatedAt: v.number(),
|
||||
registeredBy: v.optional(v.string()),
|
||||
remoteAccess: v.optional(v.any()),
|
||||
usbPolicy: v.optional(v.string()), // ALLOW | BLOCK_ALL | READONLY
|
||||
usbPolicyAppliedAt: v.optional(v.number()),
|
||||
usbPolicyStatus: v.optional(v.string()), // PENDING | APPLIED | FAILED
|
||||
usbPolicyError: v.optional(v.string()),
|
||||
usbPolicyReportedAt: v.optional(v.number()),
|
||||
})
|
||||
.index("by_tenant", ["tenantId"])
|
||||
.index("by_tenant_company", ["tenantId", "companyId"])
|
||||
|
|
@ -644,6 +649,23 @@ export default defineSchema({
|
|||
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
|
||||
.index("by_auth_email", ["authEmail"]),
|
||||
|
||||
usbPolicyEvents: defineTable({
|
||||
tenantId: v.string(),
|
||||
machineId: v.id("machines"),
|
||||
actorId: v.optional(v.id("users")),
|
||||
actorEmail: v.optional(v.string()),
|
||||
actorName: v.optional(v.string()),
|
||||
oldPolicy: v.optional(v.string()),
|
||||
newPolicy: v.string(),
|
||||
status: v.string(), // PENDING | APPLIED | FAILED
|
||||
error: v.optional(v.string()),
|
||||
appliedAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_machine", ["machineId"])
|
||||
.index("by_machine_created", ["machineId", "createdAt"])
|
||||
.index("by_tenant_created", ["tenantId", "createdAt"]),
|
||||
|
||||
machineAlerts: defineTable({
|
||||
tenantId: v.string(),
|
||||
machineId: v.id("machines"),
|
||||
|
|
|
|||
251
convex/usbPolicy.ts
Normal file
251
convex/usbPolicy.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { v } from "convex/values"
|
||||
import { mutation, query } from "./_generated/server"
|
||||
import type { Id, Doc } from "./_generated/dataModel"
|
||||
|
||||
const DEFAULT_TENANT_ID = "default"
|
||||
|
||||
export const USB_POLICY_VALUES = ["ALLOW", "BLOCK_ALL", "READONLY"] as const
|
||||
export type UsbPolicyValue = (typeof USB_POLICY_VALUES)[number]
|
||||
|
||||
export const USB_POLICY_STATUS = ["PENDING", "APPLIED", "FAILED"] as const
|
||||
export type UsbPolicyStatus = (typeof USB_POLICY_STATUS)[number]
|
||||
|
||||
export const setUsbPolicy = mutation({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
policy: v.string(),
|
||||
actorId: v.optional(v.id("users")),
|
||||
actorEmail: v.optional(v.string()),
|
||||
actorName: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const machine = await ctx.db.get(args.machineId)
|
||||
if (!machine) {
|
||||
throw new Error("Dispositivo nao encontrado")
|
||||
}
|
||||
|
||||
if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
|
||||
throw new Error(`Politica USB invalida: ${args.policy}. Valores validos: ${USB_POLICY_VALUES.join(", ")}`)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const oldPolicy = machine.usbPolicy ?? "ALLOW"
|
||||
|
||||
await ctx.db.patch(args.machineId, {
|
||||
usbPolicy: args.policy,
|
||||
usbPolicyStatus: "PENDING",
|
||||
usbPolicyError: undefined,
|
||||
usbPolicyAppliedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
await ctx.db.insert("usbPolicyEvents", {
|
||||
tenantId: machine.tenantId,
|
||||
machineId: args.machineId,
|
||||
actorId: args.actorId,
|
||||
actorEmail: args.actorEmail,
|
||||
actorName: args.actorName,
|
||||
oldPolicy,
|
||||
newPolicy: args.policy,
|
||||
status: "PENDING",
|
||||
createdAt: now,
|
||||
})
|
||||
|
||||
return { ok: true, policy: args.policy, status: "PENDING" }
|
||||
},
|
||||
})
|
||||
|
||||
export const reportUsbPolicyStatus = mutation({
|
||||
args: {
|
||||
machineToken: v.string(),
|
||||
status: v.string(),
|
||||
error: v.optional(v.string()),
|
||||
currentPolicy: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const tokenHash = args.machineToken
|
||||
|
||||
const tokenRecord = await ctx.db
|
||||
.query("machineTokens")
|
||||
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
|
||||
.first()
|
||||
|
||||
if (!tokenRecord || tokenRecord.revoked) {
|
||||
throw new Error("Token de maquina invalido ou revogado")
|
||||
}
|
||||
|
||||
if (tokenRecord.expiresAt < Date.now()) {
|
||||
throw new Error("Token de maquina expirado")
|
||||
}
|
||||
|
||||
const machine = await ctx.db.get(tokenRecord.machineId)
|
||||
if (!machine) {
|
||||
throw new Error("Dispositivo nao encontrado")
|
||||
}
|
||||
|
||||
if (!USB_POLICY_STATUS.includes(args.status as UsbPolicyStatus)) {
|
||||
throw new Error(`Status de politica USB invalido: ${args.status}`)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
await ctx.db.patch(machine._id, {
|
||||
usbPolicyStatus: args.status,
|
||||
usbPolicyError: args.error,
|
||||
usbPolicyReportedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
const latestEvent = await ctx.db
|
||||
.query("usbPolicyEvents")
|
||||
.withIndex("by_machine_created", (q) => q.eq("machineId", machine._id))
|
||||
.order("desc")
|
||||
.first()
|
||||
|
||||
if (latestEvent && latestEvent.status === "PENDING") {
|
||||
await ctx.db.patch(latestEvent._id, {
|
||||
status: args.status,
|
||||
error: args.error,
|
||||
appliedAt: args.status === "APPLIED" ? now : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
},
|
||||
})
|
||||
|
||||
export const getUsbPolicy = query({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const machine = await ctx.db.get(args.machineId)
|
||||
if (!machine) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
policy: machine.usbPolicy ?? "ALLOW",
|
||||
status: machine.usbPolicyStatus ?? null,
|
||||
error: machine.usbPolicyError ?? null,
|
||||
appliedAt: machine.usbPolicyAppliedAt ?? null,
|
||||
reportedAt: machine.usbPolicyReportedAt ?? null,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const getPendingUsbPolicy = query({
|
||||
args: {
|
||||
machineToken: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const tokenHash = args.machineToken
|
||||
|
||||
const tokenRecord = await ctx.db
|
||||
.query("machineTokens")
|
||||
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
|
||||
.first()
|
||||
|
||||
if (!tokenRecord || tokenRecord.revoked || tokenRecord.expiresAt < Date.now()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const machine = await ctx.db.get(tokenRecord.machineId)
|
||||
if (!machine) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (machine.usbPolicyStatus === "PENDING") {
|
||||
return {
|
||||
policy: machine.usbPolicy ?? "ALLOW",
|
||||
appliedAt: machine.usbPolicyAppliedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
export const listUsbPolicyEvents = query({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const limit = args.limit ?? 50
|
||||
|
||||
const events = await ctx.db
|
||||
.query("usbPolicyEvents")
|
||||
.withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId))
|
||||
.order("desc")
|
||||
.take(limit)
|
||||
|
||||
return events.map((event) => ({
|
||||
id: event._id,
|
||||
oldPolicy: event.oldPolicy,
|
||||
newPolicy: event.newPolicy,
|
||||
status: event.status,
|
||||
error: event.error,
|
||||
actorEmail: event.actorEmail,
|
||||
actorName: event.actorName,
|
||||
createdAt: event.createdAt,
|
||||
appliedAt: event.appliedAt,
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
export const bulkSetUsbPolicy = mutation({
|
||||
args: {
|
||||
machineIds: v.array(v.id("machines")),
|
||||
policy: v.string(),
|
||||
actorId: v.optional(v.id("users")),
|
||||
actorEmail: v.optional(v.string()),
|
||||
actorName: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
|
||||
throw new Error(`Politica USB invalida: ${args.policy}`)
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const results: Array<{ machineId: Id<"machines">; success: boolean; error?: string }> = []
|
||||
|
||||
for (const machineId of args.machineIds) {
|
||||
try {
|
||||
const machine = await ctx.db.get(machineId)
|
||||
if (!machine) {
|
||||
results.push({ machineId, success: false, error: "Dispositivo nao encontrado" })
|
||||
continue
|
||||
}
|
||||
|
||||
const oldPolicy = machine.usbPolicy ?? "ALLOW"
|
||||
|
||||
await ctx.db.patch(machineId, {
|
||||
usbPolicy: args.policy,
|
||||
usbPolicyStatus: "PENDING",
|
||||
usbPolicyError: undefined,
|
||||
usbPolicyAppliedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
await ctx.db.insert("usbPolicyEvents", {
|
||||
tenantId: machine.tenantId,
|
||||
machineId,
|
||||
actorId: args.actorId,
|
||||
actorEmail: args.actorEmail,
|
||||
actorName: args.actorName,
|
||||
oldPolicy,
|
||||
newPolicy: args.policy,
|
||||
status: "PENDING",
|
||||
createdAt: now,
|
||||
})
|
||||
|
||||
results.push({ machineId, success: true })
|
||||
} catch (err) {
|
||||
results.push({ machineId, success: false, error: String(err) })
|
||||
}
|
||||
}
|
||||
|
||||
return { results, total: args.machineIds.length, successful: results.filter((r) => r.success).length }
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue