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
235 changes: 121 additions & 114 deletions apps/app/src/actions/organization/lib/initialize-organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,61 +216,61 @@ export const _upsertOrgFrameworkStructureCore = async ({
);

if (policyTemplatesForCreation.length > 0) {
// Pre-generate Policy and PolicyVersion IDs in a single round-trip so we can
// skip the post-insert findMany lookups and the per-policy update loop.
// Policy.currentVersionId -> PolicyVersion.id and PolicyVersion.policyId ->
// Policy.id form an FK cycle, so we insert Policy first (currentVersionId null),
// insert PolicyVersion, then set currentVersionId in one bulk UPDATE.
const idPairs = await tx.$queryRaw<
Array<{ policy_id: string; version_id: string }>
>`
SELECT
generate_prefixed_cuid('pol'::text) AS policy_id,
generate_prefixed_cuid('pv'::text) AS version_id
FROM generate_series(1, ${policyTemplatesForCreation.length}::int)
`;
const preparedPolicies = policyTemplatesForCreation.map((template, i) => ({
template,
policyId: idPairs[i].policy_id,
versionId: idPairs[i].version_id,
contentArray: extractTipTapContentArray(template.content),
}));

await tx.policy.createMany({
data: policyTemplatesForCreation.map((policyTemplate) => {
const templateContent = policyTemplate.content;
const contentArray = extractTipTapContentArray(templateContent);
return {
name: policyTemplate.name,
description: policyTemplate.description,
department: policyTemplate.department,
frequency: policyTemplate.frequency,
content: { set: contentArray },
organizationId: organizationId,
policyTemplateId: policyTemplate.id,
};
}),
data: preparedPolicies.map(({ template, policyId, contentArray }) => ({
id: policyId,
name: template.name,
description: template.description,
department: template.department,
frequency: template.frequency,
content: { set: contentArray },
organizationId: organizationId,
policyTemplateId: template.id,
})),
});

// Fetch newly created policies to create versions for them
const newlyCreatedPolicies = await tx.policy.findMany({
where: {
organizationId: organizationId,
policyTemplateId: {
in: policyTemplatesForCreation.map((t) => t.id),
},
},
select: { id: true, policyTemplateId: true, content: true },
await tx.policyVersion.createMany({
data: preparedPolicies.map(({ policyId, versionId, contentArray }) => ({
id: versionId,
policyId,
version: 1,
content: { set: contentArray },
changelog: 'Initial version from template',
})),
});

// Create version 1 for each newly created policy
if (newlyCreatedPolicies.length > 0) {
await tx.policyVersion.createMany({
data: newlyCreatedPolicies.map((policy) => ({
policyId: policy.id,
version: 1,
content: { set: policy.content as Prisma.InputJsonValue[] },
changelog: 'Initial version from template',
})),
});

// Fetch the created versions to update policies with currentVersionId
const createdVersions = await tx.policyVersion.findMany({
where: {
policyId: { in: newlyCreatedPolicies.map((p) => p.id) },
version: 1,
},
select: { id: true, policyId: true },
});

// Update each policy with its currentVersionId
for (const version of createdVersions) {
await tx.policy.update({
where: { id: version.policyId },
data: { currentVersionId: version.id },
});
}
}
const currentVersionValues = Prisma.join(
preparedPolicies.map(
({ policyId, versionId }) =>
Prisma.sql`(${policyId}::text, ${versionId}::text)`,
),
);
await tx.$executeRaw`
UPDATE "Policy"
SET "currentVersionId" = v.version_id
FROM (VALUES ${currentVersionValues}) AS v(policy_id, version_id)
WHERE "Policy".id = v.policy_id
`;
}

/**
Expand Down Expand Up @@ -350,6 +350,8 @@ export const _upsertOrgFrameworkStructureCore = async ({
);

const requirementMapEntriesToCreate: Prisma.RequirementMapCreateManyInput[] = [];
const controlToPolicyPairs: Array<{ controlId: string; policyId: string }> = [];
const controlToTaskPairs: Array<{ controlId: string; taskId: string }> = [];

for (const controlTemplateRelation of groupedControlTemplateRelations) {
const newControlId = controlTemplateIdToInstanceIdMap.get(
Expand All @@ -363,81 +365,86 @@ export const _upsertOrgFrameworkStructureCore = async ({
continue;
}

const updateData: Prisma.ControlUpdateInput = {};
let needsUpdate = false;

// --- Process Requirements for RequirementMap ---
if (controlTemplateRelation.requirementTemplateIds.length > 0) {
for (const reqTemplateId of controlTemplateRelation.requirementTemplateIds) {
let frameworkEditorFrameworkIdForReq: string | undefined;
for (const fw of frameworkEditorFrameworks) {
if (fw.requirements.some((r) => r.id === reqTemplateId)) {
frameworkEditorFrameworkIdForReq = fw.id;
break;
}
}
const frameworkInstanceId = frameworkEditorFrameworkIdForReq
? editorFrameworkIdToInstanceIdMap.get(frameworkEditorFrameworkIdForReq)
: undefined;

if (frameworkInstanceId) {
requirementMapEntriesToCreate.push({
controlId: newControlId,
requirementId: reqTemplateId,
frameworkInstanceId: frameworkInstanceId,
});
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Could not find FrameworkInstanceId for editor requirement ID ${reqTemplateId}. Cannot create RequirementMap for Control ${newControlId}.`,
);
for (const reqTemplateId of controlTemplateRelation.requirementTemplateIds) {
let frameworkEditorFrameworkIdForReq: string | undefined;
for (const fw of frameworkEditorFrameworks) {
if (fw.requirements.some((r) => r.id === reqTemplateId)) {
frameworkEditorFrameworkIdForReq = fw.id;
break;
}
}
const frameworkInstanceId = frameworkEditorFrameworkIdForReq
? editorFrameworkIdToInstanceIdMap.get(frameworkEditorFrameworkIdForReq)
: undefined;

if (frameworkInstanceId) {
requirementMapEntriesToCreate.push({
controlId: newControlId,
requirementId: reqTemplateId,
frameworkInstanceId: frameworkInstanceId,
});
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Could not find FrameworkInstanceId for editor requirement ID ${reqTemplateId}. Cannot create RequirementMap for Control ${newControlId}.`,
);
}
}

// --- Connect Policies ---
if (controlTemplateRelation.policyTemplateIds.length > 0) {
const policiesToConnect = [];
for (const policyTemplateId of controlTemplateRelation.policyTemplateIds) {
const newPolicyId = policyTemplateIdToInstanceIdMap.get(policyTemplateId);
if (newPolicyId) {
policiesToConnect.push({ id: newPolicyId });
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Policy instance not found for template ID ${policyTemplateId}. Cannot connect to Control ${newControlId}.`,
);
}
}
if (policiesToConnect.length > 0) {
updateData.policies = { connect: policiesToConnect };
needsUpdate = true;
// --- Collect Control <-> Policy pairs ---
for (const policyTemplateId of controlTemplateRelation.policyTemplateIds) {
const newPolicyId = policyTemplateIdToInstanceIdMap.get(policyTemplateId);
if (newPolicyId) {
controlToPolicyPairs.push({ controlId: newControlId, policyId: newPolicyId });
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Policy instance not found for template ID ${policyTemplateId}. Cannot connect to Control ${newControlId}.`,
);
}
}

// --- Connect Tasks ---
if (controlTemplateRelation.taskTemplateIds.length > 0) {
const tasksToConnect = [];
for (const taskTemplateId of controlTemplateRelation.taskTemplateIds) {
const newTaskId = taskTemplateIdToInstanceIdMap.get(taskTemplateId);
if (newTaskId) {
tasksToConnect.push({ id: newTaskId });
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Task instance not found for template ID ${taskTemplateId}. Cannot connect to Control ${newControlId}.`,
);
}
}
if (tasksToConnect.length > 0) {
updateData.tasks = { connect: tasksToConnect };
needsUpdate = true;
// --- Collect Control <-> Task pairs ---
for (const taskTemplateId of controlTemplateRelation.taskTemplateIds) {
const newTaskId = taskTemplateIdToInstanceIdMap.get(taskTemplateId);
if (newTaskId) {
controlToTaskPairs.push({ controlId: newControlId, taskId: newTaskId });
} else {
console.warn(
`UpsertOrgFrameworkStructureCore: Task instance not found for template ID ${taskTemplateId}. Cannot connect to Control ${newControlId}.`,
);
}
}
}

if (needsUpdate) {
await tx.control.update({
where: { id: newControlId },
data: updateData,
});
}
// Bulk-insert into the implicit M2M join tables instead of N `control.update({ connect })`
// calls. ON CONFLICT DO NOTHING preserves the idempotency the connect loop provided for
// re-runs where some links already exist (e.g., adding a framework to an existing org).
if (controlToPolicyPairs.length > 0) {
const rows = Prisma.join(
controlToPolicyPairs.map(
({ controlId, policyId }) =>
Prisma.sql`(${controlId}::text, ${policyId}::text)`,
),
);
await tx.$executeRaw`
INSERT INTO "_ControlToPolicy" ("A", "B")
VALUES ${rows}
ON CONFLICT ("A", "B") DO NOTHING
`;
}

if (controlToTaskPairs.length > 0) {
const rows = Prisma.join(
controlToTaskPairs.map(
({ controlId, taskId }) =>
Prisma.sql`(${controlId}::text, ${taskId}::text)`,
),
);
await tx.$executeRaw`
INSERT INTO "_ControlToTask" ("A", "B")
VALUES ${rows}
ON CONFLICT ("A", "B") DO NOTHING
`;
}

// --- Create RequirementMap entries ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,18 @@ function CheckRunItem({
)}
</div>
</div>
{finding.evidence && Object.keys(finding.evidence).length > 0 && (
<details className="text-xs">
<summary className="text-muted-foreground cursor-pointer">
View Evidence
</summary>
<EvidenceJsonView
evidence={finding.evidence}
organizationName={organizationName}
automationName={run.checkName}
/>
</details>
)}
</div>
))}
{findings.length > 3 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@
{
"id": "frk_tt_6840796f77d8a0dff53f947a",
"name": "Secure Devices",
"description": "Ensure all devices meet the following security requirements:\n\n• Full disk encryption enabled (BitLocker for Windows, FileVault for macOS)\n\n• Screen lock enabled after 5 minutes of inactivity on macOS and 15 minutes on Windows\n\n• Minimum password length of 8+ characters\n\n• Automatic installation of security updates\n\n• Anti-virus enabled (Windows Defender on Windows or macOS XProtect)\n\nYou may verify compliance by uploading screenshots for each device or by installing the Comp AI device agent.\n\nIf you already use a third-party MDM provider (e.g., Jamf, Intune, Kandji, etc.), it is recommended that you do not install the Comp AI device agent, as running multiple device management agents may cause conflicts. In these cases, please upload screenshots instead.\n\nThe Comp AI device agent can be downloaded from: portal.trycomp.ai",
"description": "Ensure all devices meet the following security requirements:\n\n• Full disk encryption enabled (BitLocker for Windows, FileVault for macOS)\n\n• Screen lock enabled after 15 minutes of inactivity\n\n• Minimum password length of 8+ characters\n\n• Automatic installation of security updates\n\n• Anti-virus enabled (Windows Defender on Windows or macOS XProtect)\n\nYou may verify compliance by uploading screenshots for each device or by installing the Comp AI device agent.\n\nIf you already use a third-party MDM provider (e.g., Jamf, Intune, Kandji, etc.), it is recommended that you do not install the Comp AI device agent, as running multiple device management agents may cause conflicts. In these cases, please upload screenshots instead.\n\nThe Comp AI device agent can be downloaded from: portal.trycomp.ai",
"frequency": "yearly",
"department": "itsm",
"createdAt": "2025-06-04 16:50:54.671",
Expand Down
2 changes: 1 addition & 1 deletion packages/device-agent/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The agent runs four compliance checks every hour:
1. **Disk Encryption** -- FileVault (macOS), BitLocker (Windows), LUKS (Linux)
2. **Antivirus** -- XProtect (macOS), Windows Defender (Windows), ClamAV/AppArmor/SELinux (Linux)
3. **Password Policy** -- Minimum 8-character password enforced at OS level
4. **Screen Lock** -- Automatic screen lock within 5 minutes of inactivity
4. **Screen Lock** -- Automatic screen lock within 15 minutes of inactivity

A device is **compliant** when all four checks pass.

Expand Down
6 changes: 3 additions & 3 deletions packages/device-agent/src/checks/linux/screen-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { execSync } from 'node:child_process';
import type { CheckResult } from '../../shared/types';
import type { ComplianceCheck } from '../types';

const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
const MAX_IDLE_TIME_SECONDS = 900; // 15 minutes

/**
* Checks if screen lock is enabled and set to 5 minutes or less on Linux.
* Checks if screen lock is enabled and set to 15 minutes or less on Linux.
*
* Detection methods:
* 1. GNOME: gsettings for org.gnome.desktop.session idle-delay and
Expand All @@ -16,7 +16,7 @@ const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
*/
export class LinuxScreenLockCheck implements ComplianceCheck {
checkType = 'screen_lock' as const;
displayName = 'Screen Lock (5 min or less)';
displayName = 'Screen Lock (15 min or less)';

async run(): Promise<CheckResult> {
try {
Expand Down
6 changes: 3 additions & 3 deletions packages/device-agent/src/checks/macos/screen-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { execSync } from 'node:child_process';
import type { CheckResult } from '../../shared/types';
import type { ComplianceCheck } from '../types';

const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
const MAX_IDLE_TIME_SECONDS = 900; // 15 minutes

/**
* Checks if screen lock is enabled and set to 5 minutes or less on macOS.
* Checks if screen lock is enabled and set to 15 minutes or less on macOS.
*
* Checks two settings:
* 1. Screen saver idle time (how long before screen saver activates)
Expand All @@ -20,7 +20,7 @@ const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
*/
export class MacOSScreenLockCheck implements ComplianceCheck {
checkType = 'screen_lock' as const;
displayName = 'Screen Lock (5 min or less)';
displayName = 'Screen Lock (15 min or less)';

async run(): Promise<CheckResult> {
try {
Expand Down
6 changes: 3 additions & 3 deletions packages/device-agent/src/checks/windows/screen-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { execSync } from 'node:child_process';
import type { CheckResult } from '../../shared/types';
import type { ComplianceCheck } from '../types';

const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
const MAX_IDLE_TIME_SECONDS = 900; // 15 minutes

/**
* Checks if screen lock is enabled and set to 5 minutes or less on Windows.
* Checks if screen lock is enabled and set to 15 minutes or less on Windows.
*
* Checks:
* 1. Screen saver timeout (ScreenSaveTimeOut registry key)
Expand All @@ -14,7 +14,7 @@ const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes
*/
export class WindowsScreenLockCheck implements ComplianceCheck {
checkType = 'screen_lock' as const;
displayName = 'Screen Lock (5 min or less)';
displayName = 'Screen Lock (15 min or less)';

async run(): Promise<CheckResult> {
try {
Expand Down
Loading
Loading