diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/SplitView.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/SplitView.tsx index 0846203ff..9dbaef447 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/SplitView.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/SplitView.tsx @@ -27,6 +27,7 @@ import { CreateRunPanel } from './CreateRunPanel'; import { DetailPane } from './DetailPane'; import { EmptyState } from './EmptyState'; import { OverviewPane } from './OverviewPane'; +import { PenTestMarketingEmptyState } from './pen-test-marketing/PenTestMarketingEmptyState'; import { RunList } from './RunList'; import './pentest-tokens.css'; @@ -71,8 +72,25 @@ export function SplitView({ orgId, selectedRunId, mode = 'default' }: SplitViewP const { balance, planRequired } = getPentestAllowance(billingStatus); const quotaLabel = 'Plan'; + // Marketing empty state — shown when the workspace has no pentest entitlement + // (no active/trialing subscription and no wallet credit) AND has never run a + // scan. Once either flips, fall through to the existing list / empty-state + // surface. The boolean uses the universal `isMarketingStateEnabled` naming + // so any feature that adopts stays consistent. + const isMarketingStateEnabled = + !listLoading && + billingStatus !== undefined && + planRequired && + reports.length === 0 && + selectedRunId === null && + mode !== 'create'; + const showEmptyState = - !listLoading && reports.length === 0 && selectedRunId === null && mode !== 'create'; + !listLoading && + reports.length === 0 && + selectedRunId === null && + mode !== 'create' && + !isMarketingStateEnabled; const isCreateMode = mode === 'create'; const handleCreateSubmit = async (payload: PentestCreateRequest): Promise<{ id: string }> => { @@ -133,6 +151,14 @@ export function SplitView({ orgId, selectedRunId, mode = 'default' }: SplitViewP // main pane to <600px and the SevTally / detail header overflow. const showListOnMobile = selectedRunId === null && !isCreateMode; + if (isMarketingStateEnabled) { + return ( +
+ +
+ ); + } + // Empty state only shown when there are no runs AND no selection AND not // in create mode. Once there is at least one run we always render the // split view. diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/AttackCategoryGrid.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/AttackCategoryGrid.tsx new file mode 100644 index 000000000..48d284152 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/AttackCategoryGrid.tsx @@ -0,0 +1,24 @@ +import { ATTACK_CATEGORIES } from './attack-categories'; + +export function AttackCategoryGrid() { + return ( +
+ {ATTACK_CATEGORIES.map((c) => ( +
+
+ {c.code} +
+
+
{c.name}
+
+ {c.description} +
+
+
+ ))} +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniFindingCard.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniFindingCard.tsx new file mode 100644 index 000000000..ffbc00bd2 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniFindingCard.tsx @@ -0,0 +1,51 @@ +const FINDING = { + id: 'F-031', + cvss: 9.8, + cwe: 'CWE-89', + title: 'SQL injection in /api/v2/reports filter parameter', + endpoint: 'POST /api/v2/reports', + summary: + 'The status filter is interpolated into a SQL WHERE clause without parameterization. The allowlist regex permits an escape via UNION SELECT, exposing the full reports table across all tenants.', + found: 'discovered by sql-injection-agent · 6 min into scan', +}; + +const AGENT_LOG = `> 5 of 9 agents · 47 min elapsed · ETA ~1h +> sql-injection-agent: response-time delta 5.02s +> exploit confirmed: UNION SELECT ✓ +> writing PoC + remediation diff`; + +/** + * The "what a finding looks like" sample card. Mirrors the real finding-detail + * surface from the pentest product so the marketing screenshot feels like the + * actual app, not a polished mockup. + */ +export function MiniFindingCard() { + return ( +
+
+ + CRITICAL · {FINDING.cvss} + + {FINDING.cwe} + {FINDING.id} +
+
+
{FINDING.title}
+
+ {FINDING.endpoint} +
+

{FINDING.summary}

+
+          {AGENT_LOG}
+        
+
+
+ {FINDING.found} + + Maps to SOC 2 · CC7.1 and{' '} + ISO 27001 · A.12.6 + +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniRunList.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniRunList.tsx new file mode 100644 index 000000000..6015a81e7 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/MiniRunList.tsx @@ -0,0 +1,68 @@ +interface RunRow { + id: string; + target: string; + when: string; + state: 'running' | 'completed' | 'clean'; + count: number; +} + +const ROWS: RunRow[] = [ + { id: 'PT-0042', target: 'app.staging.trycomp.ai', when: '2 min ago', state: 'running', count: 7 }, + { id: 'PT-0041', target: 'app.trycomp.ai', when: 'Apr 22', state: 'completed', count: 6 }, + { id: 'PT-0040', target: 'api.trycomp.ai', when: 'Apr 20', state: 'completed', count: 3 }, + { id: 'PT-0038', target: 'app.trycomp.ai', when: 'Apr 17', state: 'clean', count: 0 }, +]; + +/** + * Pen-test-specific decorative "recent scans" preview rendered inside the + * marketing hero. The values are deliberately plausible — not real data — so + * the screenshot reads as a real product surface, not a glossy mockup. + */ +export function MiniRunList() { + return ( +
+
+
+ + Recent scans + + + {ROWS.length} + +
+ {ROWS[0].target} +
+ {ROWS.map((r, i) => ( +
+ {r.id} + {r.target} + {r.state === 'running' && ( + + + RUNNING + + )} + {r.state === 'completed' && ( + + {r.count} FINDINGS + + )} + {r.state === 'clean' && ( + + CLEAN + + )} + {r.when} +
+ ))} +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/PenTestMarketingEmptyState.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/PenTestMarketingEmptyState.tsx new file mode 100644 index 000000000..d0910f234 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/PenTestMarketingEmptyState.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { + MarketingEmptyState, + MarketingHeader, + MarketingHelpBar, + MarketingHero, + MarketingSection, + MarketingSteps, + type MarketingStep, +} from '@/components/marketing-empty-state'; +import { Button } from '@trycompai/design-system'; +import { ArrowRight } from '@trycompai/design-system/icons'; +import { AttackCategoryGrid } from './AttackCategoryGrid'; +import { MiniFindingCard } from './MiniFindingCard'; +import { MiniRunList } from './MiniRunList'; +import { ReportPreviewSpread } from './ReportPreviewSpread'; + +const STEPS: MarketingStep[] = [ + { + number: '01', + title: 'Connect a target', + description: + 'Point us at a staging URL, production host, or VPN-bound endpoint. Optionally link a GitHub repo for source-aware testing.', + }, + { + number: '02', + title: '22 agents go to work', + description: + 'Specialist agents probe injection, auth, access, headers, crypto, and logic flaws concurrently. Scans run in 1–3 hours.', + }, + { + number: '03', + title: 'Validated findings', + description: + 'Every finding ships with an exploit PoC, blast-radius analysis, and step-by-step remediation mapped to your frameworks.', + }, +]; + +interface PenTestMarketingEmptyStateProps { + onViewPlans: () => void; +} + +export function PenTestMarketingEmptyState({ onViewPlans }: PenTestMarketingEmptyStateProps) { + const viewPlansButton = ( + + ); + + return ( + + + + } + previewAnnotation="Preview · your workspace, after a few scans" + /> + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportCover.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportCover.tsx new file mode 100644 index 000000000..9b837e3f7 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportCover.tsx @@ -0,0 +1,70 @@ +import { Meta, ReportPage } from './report-page'; + +export function ReportCover() { + return ( + +
+ C O M P  A I + CONFIDENTIAL +
+
+ yourapp.example.com +
+
+ Web Application Penetration Test +
+
Security Assessment Report
+
+ PENETRATION TEST REPORT +
+
+ + + + +
+
+ Comp AI — Penetration Test Report + 1 of 33 +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportDetailPage.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportDetailPage.tsx new file mode 100644 index 000000000..285e0dfd0 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportDetailPage.tsx @@ -0,0 +1,94 @@ +import { FieldRow, ReportPage } from './report-page'; + +export function ReportDetailPage() { + return ( + +
+
+ CORS VULN 01 +
+
+ HIGH +
+
+
+ CORS Misconfiguration with Credentials +
+ + +
+ EVIDENCE +
+
+        {`HTTP/1.1 200 OK
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Credentials: true
+Access-Control-Allow-Methods: GET, POST,`}
+      
+ +
+ Effort: Low · Priority: HIGH + 8 of 33 +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportExecSummary.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportExecSummary.tsx new file mode 100644 index 000000000..a58fe5620 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportExecSummary.tsx @@ -0,0 +1,108 @@ +import { ReportPage } from './report-page'; + +const COUNTERS: Array<{ n: number; l: string; c: string }> = [ + { n: 0, l: 'CRITICAL', c: '#b8290c' }, + { n: 3, l: 'HIGH', c: '#c25b1f' }, + { n: 2, l: 'MEDIUM', c: '#8a6b1a' }, + { n: 2, l: 'LOW', c: '#3c6b8a' }, + { n: 1, l: 'INFO', c: '#777' }, +]; + +export function ReportExecSummary() { + return ( + +
+ 1. EXECUTIVE SUMMARY +
+
+ This penetration test was conducted on the staging environment to identify security + vulnerabilities and misconfigurations that could be exploited by malicious actors. +
+
+ OVERALL RISK · HIGH +
+
+ KEY FINDINGS +
+ +
+ {COUNTERS.map((s) => ( +
+
+ {s.n} +
+
+ {s.l} +
+
+ ))} +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportPreviewSpread.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportPreviewSpread.tsx new file mode 100644 index 000000000..363795a98 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/ReportPreviewSpread.tsx @@ -0,0 +1,66 @@ +import { Checkmark } from '@trycompai/design-system/icons'; +import { ReportCover } from './ReportCover'; +import { ReportDetailPage } from './ReportDetailPage'; +import { ReportExecSummary } from './ReportExecSummary'; + +const CHECKLIST = [ + 'Cover sheet · assessment period · reference', + 'Per-finding evidence, PoC, and remediation', + 'Executive summary with severity counter', + 'Findings table with confirmed / potential', + 'Framework control mapping appendix', + 'Reissued on retest, no extra cost', +]; + +const PAGES = [ + { node: , label: '1 · Cover' }, + { node: , label: '2 · Executive summary' }, + { node: , label: '8 · Finding detail' }, +]; + +/** + * The "what you take away" deliverable section — a muted card containing a + * centered intro, three scaled-down PDF page thumbnails, and a checklist of + * what ships inside the report. Pen-test-specific because the page mocks + * mirror the actual pentest report template. + */ +export function ReportPreviewSpread() { + return ( +
+
+
+ The deliverable +
+
+ Every completed scan ships a signed PDF report — the same document you'd give an + external pentest firm. +
+

+ Executive summary, scope & methodology, findings table, per-finding detail with + evidence, remediation, and references — wired to your SOC 2 or ISO 27001 controls. + Generated at the end of every scan and re-issued on retest. +

+
+
+ {PAGES.map((p) => ( +
+ {p.node} +
+ {p.label} +
+
+ ))} +
+
    + {CHECKLIST.map((t) => ( +
  • + + + + {t} +
  • + ))} +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/attack-categories.ts b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/attack-categories.ts new file mode 100644 index 000000000..5f42fd6bb --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/attack-categories.ts @@ -0,0 +1,20 @@ +export interface AttackCategory { + code: string; + name: string; + description: string; +} + +export const ATTACK_CATEGORIES: AttackCategory[] = [ + { code: 'INJ', name: 'Injection', description: 'SQL, NoSQL, OS command, LDAP, template' }, + { code: 'AUTH', name: 'Broken Auth', description: 'Session fixation, token leak, weak MFA' }, + { code: 'AUTHZ', name: 'Broken Access', description: 'IDOR, privilege escalation, multi-tenant' }, + { code: 'XSS', name: 'Cross-Site Scripting', description: 'Reflected, stored, DOM-based' }, + { code: 'CSRF', name: 'CSRF', description: 'State-changing requests without proof' }, + { code: 'SSRF', name: 'SSRF', description: 'Outbound requests via user-controlled URLs' }, + { code: 'XXE', name: 'XML / XXE', description: 'External entity, billion-laughs' }, + { code: 'RL', name: 'Rate Limits', description: 'Credential stuffing, enumeration' }, + { code: 'CFG', name: 'Misconfiguration', description: 'Headers, CORS, exposed admin routes' }, + { code: 'CRY', name: 'Cryptography', description: 'TLS posture, weak ciphers, JWT bugs' }, + { code: 'LEAK', name: 'Information Leak', description: 'Stack traces, version banners, debug data' }, + { code: 'LOG', name: 'Logic Flaws', description: 'Race conditions, workflow bypass' }, +]; diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/report-page.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/report-page.tsx new file mode 100644 index 000000000..f5cc533e7 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/pen-test-marketing/report-page.tsx @@ -0,0 +1,61 @@ +import type { CSSProperties, ReactNode } from 'react'; + +const PAGE_STYLE: CSSProperties = { + width: 240, + height: 312, + background: '#ffffff', + border: '1px solid var(--border)', + borderRadius: 4, + boxShadow: '0 1px 2px rgba(0,0,0,0.06), 0 8px 24px -10px rgba(0,0,0,0.18)', + padding: '20px 18px', + display: 'flex', + flexDirection: 'column', + color: '#111', + fontSize: 8, + lineHeight: 1.35, + overflow: 'hidden', + flex: 'none', + position: 'relative', +}; + +export function ReportPage({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function Meta({ label, value }: { label: string; value: string }) { + return ( +
+
+ {label} +
+
{value}
+
+ ); +} + +export function FieldRow({ label, value }: { label: string; value: string }) { + return ( +
+
+ {label} +
+
{value}
+
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/LockPill.tsx b/apps/app/src/components/marketing-empty-state/LockPill.tsx new file mode 100644 index 000000000..bed6ca640 --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/LockPill.tsx @@ -0,0 +1,14 @@ +import { Locked } from '@trycompai/design-system/icons'; + +interface LockPillProps { + label: string; +} + +export function LockPill({ label }: LockPillProps) { + return ( + + + {label} + + ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingEmptyState.tsx b/apps/app/src/components/marketing-empty-state/MarketingEmptyState.tsx new file mode 100644 index 000000000..a3c52c8ec --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingEmptyState.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react'; + +interface MarketingEmptyStateProps { + children: ReactNode; +} + +/** + * Universal container for marketing/upsell empty states. Vertical stack with + * the page padding + section gap shared across every feature that adopts the + * pattern. Content is intentionally slot-based — this component knows nothing + * about plans, scans, or any feature-specific gating. Each feature renders it + * conditionally based on its own `isMarketingStateEnabled` boolean. + */ +export function MarketingEmptyState({ children }: MarketingEmptyStateProps) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingHeader.tsx b/apps/app/src/components/marketing-empty-state/MarketingHeader.tsx new file mode 100644 index 000000000..0cecb1879 --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingHeader.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react'; +import { LockPill } from './LockPill'; + +interface MarketingHeaderProps { + /** Small "Add-on" / "Beta" / etc. lock pill on the top row. Optional. */ + lockLabel?: string; + /** Muted mono status line next to the pill, e.g. "No active plan". Optional. */ + statusText?: string; + /** Page h1. */ + title: string; + /** Muted description paragraph below the title. */ + description: string; + /** Right-aligned action row — usually one or two Buttons. */ + actions?: ReactNode; +} + +export function MarketingHeader({ + lockLabel, + statusText, + title, + description, + actions, +}: MarketingHeaderProps) { + return ( +
+
+ {(lockLabel || statusText) && ( +
+ {lockLabel && } + {statusText && ( + + {statusText} + + )} +
+ )} +

+ {title} +

+

+ {description} +

+
+ {actions &&
{actions}
} +
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingHelpBar.tsx b/apps/app/src/components/marketing-empty-state/MarketingHelpBar.tsx new file mode 100644 index 000000000..3ce279459 --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingHelpBar.tsx @@ -0,0 +1,24 @@ +import { Help } from '@trycompai/design-system/icons'; +import type { ReactNode } from 'react'; + +interface MarketingHelpBarProps { + title: string; + body: string; + /** Right-aligned action row, usually one or two Buttons. */ + actions?: ReactNode; +} + +export function MarketingHelpBar({ title, body, actions }: MarketingHelpBarProps) { + return ( +
+
+ +
+
+
{title}
+
{body}
+
+ {actions &&
{actions}
} +
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingHero.tsx b/apps/app/src/components/marketing-empty-state/MarketingHero.tsx new file mode 100644 index 000000000..8cc5702bb --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingHero.tsx @@ -0,0 +1,82 @@ +import type { ReactNode } from 'react'; + +interface MarketingHeroProps { + /** ALL-CAPS small eyebrow above the headline. Optional. */ + eyebrow?: string; + /** Headline content. Use `accentWord` to highlight a single word in primary. */ + titleBefore?: string; + accentWord?: string; + titleAfter?: string; + /** One or more body paragraphs. First gets foreground color, rest are muted. */ + paragraphs: ReactNode[]; + /** Right-aligned action row. */ + actions?: ReactNode; + /** Right-column slot — feature-specific preview (mini list, screenshot, etc.). */ + preview?: ReactNode; + /** Small ALL-CAPS pill anchored to the top-left of the preview slot. */ + previewAnnotation?: string; +} + +/** + * 60/40 hero band — left side carries the marketing copy, right side carries + * a feature-supplied preview surface. Feature owns the preview content; this + * component only owns the layout, padding, and annotation pill position. + */ +export function MarketingHero({ + eyebrow, + titleBefore, + accentWord, + titleAfter, + paragraphs, + actions, + preview, + previewAnnotation, +}: MarketingHeroProps) { + const [first, ...rest] = paragraphs; + return ( +
+
+ {eyebrow && ( +
+ {eyebrow} +
+ )} +

+ {titleBefore} + {accentWord && ( + <> + {titleBefore ? ' ' : ''} + {accentWord} + {titleAfter ? ' ' : ''} + + )} + {titleAfter} +

+ {first !== undefined && ( +

+ {first} +

+ )} + {rest.map((paragraph, i) => ( +

+ {paragraph} +

+ ))} + {actions &&
{actions}
} +
+ {preview && ( +
+ {previewAnnotation && ( +
+ {previewAnnotation} +
+ )} + {preview} +
+ )} +
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingSection.tsx b/apps/app/src/components/marketing-empty-state/MarketingSection.tsx new file mode 100644 index 000000000..1ddba1689 --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingSection.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; + +interface MarketingSectionProps { + title: string; + /** Right-aligned muted note next to the section title. */ + note?: string; + children: ReactNode; +} + +/** + * The repeating "section header row + body" pattern. Renders an h3 with an + * optional right-aligned muted note, then the supplied children. Use it for + * "How it works", "What's included", and any analogous block. + */ +export function MarketingSection({ title, note, children }: MarketingSectionProps) { + return ( +
+
+

{title}

+ {note && ( + {note} + )} +
+ {children} +
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/MarketingSteps.tsx b/apps/app/src/components/marketing-empty-state/MarketingSteps.tsx new file mode 100644 index 000000000..cfc82e24c --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/MarketingSteps.tsx @@ -0,0 +1,36 @@ +export interface MarketingStep { + /** Step label, e.g. "01". Rendered with a "Step " prefix in mono. */ + number: string; + title: string; + description: string; +} + +interface MarketingStepsProps { + steps: MarketingStep[]; +} + +/** + * Three-column step grid used by "How it works"-style sections. Number of + * columns mirrors the step count up to 3, then wraps. Feature supplies the + * step content; layout and typography are fixed. + */ +export function MarketingSteps({ steps }: MarketingStepsProps) { + return ( +
+ {steps.map((step) => ( +
+
+ Step {step.number} +
+
{step.title}
+
+ {step.description} +
+
+ ))} +
+ ); +} diff --git a/apps/app/src/components/marketing-empty-state/index.ts b/apps/app/src/components/marketing-empty-state/index.ts new file mode 100644 index 000000000..9b90afd2a --- /dev/null +++ b/apps/app/src/components/marketing-empty-state/index.ts @@ -0,0 +1,8 @@ +export { LockPill } from './LockPill'; +export { MarketingEmptyState } from './MarketingEmptyState'; +export { MarketingHeader } from './MarketingHeader'; +export { MarketingHero } from './MarketingHero'; +export { MarketingSection } from './MarketingSection'; +export { MarketingSteps } from './MarketingSteps'; +export type { MarketingStep } from './MarketingSteps'; +export { MarketingHelpBar } from './MarketingHelpBar'; diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index cb75696ff..a0f564114 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -11771,7 +11771,8 @@ "task", "vendor", "risk", - "policy" + "policy", + "finding" ], "type": "string" } @@ -17111,6 +17112,152 @@ } } }, + "/v1/cloud-security/findings/{findingId}/exception": { + "post": { + "operationId": "CloudSecurityController_markFindingAsException_v1", + "parameters": [ + { + "name": "findingId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkExceptionDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "summary": "Mark a finding as an exception so it no longer appears in the active Scan Results list", + "tags": [ + "CloudSecurity" + ], + "description": "Mark a finding as an exception so it no longer appears in the active Scan Results list in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect cloud posture results to compliance work.", + "x-mint": { + "metadata": { + "title": "Mark a finding as an exception so it no | Comp AI API", + "sidebarTitle": "Mark a finding as an exception so it no longer appears in the active Scan Results list", + "description": "Mark a finding as an exception so it no longer appears in the active Scan Results list in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect.", + "og:title": "Mark a finding as an exception so it no | Comp AI API", + "og:description": "Mark a finding as an exception so it no longer appears in the active Scan Results list in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect." + } + } + } + }, + "/v1/cloud-security/exceptions/{exceptionId}": { + "delete": { + "operationId": "CloudSecurityController_revokeException_v1", + "parameters": [ + { + "name": "exceptionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Revoke an exception, reopening the finding", + "tags": [ + "CloudSecurity" + ], + "description": "Revoke an exception, reopening the finding in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect cloud posture results to compliance work.", + "x-mint": { + "metadata": { + "title": "Revoke an exception, reopening the finding | Comp AI API", + "sidebarTitle": "Revoke an exception, reopening the finding", + "description": "Revoke an exception, reopening the finding in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect.", + "og:title": "Revoke an exception, reopening the finding | Comp AI API", + "og:description": "Revoke an exception, reopening the finding in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect." + } + } + } + }, + "/v1/cloud-security/history": { + "get": { + "operationId": "CloudSecurityController_getHistory_v1", + "parameters": [ + { + "name": "connectionId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "summary": "List resolution, exception, and regression history for a connection", + "tags": [ + "CloudSecurity" + ], + "description": "List resolution, exception, and regression history for a connection in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect cloud posture results to compliance work.", + "x-mint": { + "metadata": { + "title": "List resolution, exception, and regression | Comp AI API", + "sidebarTitle": "List resolution, exception, and regression history for a connection", + "description": "List resolution, exception, and regression history for a connection in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services.", + "og:title": "List resolution, exception, and regression | Comp AI API", + "og:description": "List resolution, exception, and regression history for a connection in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services." + } + } + } + }, + "/v1/cloud-security/findings/{findingId}/check-definition": { + "get": { + "operationId": "CloudSecurityController_getCheckDefinition_v1", + "parameters": [ + { + "name": "findingId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Resolve the \"About this check\" description for a finding (AI-cached for AWS; provider-derived for GCP/Azure)", + "tags": [ + "CloudSecurity" + ], + "description": "Resolve the \"About this check\" description for a finding (AI-cached for AWS; provider-derived for GCP/Azure) in Comp AI. Run AWS, Azure, and GCP cloud security scans, detect enabled services, review findings, and connect cloud posture.", + "x-mint": { + "metadata": { + "title": "Resolve the \"About this check\" description | Comp AI API", + "sidebarTitle": "Resolve the \"About this check\" description for a finding (AI-cached for AWS; provider-derived for GCP/Azure)", + "description": "Resolve the \"About this check\" description for a finding (AI-cached for AWS; provider-derived for GCP/Azure) in Comp AI. Run AWS, Azure, and GCP cloud.", + "og:title": "Resolve the \"About this check\" description | Comp AI API", + "og:description": "Resolve the \"About this check\" description for a finding (AI-cached for AWS; provider-derived for GCP/Azure) in Comp AI. Run AWS, Azure, and GCP cloud." + } + } + } + }, "/v1/cloud-security/scan/{connectionId}": { "post": { "operationId": "CloudSecurityController_scan_v1", @@ -21667,6 +21814,16 @@ "type": "boolean", "description": "When true, this member is exempt from the org-level background check requirement and will count as complete in people scores.", "example": false + }, + "backgroundCheckExemptReason": { + "type": "string", + "description": "Reason code for the exemption (e.g. \"contractor_with_vendor_check\", \"other\"). Persisted alongside backgroundCheckExempt and cleared when the member becomes non-exempt.", + "example": "other" + }, + "backgroundCheckExemptJustification": { + "type": "string", + "description": "Free-text justification for the exemption, attached to the audit log. Cleared when the member becomes non-exempt.", + "example": "Contractor with existing background check on file from staffing agency." } } }, @@ -23797,7 +23954,8 @@ "task", "vendor", "risk", - "policy" + "policy", + "finding" ], "example": "task" }, @@ -24463,6 +24621,29 @@ "type": "object", "properties": {} }, + "MarkExceptionDto": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "Documentation for why this finding does not apply or is being accepted. Minimum 20 non-whitespace characters.", + "example": "Bucket hosts intentionally public marketing assets; writes restricted to the marketing IAM role." + }, + "reviewedBy": { + "type": "string", + "description": "Free-text reviewer or approval reference.", + "example": "Approved by CISO 2026-Q1" + }, + "expiresAt": { + "type": "string", + "description": "ISO date when this exception should auto-expire. Null/missing = never.", + "example": "2026-08-13" + } + }, + "required": [ + "reason" + ] + }, "TaskItemAssigneeDto": { "type": "object", "properties": {