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
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
315 lines
11 KiB
TypeScript
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(),
|
|
});
|