diff --git a/.claude/agents/api-endpoint-auditor.md b/.claude/agents/api-endpoint-auditor.md new file mode 100644 index 0000000000..85deb61d02 --- /dev/null +++ b/.claude/agents/api-endpoint-auditor.md @@ -0,0 +1,44 @@ +--- +name: api-endpoint-auditor +description: Audits API controllers for RBAC compliance and checks matching frontend permission gating +tools: Read, Grep, Glob, Bash +--- + +You audit NestJS API controllers and their corresponding frontend consumers for full RBAC compliance. + +## API checks (apps/api/src/) + +For each controller endpoint, verify: + +1. **Guards**: `@UseGuards(HybridAuthGuard, PermissionGuard)` at controller or endpoint level +2. **Permissions**: `@RequirePermission('resource', 'action')` on every endpoint +3. **Controller format**: `@Controller({ path: 'name', version: '1' })` — NOT `@Controller('v1/name')` (double prefix bug) +4. **Webhooks**: External webhook endpoints use `@Public()` only +5. **Self-endpoints** (`/me/*`): `HybridAuthGuard` sufficient, `@RequirePermission` optional + +## Frontend checks (apps/app/src/) + +After auditing the controller, find frontend code that calls these endpoints: + +```bash +# Find frontend files calling this endpoint +grep -r "v1/endpoint-path" apps/app/src/ --include="*.ts" --include="*.tsx" -l +``` + +For each frontend consumer, verify: +1. Mutation buttons gated with `hasPermission('resource', 'action')` +2. `usePermissions` hook imported and used +3. No manual role string parsing (`role.includes('admin')`) +4. Actions columns hidden when user lacks write permission + +## Permission resources +`organization`, `member`, `control`, `evidence`, `policy`, `risk`, `vendor`, `task`, `framework`, `audit`, `finding`, `questionnaire`, `integration`, `apiKey`, `trust`, `pentest`, `app`, `compliance` + +## Output format + +Report per-endpoint: +- Endpoint: `METHOD /path` +- Guard status: present / MISSING +- Permission status: present (`resource:action`) / MISSING +- Frontend consumers: file paths +- Frontend gating status: gated / MISSING (with specific line numbers) diff --git a/.claude/agents/ds-migration-reviewer.md b/.claude/agents/ds-migration-reviewer.md new file mode 100644 index 0000000000..e6637a5e35 --- /dev/null +++ b/.claude/agents/ds-migration-reviewer.md @@ -0,0 +1,40 @@ +--- +name: ds-migration-reviewer +description: Checks files for @comp/ui and lucide-react imports that can be migrated to @trycompai/design-system +tools: Read, Grep, Glob, Bash +--- + +You review frontend files for design system migration opportunities. + +## What to check + +For each file provided, identify: + +1. **`@comp/ui` imports** — check if `@trycompai/design-system` has an equivalent: + ```bash + node -e "console.log(Object.keys(require('@trycompai/design-system')))" + ``` + +2. **`lucide-react` imports** — find matching Carbon icons: + ```bash + node -e "const i = require('@trycompai/design-system/icons'); console.log(Object.keys(i).filter(k => k.match(/SearchTerm/i)))" + ``` + +3. **`@comp/ui/button` Button** — DS Button has `loading`, `iconLeft`, `iconRight` props. Manual spinner/icon rendering inside buttons should use these props instead. + +4. **Raw HTML layout** (`
`) — check if `Stack`, `HStack`, `PageLayout`, `PageHeader`, `Section` could replace it. + +## Important rules + +- DS `Text`, `Stack`, `HStack`, `Badge`, `Button` do NOT accept `className` — wrap in `
` if styling needed +- Icons come from `@trycompai/design-system/icons` (Carbon icons) +- Only flag migrations where a DS equivalent actually exists — verify by checking exports +- Don't flag `@comp/ui` usage for components that have no DS equivalent yet + +## Output format + +For each file, report: +- File path +- Each import that can be migrated, with the DS replacement +- Specific icon mappings (e.g., `Trash2` → `TrashCan`, `ExternalLink` → `Launch`) +- Any Button instances that should use `loading`/`iconLeft`/`iconRight` props diff --git a/.claude/agents/rbac-reviewer.md b/.claude/agents/rbac-reviewer.md new file mode 100644 index 0000000000..f515f87843 --- /dev/null +++ b/.claude/agents/rbac-reviewer.md @@ -0,0 +1,28 @@ +--- +name: rbac-reviewer +description: Reviews code for RBAC compliance — checks guards, permissions, and frontend gating +tools: Read, Grep, Glob, Bash +--- + +You are a senior security engineer reviewing code for RBAC compliance in a NestJS + Next.js monorepo. + +## API Endpoints (apps/api/src/) + +Check every controller endpoint for: +- `@UseGuards(HybridAuthGuard, PermissionGuard)` at controller or endpoint level +- `@RequirePermission('resource', 'action')` on every endpoint +- Controller format: `@Controller({ path: 'name', version: '1' })` (NOT `@Controller('v1/name')`) +- `@Public()` only on webhooks and unauthenticated endpoints + +## Frontend Components (apps/app/src/) + +Check every mutation element for: +- `usePermissions` hook imported and used +- Buttons/forms gated with `hasPermission('resource', 'action')` +- No manual role string parsing (`role.includes('admin')`) +- Actions columns hidden when user lacks write permission + +## Permission Resources +`organization`, `member`, `control`, `evidence`, `policy`, `risk`, `vendor`, `task`, `framework`, `audit`, `finding`, `questionnaire`, `integration`, `apiKey`, `trust`, `pentest`, `app`, `compliance` + +Provide specific file paths, line numbers, and suggested fixes for every violation found. diff --git a/.claude/agents/test-writer.md b/.claude/agents/test-writer.md new file mode 100644 index 0000000000..f4300b9195 --- /dev/null +++ b/.claude/agents/test-writer.md @@ -0,0 +1,87 @@ +--- +name: test-writer +description: Writes permission-gated component tests following the established Vitest + testing-library pattern +tools: Read, Grep, Glob, Bash +--- + +You write unit tests for React components that use `usePermissions` for RBAC gating. + +## Infrastructure + +- **Framework**: Vitest with jsdom +- **Libraries**: `@testing-library/react` + `@testing-library/jest-dom` +- **Setup**: `apps/app/src/test-utils/setup.ts` +- **Permission mocks**: `apps/app/src/test-utils/mocks/permissions.ts` +- **Run**: `cd apps/app && npx vitest run path/to/test` + +## Process + +1. Read the component file to understand: + - What `hasPermission()` checks it uses + - Which UI elements are gated (buttons, forms, menu items) + - What data it renders unconditionally + - What props it expects and what hooks it uses + +2. Read existing test files nearby for patterns (mock setup, render helpers) + +3. Write the test file with these required scenarios: + +### Admin (write) user +- All mutation elements (buttons, form submits, toggles) are **visible and enabled** +- Data renders correctly + +### Auditor (read-only) user +- Mutation elements are **hidden or disabled** +- Read-only content still renders +- No error states from missing permissions + +### Data always visible +- Tables, lists, text content render regardless of permission level + +## Test template + +```tsx +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { setMockPermissions, ADMIN_PERMISSIONS, AUDITOR_PERMISSIONS } from '@/test-utils/mocks/permissions'; +import { ComponentUnderTest } from './ComponentUnderTest'; + +// Mock hooks/dependencies as needed +vi.mock('@/hooks/use-permissions'); + +describe('ComponentUnderTest', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('admin user', () => { + beforeEach(() => setMockPermissions(ADMIN_PERMISSIONS)); + + it('renders mutation buttons', () => { + render(); + expect(screen.getByRole('button', { name: /create/i })).toBeInTheDocument(); + }); + }); + + describe('read-only user', () => { + beforeEach(() => setMockPermissions(AUDITOR_PERMISSIONS)); + + it('hides mutation buttons', () => { + render(); + expect(screen.queryByRole('button', { name: /create/i })).not.toBeInTheDocument(); + }); + + it('still renders data', () => { + render(); + expect(screen.getByText(/expected content/i)).toBeInTheDocument(); + }); + }); +}); +``` + +## Rules + +- Mock all external hooks (`useSWR`, `useRouter`, `apiClient`, etc.) +- Use `screen.queryBy*` for elements that should NOT exist (returns null instead of throwing) +- Use `screen.getBy*` for elements that MUST exist +- Run the test after writing to verify it passes: `cd apps/app && npx vitest run path/to/file.test.tsx` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..8c97fa017d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "file=\"$CLAUDE_FILE_PATH\"; if echo \"$file\" | grep -qE '\\.(ts|tsx)$' && echo \"$file\" | grep -qE '^(apps/api|apps/app)/'; then echo 'TypeScript file modified — remember to run typecheck before committing'; fi" + } + ] + }, + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "file=\"$CLAUDE_FILE_PATH\"; if echo \"$file\" | grep -q 'packages/db/prisma/generated'; then echo 'BLOCKED: Do not write to generated Prisma files' && exit 1; fi" + } + ] + } + ] + } +} diff --git a/.claude/skills/add-integration/SKILL.md b/.claude/skills/add-integration/SKILL.md new file mode 100644 index 0000000000..efcdadf153 --- /dev/null +++ b/.claude/skills/add-integration/SKILL.md @@ -0,0 +1,157 @@ +--- +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 new file mode 100644 index 0000000000..e3a35fdb8f --- /dev/null +++ b/.claude/skills/add-integration/examples.md @@ -0,0 +1,337 @@ +# 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/.claude/skills/audit-design-system/SKILL.md b/.claude/skills/audit-design-system/SKILL.md new file mode 100644 index 0000000000..4afb32b178 --- /dev/null +++ b/.claude/skills/audit-design-system/SKILL.md @@ -0,0 +1,23 @@ +--- +name: audit-design-system +description: Audit & fix design system usage — migrate @comp/ui and lucide-react to @trycompai/design-system +--- + +Audit the specified files for design system compliance. **Fix every issue found immediately.** + +## Rules + +1. **`@trycompai/design-system`** is the primary component library. `@comp/ui` is legacy — only use as last resort when no DS equivalent exists. +2. **Always check DS exports first** before reaching for `@comp/ui`. Run `node -e "console.log(Object.keys(require('@trycompai/design-system')))"` to check. +3. **Icons**: Use `@trycompai/design-system/icons` (Carbon icons), NOT `lucide-react`. Check with `node -e "const i = require('@trycompai/design-system/icons'); console.log(Object.keys(i).filter(k => k.match(/YourSearch/i)))"`. +4. **DS components that do NOT accept `className`**: `Text`, `Stack`, `HStack`, `Badge`, `Button` — wrap in `
` for custom styling. +5. **Button**: Use DS `Button` with `loading`, `iconLeft`, `iconRight` props instead of manually rendering spinners/icons. +6. **Layout**: Use `PageLayout`, `PageHeader`, `Stack`, `HStack`, `Section`, `SettingGroup`. +7. **Patterns**: Sheet (`Sheet > SheetContent > SheetHeader + SheetBody`), Drawer, Collapsible. + +## Process +1. Read files specified in `$ARGUMENTS` +2. Find `@comp/ui` imports — check if DS equivalent exists +3. Find `lucide-react` imports — find matching Carbon icons +4. Migrate components and icons +5. Run build to verify: `npx turbo run typecheck --filter=@comp/app` diff --git a/.claude/skills/audit-hooks/SKILL.md b/.claude/skills/audit-hooks/SKILL.md new file mode 100644 index 0000000000..ab74d70795 --- /dev/null +++ b/.claude/skills/audit-hooks/SKILL.md @@ -0,0 +1,34 @@ +--- +name: audit-hooks +description: Audit & fix hooks and API usage patterns — eliminate server actions, raw fetch, and stale patterns +--- + +Audit the specified files for hook and API usage compliance. **Fix every issue found immediately.** + +## Forbidden Patterns (fix immediately) + +1. **`useAction` from `next-safe-action`** → replace with SWR hook or custom mutation hook +2. **Server actions mutating via `@db`** → delete and use API hook instead +3. **Direct `@db` in client components** → replace with `apiClient` via hook +4. **Direct `@db` in Next.js pages for mutations** → replace with `serverApi` +5. **Raw `fetch()` without `credentials: 'include'`** → use `apiClient` +6. **`window.location.reload()` after mutations** → use SWR `mutate()` +7. **`router.refresh()` after mutations** → use SWR `mutate()` +8. **`useEffect` + `apiClient.get` for data fetching** → replace with `useSWR` +9. **Callback props for data refresh** (`onXxxAdded`, `onSuccess`) → remove, rely on SWR cache sharing + +## Required Patterns + +- **Client data fetching**: `useSWR` with `apiClient` or custom hook +- **Client mutations**: custom hooks wrapping `apiClient` with `mutate()` for cache invalidation +- **Server components**: `serverApi` from `apps/app/src/lib/api-server.ts` +- **SWR**: `fallbackData` for SSR data, `revalidateOnMount: !initialData` +- **API response**: lists = `response.data.data`, single = `response.data` +- **`mutate()` safety**: guard against `undefined` in optimistic update functions +- **`Array.isArray()` checks**: when consuming SWR data that could be stale + +## Process +1. Read files specified in `$ARGUMENTS` +2. Find forbidden patterns and fix them +3. Ensure all data fetching uses SWR hooks +4. Run typecheck to verify: `npx turbo run typecheck --filter=@comp/app` diff --git a/.claude/skills/audit-rbac/SKILL.md b/.claude/skills/audit-rbac/SKILL.md new file mode 100644 index 0000000000..a7acb60e14 --- /dev/null +++ b/.claude/skills/audit-rbac/SKILL.md @@ -0,0 +1,42 @@ +--- +name: audit-rbac +description: Audit & fix RBAC and audit log compliance in API endpoints and frontend components +--- + +Audit the specified files or directories for RBAC and audit log compliance. **Fix every issue found immediately.** + +## Rules + +### API Endpoints (NestJS — `apps/api/src/`) +1. **Every mutation endpoint** (POST, PATCH, PUT, DELETE) MUST have `@RequirePermission('resource', 'action')`. If missing, **add it**. +2. **Read endpoints** (GET) should have `@RequirePermission('resource', 'read')`. If missing, **add it**. +3. **Self-endpoints** (e.g., `/me/preferences`) may skip `@RequirePermission` — authentication via `HybridAuthGuard` is sufficient. +4. **Controller format**: Must use `@Controller({ path: 'name', version: '1' })`, NOT `@Controller('v1/name')`. If wrong, **fix it**. +5. **Guards**: Use `@UseGuards(HybridAuthGuard, PermissionGuard)` at controller or endpoint level. Never skip PermissionGuard. +6. **Webhooks**: External webhook endpoints use `@Public()` — no auth required. + +### Frontend Components (`apps/app/src/`) +1. **Every mutation element** (button, form submit, toggle, switch, file upload) MUST be gated with `usePermissions` from `@/hooks/use-permissions`. If not: + - **Create/Add buttons**: Wrap with `{hasPermission('resource', 'create') && + + + + or copy and paste this URL into your browser{' '} + + {inviteLink} + + + +
+ {email && ( +
+ + this invitation was intended for{' '} + {email}. + +
+ )} + +
+