Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions data/compliance-audits.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
[
{
"id": "audit-001",
"framework": "GDPR",
"controlId": "GDPR-ART30",
"controlName": "Records of Processing Activities",
"status": "pass",
"evidence": "https://foragents.dev/evidence/gdpr-art30-records",
"auditor": "Maya Patel",
"auditDate": "2026-01-10",
"nextReviewDate": "2026-07-10",
"notes": "Processing records are complete and updated quarterly."
},
{
"id": "audit-002",
"framework": "GDPR",
"controlId": "GDPR-ART32",
"controlName": "Security of Processing",
"status": "partial",
"evidence": "https://foragents.dev/evidence/gdpr-art32-security",
"auditor": "Maya Patel",
"auditDate": "2026-01-12",
"nextReviewDate": "2026-04-12",
"notes": "Encryption at rest is enabled; key rotation policy needs formal sign-off."
},
{
"id": "audit-003",
"framework": "SOC2",
"controlId": "SOC2-CC6.1",
"controlName": "Logical and Physical Access Controls",
"status": "pass",
"evidence": "https://foragents.dev/evidence/soc2-cc6-1-access",
"auditor": "Jonah Wu",
"auditDate": "2026-01-15",
"nextReviewDate": "2026-07-15",
"notes": "RBAC and MFA enforcement verified for production systems."
},
{
"id": "audit-004",
"framework": "SOC2",
"controlId": "SOC2-CC7.2",
"controlName": "Monitoring for Security Events",
"status": "fail",
"evidence": "https://foragents.dev/evidence/soc2-cc7-2-monitoring",
"auditor": "Jonah Wu",
"auditDate": "2026-01-17",
"nextReviewDate": "2026-03-01",
"notes": "Alert escalation runbook is missing after-hours ownership definitions."
},
{
"id": "audit-005",
"framework": "HIPAA",
"controlId": "HIPAA-164.312(a)(1)",
"controlName": "Access Control",
"status": "pass",
"evidence": "https://foragents.dev/evidence/hipaa-access-control",
"auditor": "Elena Rossi",
"auditDate": "2026-01-20",
"nextReviewDate": "2026-07-20",
"notes": "Unique user identification and emergency access procedures documented."
},
{
"id": "audit-006",
"framework": "HIPAA",
"controlId": "HIPAA-164.312(b)",
"controlName": "Audit Controls",
"status": "na",
"evidence": "https://foragents.dev/evidence/hipaa-audit-controls",
"auditor": "Elena Rossi",
"auditDate": "2026-01-20",
"nextReviewDate": "2026-07-20",
"notes": "Control marked not applicable for this service boundary; covered by upstream provider."
},
{
"id": "audit-007",
"framework": "ISO27001",
"controlId": "ISO-A.12.6.1",
"controlName": "Management of Technical Vulnerabilities",
"status": "partial",
"evidence": "https://foragents.dev/evidence/iso-a-12-6-1-vuln",
"auditor": "Derrick Cole",
"auditDate": "2026-01-24",
"nextReviewDate": "2026-04-24",
"notes": "Monthly scanning is in place; remediation SLA tracking is inconsistent."
},
{
"id": "audit-008",
"framework": "ISO27001",
"controlId": "ISO-A.16.1.5",
"controlName": "Response to Information Security Incidents",
"status": "pass",
"evidence": "https://foragents.dev/evidence/iso-a-16-1-5-response",
"auditor": "Derrick Cole",
"auditDate": "2026-01-24",
"nextReviewDate": "2026-07-24",
"notes": "Incident response workflow tested in tabletop exercise with documented outcomes."
}
]
255 changes: 255 additions & 0 deletions src/app/api/compliance/audit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { promises as fs } from "fs";
import path from "path";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const AUDIT_DATA_PATH = path.join(process.cwd(), "data", "compliance-audits.json");
const FRAMEWORKS = ["GDPR", "SOC2", "HIPAA", "ISO27001"] as const;
const STATUSES = ["pass", "fail", "partial", "na"] as const;

type AuditFramework = (typeof FRAMEWORKS)[number];
type AuditStatus = (typeof STATUSES)[number];

interface ComplianceAudit {
id: string;
framework: AuditFramework;
controlId: string;
controlName: string;
status: AuditStatus;
evidence: string;
auditor: string;
auditDate: string;
nextReviewDate: string;
notes: string;
}

interface FrameworkScore {
framework: AuditFramework;
score: number;
passed: number;
partial: number;
failed: number;
notApplicable: number;
totalApplicable: number;
}

function isAuditFramework(value: unknown): value is AuditFramework {
return typeof value === "string" && FRAMEWORKS.includes(value as AuditFramework);
}

function isAuditStatus(value: unknown): value is AuditStatus {
return typeof value === "string" && STATUSES.includes(value as AuditStatus);
}

function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}

function isValidDateString(value: unknown): value is string {
if (!isNonEmptyString(value)) {
return false;
}

return !Number.isNaN(new Date(value).getTime());
}

function isComplianceAudit(value: unknown): value is ComplianceAudit {
if (!value || typeof value !== "object") {
return false;
}

const entry = value as Record<string, unknown>;

return (
isNonEmptyString(entry.id) &&
isAuditFramework(entry.framework) &&
isNonEmptyString(entry.controlId) &&
isNonEmptyString(entry.controlName) &&
isAuditStatus(entry.status) &&
isNonEmptyString(entry.evidence) &&
isNonEmptyString(entry.auditor) &&
isValidDateString(entry.auditDate) &&
isValidDateString(entry.nextReviewDate) &&
isNonEmptyString(entry.notes)
);
}

async function readAuditData(): Promise<ComplianceAudit[]> {
const raw = await fs.readFile(AUDIT_DATA_PATH, "utf-8");
const parsed = JSON.parse(raw) as unknown;

if (!Array.isArray(parsed) || !parsed.every(isComplianceAudit)) {
throw new Error("Invalid compliance audit dataset");
}

return parsed;
}

async function writeAuditData(audits: ComplianceAudit[]): Promise<void> {
await fs.writeFile(AUDIT_DATA_PATH, JSON.stringify(audits, null, 2));
}

function formatDateOnly(date: Date): string {
return date.toISOString().slice(0, 10);
}

function nextAuditId(audits: ComplianceAudit[]): string {
const maxId = audits.reduce((max, audit) => {
const parts = audit.id.match(/audit-(\d+)/i);
if (!parts) {
return max;
}

const value = Number(parts[1]);
return Number.isFinite(value) ? Math.max(max, value) : max;
}, 0);

return `audit-${String(maxId + 1).padStart(3, "0")}`;
}

function calculateScores(audits: ComplianceAudit[]): FrameworkScore[] {
return FRAMEWORKS.map((framework) => {
const frameworkAudits = audits.filter((audit) => audit.framework === framework);

const passed = frameworkAudits.filter((audit) => audit.status === "pass").length;
const partial = frameworkAudits.filter((audit) => audit.status === "partial").length;
const failed = frameworkAudits.filter((audit) => audit.status === "fail").length;
const notApplicable = frameworkAudits.filter((audit) => audit.status === "na").length;

const totalApplicable = passed + partial + failed;
const weightedPass = passed + partial * 0.5;
const score = totalApplicable > 0 ? Math.round((weightedPass / totalApplicable) * 100) : 0;

return {
framework,
score,
passed,
partial,
failed,
notApplicable,
totalApplicable,
};
});
}

function matchesSearch(audit: ComplianceAudit, searchTerm: string): boolean {
if (!searchTerm) {
return true;
}

const haystack = [
audit.id,
audit.framework,
audit.controlId,
audit.controlName,
audit.evidence,
audit.auditor,
audit.notes,
]
.join(" ")
.toLowerCase();

return haystack.includes(searchTerm);
}

export async function GET(request: NextRequest) {
try {
const allAudits = await readAuditData();
const frameworkParam = request.nextUrl.searchParams.get("framework")?.trim().toUpperCase();
const statusParam = request.nextUrl.searchParams.get("status")?.trim().toLowerCase();
const search = request.nextUrl.searchParams.get("search")?.trim().toLowerCase() ?? "";

const filtered = allAudits.filter((audit) => {
const frameworkMatch = frameworkParam ? audit.framework === frameworkParam : true;
const statusMatch = statusParam ? audit.status === statusParam : true;
const searchMatch = matchesSearch(audit, search);
return frameworkMatch && statusMatch && searchMatch;
});

return NextResponse.json(
{
audits: filtered,
total: filtered.length,
frameworkScores: calculateScores(filtered),
},
{
headers: {
"Cache-Control": "no-store",
},
}
);
} catch {
return NextResponse.json({ error: "Unable to read compliance audits" }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as Record<string, unknown>;

const framework = typeof body.framework === "string" ? body.framework.toUpperCase() : "";
const status = typeof body.status === "string" ? body.status.toLowerCase() : "";

const auditDateInput = isNonEmptyString(body.auditDate) ? new Date(body.auditDate) : new Date();
const nextReviewInput = isNonEmptyString(body.nextReviewDate)
? new Date(body.nextReviewDate)
: new Date(auditDateInput.getTime() + 180 * 24 * 60 * 60 * 1000);

const validationErrors: string[] = [];

if (!isAuditFramework(framework)) validationErrors.push("framework must be GDPR, SOC2, HIPAA, or ISO27001");
if (!isAuditStatus(status)) validationErrors.push("status must be pass, fail, partial, or na");
if (!isNonEmptyString(body.controlId)) validationErrors.push("controlId is required");
if (!isNonEmptyString(body.controlName)) validationErrors.push("controlName is required");
if (!isNonEmptyString(body.evidence)) validationErrors.push("evidence is required");
if (!isNonEmptyString(body.auditor)) validationErrors.push("auditor is required");
if (!isNonEmptyString(body.notes)) validationErrors.push("notes is required");
if (Number.isNaN(auditDateInput.getTime())) validationErrors.push("auditDate is invalid");
if (Number.isNaN(nextReviewInput.getTime())) validationErrors.push("nextReviewDate is invalid");

if (validationErrors.length > 0) {
return NextResponse.json(
{
error: "Validation failed",
details: validationErrors,
},
{ status: 400 }
);
}

const audits = await readAuditData();
const controlId = (body.controlId as string).trim();
const controlName = (body.controlName as string).trim();
const evidence = (body.evidence as string).trim();
const auditor = (body.auditor as string).trim();
const notes = (body.notes as string).trim();

const newAudit: ComplianceAudit = {
id: nextAuditId(audits),
framework: framework as AuditFramework,
controlId,
controlName,
status: status as AuditStatus,
evidence,
auditor,
auditDate: formatDateOnly(auditDateInput),
nextReviewDate: formatDateOnly(nextReviewInput),
notes,
};

const updatedAudits = [...audits, newAudit];
await writeAuditData(updatedAudits);

return NextResponse.json(
{
audit: newAudit,
total: updatedAudits.length,
},
{ status: 201 }
);
} catch {
return NextResponse.json({ error: "Invalid request body. Expected JSON." }, { status: 400 });
}
}
Loading
Loading