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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 <MarketingEmptyState> 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 }> => {
Expand Down Expand Up @@ -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 (
<div className="pt-tokens -m-4 h-[calc(100vh-4rem)] md:-m-6">
<PenTestMarketingEmptyState onViewPlans={goToPentestPlans} />
</div>
);
}

// 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ATTACK_CATEGORIES } from './attack-categories';

export function AttackCategoryGrid() {
return (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{ATTACK_CATEGORIES.map((c) => (
<div
key={c.code}
className="flex items-start gap-3 rounded-sm border border-border bg-background px-3.5 py-3"
>
<div className="w-9 shrink-0 pt-0.5 font-mono text-[9px] font-bold uppercase tracking-[0.1em] text-primary">
{c.code}
</div>
<div className="min-w-0 flex-1">
<div className="break-words text-[13px] leading-snug">{c.name}</div>
<div className="break-words text-[11px] leading-[1.45] text-muted-foreground">
{c.description}
</div>
</div>
</div>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="overflow-hidden rounded-md border border-border bg-background shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
<div className="flex flex-wrap items-center gap-2 border-b border-border bg-muted/40 px-3 py-2.5 sm:gap-2.5 sm:px-4 sm:py-3">
<span className="rounded-sm bg-[oklch(0.55_0.22_27)] px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-[0.08em] text-white">
CRITICAL · {FINDING.cvss}
</span>
<span className="font-mono text-[11px] text-muted-foreground">{FINDING.cwe}</span>
<span className="ml-auto font-mono text-[11px] text-muted-foreground">{FINDING.id}</span>
</div>
<div className="px-3 py-3 sm:px-4 sm:py-3.5">
<div className="mb-1 text-[14px] leading-[1.35]">{FINDING.title}</div>
<div className="mb-2.5 font-mono text-[11px] text-muted-foreground">
{FINDING.endpoint}
</div>
<p className="mb-3 text-[12px] leading-[1.55] text-muted-foreground">{FINDING.summary}</p>
<pre className="overflow-x-auto rounded-sm bg-[#0b0b0b] px-3 py-2.5 font-mono text-[11px] leading-[1.5] text-[#d4d4d4]">
{AGENT_LOG}
</pre>
</div>
<div className="flex flex-col gap-1.5 border-t border-border bg-muted/30 px-3 py-2.5 sm:px-4">
<span className="font-mono text-[11px] text-muted-foreground">{FINDING.found}</span>
<span className="text-[11px] text-muted-foreground">
Maps to <span className="font-mono text-foreground">SOC 2 · CC7.1</span> and{' '}
<span className="font-mono text-foreground">ISO 27001 · A.12.6</span>
</span>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="overflow-hidden rounded-md border border-border bg-background shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
<div className="flex items-center justify-between border-b border-border bg-muted/40 px-3.5 py-2.5">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold uppercase tracking-[0.1em] text-muted-foreground">
Recent scans
</span>
<span className="rounded-sm bg-muted px-1.5 py-0.5 font-mono text-[10px] tabular-nums text-muted-foreground">
{ROWS.length}
</span>
</div>
<span className="font-mono text-[11px] text-muted-foreground">{ROWS[0].target}</span>
</div>
{ROWS.map((r, i) => (
<div
key={r.id}
className="grid grid-cols-[64px_minmax(0,1fr)_auto_auto] items-center gap-2.5 px-3 py-2.5 sm:grid-cols-[76px_minmax(0,1fr)_auto_auto] sm:gap-3 sm:px-3.5 sm:py-3"
style={{
borderBottom: i === ROWS.length - 1 ? undefined : '1px solid var(--border)',
background:
i === 0 ? 'color-mix(in oklab, var(--primary) 4%, transparent)' : undefined,
}}
>
<span className="font-mono text-[11px] text-muted-foreground">{r.id}</span>
<span className="min-w-0 truncate font-mono text-[12px]">{r.target}</span>
{r.state === 'running' && (
<span className="inline-flex items-center gap-1.5 rounded-sm bg-primary/12 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-[0.08em] text-primary">
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
RUNNING
</span>
)}
{r.state === 'completed' && (
<span className="rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-[0.08em] text-muted-foreground">
{r.count} FINDINGS
</span>
)}
{r.state === 'clean' && (
<span className="rounded-sm bg-emerald-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-[0.08em] text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300">
CLEAN
</span>
)}
<span className="font-mono text-[11px] text-muted-foreground">{r.when}</span>
</div>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 = (
<Button onClick={onViewPlans} iconRight={<ArrowRight />}>
View Plans
</Button>
);

return (
<MarketingEmptyState>
<MarketingHeader
lockLabel="Add-on"
statusText="No active plan"
title="Penetration Tests"
description="Continuous black-box testing of your applications and APIs. Findings ship with validated exploits, blast-radius, and remediation that maps to SOC 2 and ISO 27001."
actions={viewPlansButton}
/>

<MarketingHero
eyebrow="The Pen Test add-on"
titleBefore="Find vulnerabilities"
accentWord="before"
titleAfter="an attacker does."
paragraphs={[
'Twenty-two autonomous agents test your surface like an experienced attacker — concurrently. A typical scan completes in one to three hours and surfaces validated findings with proof.',
"Scans run on push, on schedule, or on demand. You don't need to staff a security team to run them.",
]}
actions={viewPlansButton}
preview={<MiniRunList />}
previewAnnotation="Preview · your workspace, after a few scans"
/>

<MarketingSection title="How it works" note="Three steps from connect to attestation.">
<MarketingSteps steps={STEPS} />
</MarketingSection>

<MarketingSection
title="What gets tested"
note="OWASP Top 10 plus the categories your auditor will ask about."
>
<AttackCategoryGrid />
</MarketingSection>

<MarketingSection
title="What a finding looks like"
note="From a real scan — every detail is what you'd ship to an auditor."
>
<MiniFindingCard />
</MarketingSection>

<MarketingSection
title="What you take away"
note="A 33-page report your auditor can read end-to-end."
>
<ReportPreviewSpread />
</MarketingSection>

<MarketingHelpBar
title="Have questions about scope, scheduling, or pricing?"
body="A Comp engineer will scope your environment and recommend a plan. Typically 20 minutes."
actions={viewPlansButton}
/>
</MarketingEmptyState>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Meta, ReportPage } from './report-page';

export function ReportCover() {
return (
<ReportPage>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: 6.5,
fontWeight: 700,
letterSpacing: '0.32em',
color: '#3c3c3c',
marginBottom: 26,
}}
>
<span>C O M P&nbsp;&nbsp;A I</span>
<span style={{ color: '#777' }}>CONFIDENTIAL</span>
</div>
<div style={{ fontSize: 7, color: '#777', marginBottom: 4, fontFamily: 'var(--font-mono)' }}>
yourapp.example.com
</div>
<div
style={{
fontSize: 13,
fontWeight: 500,
letterSpacing: '-0.01em',
lineHeight: 1.15,
marginBottom: 4,
color: '#111',
}}
>
Web Application Penetration Test
</div>
<div style={{ fontSize: 8, color: '#555', marginBottom: 28 }}>Security Assessment Report</div>
<div
style={{
fontSize: 6,
fontWeight: 700,
letterSpacing: '0.22em',
color: '#888',
paddingBottom: 6,
borderBottom: '0.5px solid #ddd',
marginBottom: 12,
}}
>
PENETRATION TEST REPORT
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 4, fontSize: 7, color: '#333' }}>
<Meta label="Assessment Period" value="May 5, 2026 — May 5, 2026" />
<Meta label="Report Date" value="May 5, 2026" />
<Meta label="Version" value="1.0" />
<Meta label="Reference" value="pentest-1777989012736" />
</div>
<div
style={{
marginTop: 'auto',
display: 'flex',
justifyContent: 'space-between',
fontSize: 5.5,
color: '#888',
}}
>
<span>Comp AI — Penetration Test Report</span>
<span>1 of 33</span>
</div>
</ReportPage>
);
}
Loading
Loading