Files
hammer-queue/backend/src/db/schema.ts
Hammer 061618cfab
Some checks failed
CI/CD / test (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
Security Scan / SAST - Semgrep (push) Has been cancelled
Security Scan / Dependency Scan - Trivy (push) Has been cancelled
Security Scan / Secret Detection - Gitleaks (push) Has been cancelled
feat: comprehensive security audit system - OWASP Top 10, checklist, score history, scan pipeline
Phase 1: OWASP API Top 10 per API with real findings from code inspection
- Hammer Dashboard, Network App, Todo App, nKode all audited against 10 OWASP risks
- Per-API scorecards with visual grid, color-coded by status

Phase 2: Full security checklist
- 9 categories: Auth, Authz, Input Validation, Transport, Rate Limiting, etc
- Interactive checklist UI with click-to-cycle status
- Per-project checklist with progress tracking
- Comprehensive category audits (Auth, Data Protection, Logging, Infrastructure, etc)

Phase 3: Automated pipeline
- Semgrep SAST, Trivy dependency scan, Gitleaks secret detection
- Gitea Actions CI workflow (security-scan.yml)
- Scan results stored in DB and displayed in dashboard

Phase 4: Dashboard polish
- Overall security posture score with weighted calculation
- Score trend charts (SVG) with 7-day history
- Critical findings highlight section
- Score history snapshots API
- Tab-based navigation (Overview, Checklist, per-project)

New DB tables: security_score_history, security_checklist, security_scan_results
Seed data populated from real code review of all repos
2026-01-30 15:16:10 +00:00

315 lines
11 KiB
TypeScript

import {
pgTable,
uuid,
text,
integer,
timestamp,
jsonb,
pgEnum,
boolean,
} from "drizzle-orm/pg-core";
export const taskStatusEnum = pgEnum("task_status", [
"active",
"queued",
"blocked",
"completed",
"cancelled",
]);
export const taskPriorityEnum = pgEnum("task_priority", [
"critical",
"high",
"medium",
"low",
]);
export const taskSourceEnum = pgEnum("task_source", [
"donovan",
"david",
"hammer",
"heartbeat",
"cron",
"other",
]);
export interface ProgressNote {
timestamp: string;
note: string;
}
export interface Subtask {
id: string;
title: string;
completed: boolean;
completedAt?: string;
createdAt: string;
}
export type RecurrenceFrequency = "daily" | "weekly" | "biweekly" | "monthly";
export interface Recurrence {
frequency: RecurrenceFrequency;
/** Auto-activate the next instance (vs. queue it) */
autoActivate?: boolean;
}
// ─── Projects ───
export interface ProjectLink {
label: string;
url: string;
}
export const projects = pgTable("projects", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
description: text("description"),
context: text("context"), // Architecture notes, how-to, credentials references
repos: jsonb("repos").$type<string[]>().default([]), // Git repo URLs
links: jsonb("links").$type<ProjectLink[]>().default([]), // Related URLs (docs, domains, dashboards)
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
// ─── Tasks ───
export const tasks = pgTable("tasks", {
id: uuid("id").defaultRandom().primaryKey(),
taskNumber: integer("task_number"),
title: text("title").notNull(),
description: text("description"),
source: taskSourceEnum("source").notNull().default("donovan"),
status: taskStatusEnum("status").notNull().default("queued"),
priority: taskPriorityEnum("priority").notNull().default("medium"),
position: integer("position").notNull().default(0),
assigneeId: text("assignee_id"),
assigneeName: text("assignee_name"),
projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
dueDate: timestamp("due_date", { withTimezone: true }),
estimatedHours: integer("estimated_hours"),
tags: jsonb("tags").$type<string[]>().default([]),
recurrence: jsonb("recurrence").$type<Recurrence>(),
subtasks: jsonb("subtasks").$type<Subtask[]>().default([]),
progressNotes: jsonb("progress_notes").$type<ProgressNote[]>().default([]),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
completedAt: timestamp("completed_at", { withTimezone: true }),
});
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
// ─── Comments ───
export const taskComments = pgTable("task_comments", {
id: uuid("id").defaultRandom().primaryKey(),
taskId: uuid("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
authorId: text("author_id"), // BetterAuth user ID, or "hammer" for API, null for anonymous
authorName: text("author_name").notNull(),
content: text("content").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type TaskComment = typeof taskComments.$inferSelect;
export type NewTaskComment = typeof taskComments.$inferInsert;
// ─── Security Audits ───
export const securityAuditStatusEnum = pgEnum("security_audit_status", [
"strong",
"needs_improvement",
"critical",
]);
export interface SecurityFinding {
id: string;
status: "strong" | "needs_improvement" | "critical";
title: string;
description: string;
recommendation: string;
taskId?: string;
}
export const securityAudits = pgTable("security_audits", {
id: uuid("id").defaultRandom().primaryKey(),
projectName: text("project_name").notNull(),
category: text("category").notNull(),
findings: jsonb("findings").$type<SecurityFinding[]>().default([]),
score: integer("score").notNull().default(0), // 0-100
lastAudited: timestamp("last_audited", { withTimezone: true }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SecurityAudit = typeof securityAudits.$inferSelect;
export type NewSecurityAudit = typeof securityAudits.$inferInsert;
// ─── Security Score History ───
export const securityScoreHistory = pgTable("security_score_history", {
id: uuid("id").defaultRandom().primaryKey(),
projectName: text("project_name").notNull(),
score: integer("score").notNull(),
totalFindings: integer("total_findings").notNull().default(0),
criticalCount: integer("critical_count").notNull().default(0),
warningCount: integer("warning_count").notNull().default(0),
strongCount: integer("strong_count").notNull().default(0),
recordedAt: timestamp("recorded_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SecurityScoreHistory = typeof securityScoreHistory.$inferSelect;
// ─── Security Checklist ───
export const securityChecklistStatusEnum = pgEnum("security_checklist_status", [
"pass",
"fail",
"partial",
"not_applicable",
"not_checked",
]);
export const securityChecklist = pgTable("security_checklist", {
id: uuid("id").defaultRandom().primaryKey(),
projectName: text("project_name").notNull(),
category: text("checklist_category").notNull(),
item: text("item").notNull(),
status: securityChecklistStatusEnum("status").notNull().default("not_checked"),
notes: text("notes"),
checkedBy: text("checked_by"),
checkedAt: timestamp("checked_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SecurityChecklistItem = typeof securityChecklist.$inferSelect;
export type NewSecurityChecklistItem = typeof securityChecklist.$inferInsert;
// ─── Security Scan Results ───
export const securityScanResults = pgTable("security_scan_results", {
id: uuid("id").defaultRandom().primaryKey(),
projectName: text("project_name").notNull(),
scanType: text("scan_type").notNull(), // semgrep, trivy, gitleaks
status: text("scan_status").notNull().default("pending"), // pending, running, completed, failed
findings: jsonb("findings").$type<any[]>().default([]),
summary: jsonb("summary").$type<Record<string, any>>().default({}),
triggeredBy: text("triggered_by"), // ci, manual
commitSha: text("commit_sha"),
branch: text("branch"),
duration: integer("duration"), // seconds
startedAt: timestamp("started_at", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SecurityScanResult = typeof securityScanResults.$inferSelect;
// ─── Daily Summaries ───
export interface SummaryHighlight {
text: string;
}
export interface SummaryStats {
deploys?: number;
commits?: number;
tasksCompleted?: number;
featuresBuilt?: number;
bugsFixed?: number;
[key: string]: number | undefined;
}
export const dailySummaries = pgTable("daily_summaries", {
id: uuid("id").defaultRandom().primaryKey(),
date: text("date").notNull().unique(), // YYYY-MM-DD
content: text("content").notNull(),
highlights: jsonb("highlights").$type<SummaryHighlight[]>().default([]),
stats: jsonb("stats").$type<SummaryStats>().default({}),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type DailySummary = typeof dailySummaries.$inferSelect;
export type NewDailySummary = typeof dailySummaries.$inferInsert;
// ─── Personal Todos ───
export const todoPriorityEnum = pgEnum("todo_priority", [
"high",
"medium",
"low",
"none",
]);
export const todos = pgTable("todos", {
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id").notNull(),
title: text("title").notNull(),
description: text("description"),
isCompleted: boolean("is_completed").notNull().default(false),
priority: todoPriorityEnum("priority").notNull().default("none"),
category: text("category"),
dueDate: timestamp("due_date", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
sortOrder: integer("sort_order").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export type Todo = typeof todos.$inferSelect;
export type NewTodo = typeof todos.$inferInsert;
// ─── BetterAuth tables ───
export const users = pgTable("users", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
role: text("role").notNull().default("user"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id").notNull().references(() => users.id),
});
export const accounts = pgTable("accounts", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id").notNull().references(() => users.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true }),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { withTimezone: true }),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const verifications = pgTable("verifications", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});