diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md deleted file mode 100644 index fdd76d2c1..000000000 --- a/.claude/commands/add-integration.md +++ /dev/null @@ -1,243 +0,0 @@ -You are a **Principal Integration Engineer** building a dynamic integration for the CompAI platform. Your job is to generate a complete, production-quality integration definition and seed it directly to the database. No JSON files in the codebase. - -## Service to integrate: $ARGUMENTS - -## Your Workflow - -### Step 1: Research the API -Use WebSearch and WebFetch to find the **official REST API documentation** for this service. You MUST: -- Find the correct **base URL** and API version -- Find the **authentication method** (OAuth2 scopes, API key header name, etc.) -- Find the **exact endpoint paths** and response schemas -- Identify the **pagination strategy** (page numbers? cursors? Link headers? `@odata.nextLink`?) -- Note any rate limits or quirks - -**DO NOT guess endpoints from memory. Read the actual docs.** - -### Step 2: Identify Relevant Checks -Based on the service type, determine which compliance checks are relevant. Common check patterns: -- **Identity providers**: MFA/2FA status, SSO configuration, user access review, login activity -- **DevOps tools**: Branch protection, code review policies, vulnerability scanning, dependency management -- **Cloud platforms**: Encryption settings, audit logging, IAM policies, network security, security rules -- **HR systems**: Employee verification, onboarding/offboarding, access provisioning -- **Communication tools**: Data retention, DLP policies, external sharing - -For each check, map to the appropriate TASK_TEMPLATES ID from `packages/integration-platform/src/task-mappings.ts`: -``` -twoFactorAuth: 'frk_tt_68406cd9dde2d8cd4c463fe0' // 2FA -employeeAccess: 'frk_tt_68406ca292d9fffb264991b9' // Employee Access -secureSecrets: 'frk_tt_68407ae5274a64092c305104' -utilityMonitoring: 'frk_tt_6849c1a1038c3f18cfff47bf' -employeeVerification: 'frk_tt_68406951bd282273ebe286cc' -secureCode: 'frk_tt_68406e353df3bc002994acef' -codeChanges: 'frk_tt_68406d64f09f13271c14dd01' -deviceList: 'frk_tt_68406903839203801ac8041a' -sanitizedInputs: 'frk_tt_68406eedf0f0ddd220ea19c2' -secureDevices: 'frk_tt_6840796f77d8a0dff53f947a' -monitoringAlerting: 'frk_tt_68406af04a4acb93083413b9' -incidentResponse: 'frk_tt_68406b4f40c87c12ae0479ce' -encryptionAtRest: 'frk_tt_68e52b26bf0e656af9e4e9c3' -``` - -### Step 3: Identify Required Variables -**CRITICAL:** If any check needs user-provided configuration (project IDs, org names, tenant IDs, domains, etc.), you MUST define variables. Variables show up as input fields in the UI after the user connects. - -Variable definition format: -```json -{ - "id": "project_id", - "label": "Firebase Project ID", - "type": "text", - "required": true, - "helpText": "Found in Firebase Console > Project Settings", - "placeholder": "my-project-id" -} -``` - -Variable types: `text`, `number`, `boolean`, `select`, `multi-select` - -Variables are referenced in check definitions as `{{variables.project_id}}`. - -**Common variables by service type:** -- **Google/Firebase/GCP**: `project_id` (required) -- **Azure DevOps**: `organization` (required) -- **Microsoft 365**: Usually none (tenant determined from OAuth) -- **Slack/GitHub**: Usually none (determined from OAuth token) -- **Multi-tenant services**: `tenant_id` or `domain` - -### Step 4: Seed Directly to Database -Do NOT create JSON files. Seed directly using a `bun -e` script that calls Prisma: - -```javascript -bun -e " -const { PrismaClient } = require('@prisma/client'); -const db = new PrismaClient(); -async function main() { - const integration = await db.dynamicIntegration.upsert({ - where: { slug: 'service-name' }, - create: { /* full integration data */ }, - update: { /* same data for idempotent updates */ }, - }); - - // Upsert each check with variables - await db.dynamicCheck.upsert({ - where: { integrationId_checkSlug: { integrationId: integration.id, checkSlug: 'check_name' } }, - create: { - integrationId: integration.id, - checkSlug: 'check_name', - name: 'Display Name', - description: '...', - taskMapping: 'frk_tt_...', - defaultSeverity: 'high', - definition: { steps: [...] }, - variables: [{ id: 'project_id', label: '...', type: 'text', required: true }], - isEnabled: true, - sortOrder: 0, - }, - update: { /* same fields */ }, - }); - - // Upsert IntegrationProvider row (required for connections) - await db.integrationProvider.upsert({ - where: { slug: 'service-name' }, - create: { slug: 'service-name', name: '...', category: '...', capabilities: ['checks'], isActive: true }, - update: { name: '...', category: '...', capabilities: ['checks'], isActive: true }, - }); - - await db.\$disconnect(); -} -main(); -" -``` - -### Step 5: Verify -Confirm the output shows successful upserts with no errors. Then tell the user to restart the API server. - -## Important Lessons (from production experience) - -### Base URL trailing slash -If the base URL has a path component (e.g., `https://graph.microsoft.com/v1.0`), add a trailing slash: `https://graph.microsoft.com/v1.0/`. Otherwise `new URL('users', base)` resolves to `https://graph.microsoft.com/users` instead of `https://graph.microsoft.com/v1.0/users`. - -### Full URL in fetch paths -When a check needs to call a DIFFERENT API domain than the base URL, use the full URL in the path: -```json -{ "type": "fetch", "path": "https://firebaserules.googleapis.com/v1/projects/{{variables.project_id}}/releases", "as": "releases" } -``` -The system detects full URLs and uses them directly instead of prepending the base URL. - -### Microsoft Graph scopes -Use `https://graph.microsoft.com/.default` instead of individual scopes like `User.Read.All`. The `.default` scope requests all permissions already granted to the app. - -### Microsoft Graph pagination -Uses `@odata.nextLink` which returns a full URL. Our `fetchWithCursor` handles this — set `cursorPath` to `@odata.nextLink` and it will follow the full URL automatically. - -### Google OAuth -- Authorize: `https://accounts.google.com/o/oauth2/v2/auth` -- Token: `https://oauth2.googleapis.com/token` -- Supports PKCE and refresh tokens -- Scopes are full URLs like `https://www.googleapis.com/auth/firebase.readonly` - -### Variables are dynamic -Any variable you add to a check's `variables` array in the DB automatically shows up as a form field in the UI. No frontend changes needed. - -### Check names should match evidence task names -If the evidence task is called "2FA", name the check "2FA". If it's "Employee Access", name the check "Employee Access". This is what the customer sees. - -### Don't use `$` in query params via the `params` field -URL `$` characters get encoded to `%24` which some APIs don't accept. Put OData-style params directly in the path instead: `users?$select=id,name`. - -## DSL Reference - -### Available Step Types: - -**fetch** — Single API call: -```json -{ "type": "fetch", "path": "endpoint", "as": "varName", "dataPath": "response.data", "params": {}, "onError": "fail|skip|empty" } -``` - -**fetchPages** — Paginated API call: -```json -{ - "type": "fetchPages", "path": "endpoint", "as": "varName", - "pagination": { - "strategy": "cursor", - "cursorParam": "pageToken", - "cursorPath": "nextPageToken", - "dataPath": "items" - } -} -``` -Strategies: `cursor` (token-based or full-URL), `page` (page-number), `link` (Link header/RFC 5988) - -**forEach** — Iterate and assert per resource: -```json -{ - "type": "forEach", "collection": "varName", "itemAs": "item", - "resourceType": "user", "resourceIdPath": "item.email", - "filter": { "field": "item.active", "operator": "eq", "value": true }, - "conditions": [{ "field": "item.mfa_enabled", "operator": "eq", "value": true }], - "steps": [{ "type": "fetch", "path": "details/{{item.id}}", "as": "detail" }], - "onPass": { "title": "...", "resourceType": "...", "resourceId": "..." }, - "onFail": { "title": "...", "severity": "high", "remediation": "..." } -} -``` -- `filter`: Skip items that don't match (before evaluation) -- `steps`: Nested fetch steps per item (e.g., fetch details) -- `conditions`: All must be true for pass (AND logic) - -**aggregate** — Count/sum threshold: -```json -{ - "type": "aggregate", "collection": "items", "operation": "countWhere", - "filter": { "field": "severity", "operator": "in", "value": ["critical","high"] }, - "condition": { "operator": "lte", "value": 5 }, - "onPass": { ... }, "onFail": { ... } -} -``` - -**branch** — Conditional logic: -```json -{ "type": "branch", "condition": { "field": "settings.sso", "operator": "exists" }, "then": [...], "else": [...] } -``` - -**emit** — Direct pass/fail: -```json -{ "type": "emit", "result": "pass", "template": { "title": "...", "resourceType": "...", "resourceId": "..." } } -``` - -### Expression Operators: -`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `exists`, `notExists`, `truthy`, `falsy`, `contains`, `matches`, `in`, `age_within_days`, `age_exceeds_days` - -### Logical Operators (for combining conditions): -```json -{ "op": "and", "conditions": [...] } -{ "op": "or", "conditions": [...] } -{ "op": "not", "condition": { ... } } -``` - -### Template Variables: -`{{item.field}}`, `{{variables.project_id}}`, `{{now}}` — resolved against execution scope - -## Quality Standards - -For EACH endpoint and field you use, state your confidence: -- ✅ **Verified**: Read from official API docs -- 🟡 **Likely**: Inferred from docs structure -- ❌ **Unverified**: Needs manual testing - -**DO NOT:** -- Guess API endpoints or field names -- Use placeholder URLs -- Skip pagination handling -- Write vague remediation ("fix the issue") -- Forget to define variables for user-provided config -- Create JSON files in the codebase - -**DO:** -- Seed directly to DB via `bun -e` with Prisma -- Use correct OAuth2 scopes (least privilege) -- Handle pagination correctly for each API's specific strategy -- Write remediation that references actual UI navigation paths in the target service -- Always define variables when checks need user config (project IDs, org names, etc.) -- Name checks to match the evidence task name (e.g., "2FA", "Employee Access") -- Add trailing slash to base URLs with path components diff --git a/.claude/skills/add-integration/SKILL.md b/.claude/skills/add-integration/SKILL.md deleted file mode 100644 index efcdadf15..000000000 --- a/.claude/skills/add-integration/SKILL.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -name: add-integration -description: Build a dynamic integration for the CompAI platform using the integration DSL -disable-model-invocation: true ---- - -You are a **Principal Integration Engineer** building a dynamic integration for the CompAI platform. - -**Before starting:** Read `.claude/skills/add-integration/examples.md` for production-tested examples. Also call `GET /v1/internal/dynamic-integrations` to see existing integrations as reference. - -## Workflow - -### Step 1: Research the API -- Find official REST API docs for $ARGUMENTS -- Identify: base URL, API version, auth method (OAuth2 scopes, API key header) -- Find exact endpoint paths and response schemas -- Identify pagination strategy (page numbers, cursors, Link headers, `@odata.nextLink`) -- Note rate limits -- State confidence for each endpoint: ✅ Verified, 🟡 Likely, ❌ Unverified - -### Step 2: Identify Relevant Checks -Map to evidence tasks. **Use the task name as the check name.** - -``` -2FA: frk_tt_68406cd9dde2d8cd4c463fe0 -Employee Access: frk_tt_68406ca292d9fffb264991b9 -Role-based Access Controls: frk_tt_68e80544d9734e0402cfa807 -Access Review Log: frk_tt_68e805457c2dcc784e72e3cc -Secure Secrets: frk_tt_68407ae5274a64092c305104 -Secure Code: frk_tt_68406e353df3bc002994acef -Code Changes: frk_tt_68406d64f09f13271c14dd01 -Sanitized Inputs: frk_tt_68406eedf0f0ddd220ea19c2 -Secure Devices: frk_tt_6840796f77d8a0dff53f947a -Device List: frk_tt_68406903839203801ac8041a -Encryption at Rest: frk_tt_68e52b26bf0e656af9e4e9c3 -Monitoring & Alerting: frk_tt_68406af04a4acb93083413b9 -Utility Monitoring: frk_tt_6849c1a1038c3f18cfff47bf -Incident Response: frk_tt_68406b4f40c87c12ae0479ce -App Availability: frk_tt_68406d2e86acc048d1774ea6 -TLS / HTTPS: frk_tt_68406f411fe27e47a0d6d5f3 -Employee Verification: frk_tt_68406951bd282273ebe286cc -Employee Descriptions: frk_tt_684069a3a0dd8322b2ac3f03 -Data Masking: frk_tt_686b51339d7e9f8ef2081a70 -Backup logs: frk_tt_68e52b26b166e2c0a0d11956 -Backup Restoration Test: frk_tt_68e52b269db179c434734766 -Internal Security Audit: frk_tt_68e52b2618cb9d9722c6edfd -Separation of Environments: frk_tt_68e52a484cad0014de7a628f -Infrastructure Inventory: frk_tt_69033a6bfeb4759be36257bc -Production Firewall: frk_tt_68fa2a852e70f757188f0c39 -Organisation Chart: frk_tt_68e52b274a7c38c62db08e80 -Systems Description: frk_tt_68dc1a3a9b92bb4ffb89e334 -Publish Policies: frk_tt_684076a02261faf3d331289d -Public Policies: frk_tt_6840791cac0a7b780dbaf932 -Contact Information: frk_tt_68406a514e90bb6e32e0b107 -``` - -### Step 3: Identify Required Variables -Define user-provided config (project IDs, org names, tenant IDs, domains). -Format: `{ id, label, type, required, helpText, placeholder }` -Types: `text`, `number`, `boolean`, `select`, `multi-select` -Reference as `{{variables.project_id}}` - -Common: Google/Firebase → `project_id`, Azure DevOps → `organization`, Microsoft 365/Intune → none, Slack/GitHub → none - -### Step 4: Pre-Deployment Validation (MANDATORY) - -**JSON structure:** -- [ ] `capabilities` is `["checks"]` NOT `{"checks"}` -- [ ] `defaultHeaders` is `{}` — valid JSON object - -**Base URL:** -- [ ] Trailing slash if URL has path component: `https://api.example.com/v1.0/` - -**API paths:** -- [ ] **ALWAYS use full URLs** in fetch/fetchPages paths: `https://graph.microsoft.com/v1.0/deviceManagement/...` -- [ ] NEVER start paths with `/` -- [ ] OData `$select` params go directly in the path URL, NOT in `params` - -**OAuth config:** -- [ ] Google: `"authorizationParams": {"access_type": "offline", "prompt": "consent"}` -- [ ] Microsoft: use EXPLICIT scopes, NOT `.default` (fails for Intune) -- [ ] Microsoft: scopes must be **Delegated** (not Application) in Azure app -- [ ] `supportsRefreshToken: true` - -**Checks:** -- [ ] Every check has `variables` array with required user inputs -- [ ] `onFail` has real remediation with actual UI paths -- [ ] Check names match evidence task names exactly - -### Step 5: Deploy via API -- Upsert: `PUT /v1/internal/dynamic-integrations` -- Auth: `X-Internal-Token` header -- Base URL: `http://localhost:3333` (local) or production - -Other endpoints: `GET` (list/detail), `PATCH` (update), `DELETE`, `POST .../activate`, `POST .../deactivate` - -### Step 6: Verify -- Confirm `{ success: true, id, slug, checksCount }` -- Call `GET /v1/internal/dynamic-integrations` to verify data - -### Step 7: Post-Integration Report -**What was done:** integration name, slug, checks, task mappings -**What user needs to do (step by step):** -1. OAuth app: where to register, which Delegated scopes, grant admin consent, redirect URI `https://api.trycomp.ai/v1/integrations/oauth/callback` -2. Admin panel: `/admin/integrations` → enter client ID + secret -3. Provider-specific: e.g., enable Identity Platform, assign Intune license -4. Test: connect, enter variables, run each check - -**Complexity:** 🟢 Simple (API key) | 🟡 Medium (existing OAuth provider) | 🔴 Complex (new OAuth app + approval) - -## Provider-Specific Rules - -### Google / Firebase / GCP -- Authorize: `https://accounts.google.com/o/oauth2/v2/auth` -- Token: `https://oauth2.googleapis.com/token` -- **ALWAYS** `"authorizationParams": {"access_type": "offline", "prompt": "consent"}` -- Scopes are full URLs: `https://www.googleapis.com/auth/firebase.readonly` -- Needs Google app verification for external users -- Firebase needs Identity Platform upgrade for admin APIs - -### Microsoft Graph (Office 365, Intune, Azure AD) -- Authorize: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` -- Token: `https://login.microsoftonline.com/common/oauth2/v2.0/token` -- **Use EXPLICIT scopes** — do NOT use `.default` -- Add `offline_access`, `openid`, `profile` to scopes -- Scopes must be **Delegated** in Azure app + grant admin consent -- Always use full URLs in paths: `https://graph.microsoft.com/v1.0/endpoint` -- Pagination: `@odata.nextLink` returns full URL — cursor handler follows it -- Intune needs Intune license on connecting account -- Personal accounts (MSA) don't support admin APIs - -### Azure DevOps -- Authorize: `https://app.vssps.visualstudio.com/oauth2/authorize` -- Token: `https://app.vssps.visualstudio.com/oauth2/token` -- Base URL: `https://dev.azure.com` -- Requires `organization` variable -- Scopes: `vso.code`, `vso.build`, `vso.project` - -## DSL Reference - -### Step Types -- **fetch**: `{ type: "fetch", path: "https://full-url/endpoint", as: "varName", dataPath: "data", onError: "fail|skip|empty" }` -- **fetchPages**: `{ type: "fetchPages", path: "https://full-url/endpoint", as: "varName", pagination: { strategy: "cursor", cursorParam, cursorPath, dataPath } }` -- **forEach**: Iterate collection with filter, conditions, nested steps, onPass/onFail -- **aggregate**: Count/sum threshold with countWhere operation -- **branch**: Conditional with then/else step arrays -- **emit**: Direct pass/fail with template - -### Operators -`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `exists`, `notExists`, `truthy`, `falsy`, `contains`, `matches`, `in`, `age_within_days`, `age_exceeds_days` - -### Logical: `and`, `or`, `not` -### Templates: `{{item.field}}`, `{{variables.project_id}}`, `{{now}}` - -## Quality Standards -**DO NOT:** guess endpoints, use relative paths, use `.default` for Microsoft, skip validation checklist, forget variables, create JSON files, seed directly to DB -**DO:** use full URLs in all paths, PUT upsert endpoint, explicit OAuth scopes, proper pagination, real remediation with UI paths, match check names to task names, run validation checklist before deploy diff --git a/.claude/skills/add-integration/examples.md b/.claude/skills/add-integration/examples.md deleted file mode 100644 index e3a35fdb8..000000000 --- a/.claude/skills/add-integration/examples.md +++ /dev/null @@ -1,337 +0,0 @@ -# Dynamic Integration Examples - -Reference examples from production. Use these as templates when building new integrations. - -## Example 1: Firebase (Google OAuth) - -**Auth:** Google OAuth2 with PKCE, offline access for token refresh -**Variables:** `project_id` (user provides their Firebase project ID) -**Checks:** 3 checks mapping to 2FA, Employee Access, Encryption at Rest - -```json -{ - "slug": "firebase", - "name": "Firebase", - "description": "Monitor Firebase Authentication MFA settings, user access, and security rules", - "category": "Cloud", - "logoUrl": "https://www.vectorlogo.zone/logos/firebase/firebase-icon.svg", - "docsUrl": "https://firebase.google.com/docs/reference/rest", - "baseUrl": "https://identitytoolkit.googleapis.com/", - "defaultHeaders": {}, - "authConfig": { - "type": "oauth2", - "config": { - "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth", - "tokenUrl": "https://oauth2.googleapis.com/token", - "scopes": [ - "https://www.googleapis.com/auth/identitytoolkit", - "https://www.googleapis.com/auth/firebase.readonly" - ], - "pkce": true, - "supportsRefreshToken": true, - "clientAuthMethod": "body", - "authorizationParams": { "access_type": "offline", "prompt": "consent" } - } - }, - "capabilities": ["checks"], - "checks": [ - { - "checkSlug": "mfa_config", - "name": "2FA", - "description": "Verifies that multi-factor authentication is enabled at the Firebase project level", - "taskMapping": "frk_tt_68406cd9dde2d8cd4c463fe0", - "defaultSeverity": "high", - "definition": { - "steps": [ - { - "type": "fetch", - "path": "admin/v2/projects/{{variables.project_id}}/config", - "as": "config", - "onError": "fail" - }, - { - "type": "branch", - "condition": { - "op": "or", - "conditions": [ - { "field": "config.mfa.state", "operator": "eq", "value": "ENABLED" }, - { "field": "config.mfa.state", "operator": "eq", "value": "MANDATORY" } - ] - }, - "then": [ - { - "type": "emit", - "result": "pass", - "template": { - "title": "MFA is enabled for Firebase project", - "description": "Multi-factor authentication is set to {{config.mfa.state}}", - "resourceType": "firebase-project", - "resourceId": "{{variables.project_id}}" - } - } - ], - "else": [ - { - "type": "emit", - "result": "fail", - "template": { - "title": "MFA is not enabled for Firebase project", - "description": "MFA state is {{config.mfa.state}}. Should be ENABLED or MANDATORY.", - "resourceType": "firebase-project", - "resourceId": "{{variables.project_id}}", - "severity": "high", - "remediation": "Go to Firebase Console > Authentication > Sign-in method > Multi-factor authentication > Enable MFA." - } - } - ] - } - ] - }, - "variables": [ - { - "id": "project_id", - "label": "Firebase Project ID", - "type": "text", - "required": true, - "helpText": "Found in Firebase Console > Project Settings", - "placeholder": "my-firebase-project" - } - ] - }, - { - "checkSlug": "user_access", - "name": "Employee Access", - "description": "Reviews Firebase Authentication users and MFA enrollment", - "taskMapping": "frk_tt_68406ca292d9fffb264991b9", - "defaultSeverity": "medium", - "definition": { - "steps": [ - { - "type": "fetchPages", - "path": "v1/projects/{{variables.project_id}}/accounts:batchGet", - "as": "users", - "pagination": { - "strategy": "cursor", - "cursorParam": "nextPageToken", - "cursorPath": "nextPageToken", - "dataPath": "users" - }, - "params": { "maxResults": "100" }, - "onError": "fail" - }, - { - "type": "forEach", - "collection": "users", - "itemAs": "user", - "resourceType": "user", - "resourceIdPath": "user.email", - "filter": { - "op": "and", - "conditions": [ - { "field": "user.email", "operator": "exists" }, - { "field": "user.disabled", "operator": "neq", "value": true } - ] - }, - "conditions": [ - { "field": "user.mfaInfo.length", "operator": "gt", "value": 0 } - ], - "onPass": { - "title": "MFA enrolled: {{user.email}}", - "description": "{{user.displayName}} has MFA configured", - "resourceType": "user", - "resourceId": "{{user.email}}" - }, - "onFail": { - "title": "No MFA enrolled: {{user.email}}", - "description": "{{user.displayName}} has no MFA methods enrolled", - "resourceType": "user", - "resourceId": "{{user.email}}", - "severity": "medium", - "remediation": "Enable MFA at project level, then user can enroll via the app." - } - } - ] - }, - "variables": [ - { - "id": "project_id", - "label": "Firebase Project ID", - "type": "text", - "required": true, - "helpText": "Found in Firebase Console > Project Settings", - "placeholder": "my-firebase-project" - } - ] - } - ] -} -``` - -**Key patterns in this example:** -- `authorizationParams` with `access_type: offline` — required for Google -- `baseUrl` has trailing slash — paths are relative to it -- Security Rules check uses full URL for a different API domain (`firebaserules.googleapis.com`) -- `branch` step for simple pass/fail on a single value -- `fetchPages` with cursor pagination for user listing -- `forEach` with `filter` to skip disabled users -- Variables defined on each check for `project_id` - ---- - -## Example 2: Microsoft Intune (Microsoft Graph OAuth) - -**Auth:** Microsoft OAuth2 with explicit scopes (NOT `.default`) -**Variables:** None (tenant determined from OAuth) -**Checks:** 3 checks using full URLs to Microsoft Graph API - -```json -{ - "slug": "intune", - "name": "Microsoft Intune", - "description": "Monitor device compliance and enrollment status", - "category": "Security", - "logoUrl": "https://img.logo.dev/microsoft.com?token=pk_AZatYxV5QDSfWpRDaBxzRQ", - "baseUrl": "https://graph.microsoft.com/v1.0/", - "defaultHeaders": {}, - "authConfig": { - "type": "oauth2", - "config": { - "authorizeUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - "tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token", - "scopes": [ - "DeviceManagementManagedDevices.Read.All", - "DeviceManagementConfiguration.Read.All", - "ServiceHealth.Read.All", - "offline_access", - "openid", - "profile" - ], - "pkce": true, - "supportsRefreshToken": true, - "clientAuthMethod": "body" - } - }, - "capabilities": ["checks"], - "checks": [ - { - "checkSlug": "device_compliance", - "name": "Device Compliance Status", - "description": "Checks that all Intune-managed devices are compliant", - "taskMapping": "frk_tt_6840796f77d8a0dff53f947a", - "defaultSeverity": "high", - "definition": { - "steps": [ - { - "type": "fetchPages", - "path": "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=id,deviceName,complianceState,isEncrypted,operatingSystem,osVersion,userPrincipalName,userDisplayName,lastSyncDateTime", - "as": "devices", - "pagination": { - "strategy": "cursor", - "cursorParam": "$skiptoken", - "cursorPath": "@odata.nextLink", - "dataPath": "value" - }, - "onError": "fail" - }, - { - "type": "forEach", - "collection": "devices", - "itemAs": "device", - "resourceType": "device", - "resourceIdPath": "device.deviceName", - "conditions": [ - { "field": "device.complianceState", "operator": "eq", "value": "compliant" } - ], - "onPass": { - "title": "Device compliant: {{device.deviceName}}", - "description": "{{device.operatingSystem}} {{device.osVersion}} — {{device.userDisplayName}}", - "resourceType": "device", - "resourceId": "{{device.deviceName}}" - }, - "onFail": { - "title": "Device non-compliant: {{device.deviceName}}", - "description": "{{device.operatingSystem}} device is {{device.complianceState}}", - "resourceType": "device", - "resourceId": "{{device.deviceName}}", - "severity": "high", - "remediation": "Go to Intune admin center (https://intune.microsoft.com) > Devices > select device > Review compliance status." - } - } - ] - }, - "variables": [] - } - ] -} -``` - -**Key patterns in this example:** -- Microsoft scopes are EXPLICIT, not `.default` -- Includes `offline_access`, `openid`, `profile` for token refresh -- **Full URL in path** — `https://graph.microsoft.com/v1.0/deviceManagement/...` (most important pattern) -- `@odata.nextLink` pagination — returns full URL, our cursor handler follows it -- `$select` OData params directly in the path URL -- No variables needed — Microsoft tenant is determined from OAuth token - ---- - -## Common Patterns - -### Simple config check (pass/fail on a single setting) -```json -{ - "type": "fetch", "path": "https://api.example.com/v1/settings", "as": "settings" -}, -{ - "type": "branch", - "condition": { "field": "settings.mfa_enabled", "operator": "eq", "value": true }, - "then": [{ "type": "emit", "result": "pass", "template": { ... } }], - "else": [{ "type": "emit", "result": "fail", "template": { ... } }] -} -``` - -### Per-user check (iterate users, check each) -```json -{ - "type": "fetchPages", "path": "https://api.example.com/v1/users", "as": "users", - "pagination": { "strategy": "cursor", "cursorParam": "page_token", "cursorPath": "next_token", "dataPath": "data" } -}, -{ - "type": "forEach", "collection": "users", "itemAs": "user", - "resourceType": "user", "resourceIdPath": "user.email", - "conditions": [{ "field": "user.mfa_enabled", "operator": "eq", "value": true }], - "onPass": { "title": "MFA on: {{user.email}}", ... }, - "onFail": { "title": "MFA off: {{user.email}}", "severity": "high", "remediation": "...", ... } -} -``` - -### Threshold check (count items matching criteria) -```json -{ - "type": "fetch", "path": "https://api.example.com/v1/issues", "as": "issues", "dataPath": "items" -}, -{ - "type": "aggregate", "collection": "issues", "operation": "countWhere", - "filter": { "field": "severity", "operator": "in", "value": ["critical", "high"] }, - "condition": { "operator": "lte", "value": 5 }, - "onPass": { "title": "Critical issues within threshold", ... }, - "onFail": { "title": "Too many critical issues", "severity": "high", ... } -} -``` - -### Empty collection handling -```json -{ - "type": "fetch", "path": "https://api.example.com/v1/rules", "as": "rulesResponse" -}, -{ - "type": "branch", - "condition": { "field": "rulesResponse.rules.length", "operator": "gt", "value": 0 }, - "then": [ - { "type": "forEach", "collection": "rulesResponse.rules", ... } - ], - "else": [ - { "type": "emit", "result": "fail", "template": { "title": "No rules configured", ... } } - ] -} -``` diff --git a/apps/api/src/frameworks/frameworks-scores.helper.ts b/apps/api/src/frameworks/frameworks-scores.helper.ts index 4401fefb3..a5e9bc3c5 100644 --- a/apps/api/src/frameworks/frameworks-scores.helper.ts +++ b/apps/api/src/frameworks/frameworks-scores.helper.ts @@ -2,6 +2,7 @@ import { evidenceFormDefinitionList, meetingSubTypeValues, toDbEvidenceFormType, + toExternalEvidenceFormType, } from '@comp/company'; import { db } from '@trycompai/db'; import { filterComplianceMembers } from '../utils/compliance-filters'; @@ -142,7 +143,7 @@ async function computeDocumentsScore(organizationId: string) { } async function getOrganizationFindings(organizationId: string) { - return db.finding.findMany({ + const findings = await db.finding.findMany({ where: { organizationId }, include: { task: { select: { id: true, title: true } }, @@ -150,6 +151,19 @@ async function getOrganizationFindings(organizationId: string) { }, orderBy: [{ status: 'asc' }, { createdAt: 'desc' }], }); + + return findings.map((finding) => ({ + ...finding, + evidenceFormType: toExternalEvidenceFormType(finding.evidenceFormType), + evidenceSubmission: finding.evidenceSubmission + ? { + ...finding.evidenceSubmission, + formType: + toExternalEvidenceFormType(finding.evidenceSubmission.formType) ?? + 'meeting', + } + : null, + })); } export async function getCurrentMember( diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FindingsOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FindingsOverview.tsx index 79cb9b5fd..648c6fad9 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FindingsOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FindingsOverview.tsx @@ -61,9 +61,9 @@ function FindingsList({ {finding.task?.title ?? (finding.evidenceFormType - ? `Document: ${finding.evidenceFormType}` + ? `Document: ${finding.evidenceFormType.replace(/-/g, ' ')}` : finding.evidenceSubmission - ? `Document: ${finding.evidenceSubmission.formType}` + ? `Document: ${finding.evidenceSubmission.formType.replace(/-/g, ' ')}` : 'Finding')}

diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts index 80213bcfe..4002bc5a1 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/generate-suggestions.ts @@ -14,8 +14,8 @@ const SuggestionsSchema = z.object({ z.object({ title: z.string(), prompt: z.string(), - vendorName: z.string().optional(), - vendorWebsite: z.string().optional(), + vendorName: z.string().nullable(), + vendorWebsite: z.string().nullable(), }), ), }); @@ -24,39 +24,39 @@ export async function generateAutomationSuggestions( taskDescription: string, organizationId: string, ): Promise<{ title: string; prompt: string; vendorName?: string; vendorWebsite?: string }[]> { - // Get vendors from the Vendor table - const vendors = await db.vendor.findMany({ - where: { - organizationId, - }, - select: { - name: true, - website: true, - description: true, - }, - }); - // Get vendors from context table as well - const contextEntries = await db.context.findMany({ - where: { - organizationId, - }, - select: { - question: true, - answer: true, - }, - }); - const vendorList = - vendors.length > 0 - ? vendors.map((v) => `${v.name}${v.website ? ` (${v.website})` : ''}`).join(', ') - : 'No vendors configured yet'; + try { + // Get vendors from the Vendor table + const vendors = await db.vendor.findMany({ + where: { + organizationId, + }, + select: { + name: true, + website: true, + description: true, + }, + }); + // Get vendors from context table as well + const contextEntries = await db.context.findMany({ + where: { + organizationId, + }, + select: { + question: true, + answer: true, + }, + }); + const vendorList = + vendors.length > 0 + ? vendors.map((v) => `${v.name}${v.website ? ` (${v.website})` : ''}`).join(', ') + : 'No vendors configured yet'; - const contextInfo = - contextEntries.length > 0 - ? contextEntries.map((c) => `Q: ${c.question}\nA: ${c.answer}`).join('\n\n') - : 'No additional context available'; + const contextInfo = + contextEntries.length > 0 + ? contextEntries.map((c) => `Q: ${c.question}\nA: ${c.answer}`).join('\n\n') + : 'No additional context available'; - // Generate AI suggestions - try { + // Generate AI suggestions const { object } = await generateObject({ model: groq('meta-llama/llama-4-scout-17b-16e-instruct'), schema: SuggestionsSchema, @@ -73,7 +73,12 @@ export async function generateAutomationSuggestions( } } - return suggestions; + return suggestions.map((s) => ({ + title: s.title, + prompt: s.prompt, + vendorName: s.vendorName ?? undefined, + vendorWebsite: s.vendorWebsite ?? undefined, + })); } catch (error) { console.error('[generateAutomationSuggestions] Error generating suggestions:', error); // Try to extract suggestions from error if available @@ -87,7 +92,12 @@ export async function generateAutomationSuggestions( ? parsed.suggestions : [parsed.suggestions]; if (suggestions.length > 0 && suggestions[0].title) { - return suggestions; + return suggestions.map((s: Record) => ({ + title: String(s.title), + prompt: String(s.prompt), + vendorName: (s.vendorName as string) ?? undefined, + vendorWebsite: (s.vendorWebsite as string) ?? undefined, + })); } } } diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx index 5489188f6..2b792e41d 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx @@ -57,7 +57,6 @@ export function AutomationPageClient({ .catch((error) => { console.error('Failed to generate suggestions:', error); setIsLoadingSuggestions(false); - // Keep empty array, will use static suggestions }); } else { // Not a new automation, no need to load suggestions