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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
-->

<p align="center">
<img src="docs-site/public/logo.png" alt="OctoC2" width="260" />
<img src="docs-site/public/logo.png" alt="OctoC2" width="360" />
</p>

<p align="center">
Expand Down
203 changes: 203 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion docs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"preview": "vite preview"
},
"dependencies": {
"react-markdown": "10.1.0",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"rehype-slug": "6.0.0",
"remark-gfm": "4.0.1"
},
"devDependencies": {
"@eslint/js": "9.39.4",
Expand Down
94 changes: 12 additions & 82 deletions docs-site/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState } from 'react'
import { DocumentationPage } from './DocumentationPage'
import { DOCUMENTATION, DOCUMENTATION_BY_ID, documentationUrl } from './docs'

const owner = import.meta.env.VITE_GITHUB_OWNER || 'dstours'
const repo = import.meta.env.VITE_GITHUB_REPO || 'OctoC2'
const repositoryUrl = `https://github.com/${owner}/${repo}`
const docsUrl = (path: string) => `${repositoryUrl}/blob/main/${path}`
const logoUrl = `${import.meta.env.BASE_URL}logo.png`

const installCommand = `bun install --frozen-lockfile
Expand Down Expand Up @@ -75,81 +76,6 @@ const transportGroups = [
},
]

const guides = [
{
title: 'Installation',
description: 'Install the pinned toolchain, run each component, and build platform beacon binaries.',
href: docsUrl('docs/INSTALLATION.md'),
label: 'Install',
},
{
title: 'GitHub setup',
description: 'Configure repositories, the GitHub App, least-privilege permissions, PAT roles, and rotation.',
href: docsUrl('docs/GITHUB_SETUP.md'),
label: 'Provision',
},
{
title: 'Quickstart',
description: 'Build and import a pre-enrolled beacon, then verify an accepted ping result.',
href: docsUrl('docs/QUICKSTART.md'),
label: 'First run',
},
{
title: 'Architecture',
description: 'Understand components, identity boundaries, task lifecycle, durable state, and recovery.',
href: docsUrl('docs/ARCHITECTURE.md'),
label: 'Learn',
},
{
title: 'Channel guide',
description: 'Compare every transport, permission, prerequisite, priority rule, and qualification step.',
href: docsUrl('docs/CHANNELS.md'),
label: 'Transports',
},
{
title: 'Configuration',
description: 'Look up controller, listener, beacon, OIDC, recovery, dashboard, and CLI settings.',
href: docsUrl('docs/CONFIGURATION.md'),
label: 'Reference',
},
{
title: 'CLI reference',
description: 'Use setup, enrollment, builds, inventory, tasks, results, proxy, and service commands.',
href: docsUrl('docs/CLI.md'),
label: 'Operate',
},
{
title: 'Operations & assurance',
description: 'Listener policy, lifecycle behavior, replay protection, and certificate handling.',
href: docsUrl('docs/PRODUCTION.md'),
label: 'Operate safely',
},
{
title: 'Recovery',
description: 'Provision signed recovery records and short-lived GitHub App token leases.',
href: docsUrl('docs/RECOVERY.md'),
label: 'Configure recovery',
},
{
title: 'Troubleshooting',
description: 'Diagnose GitHub errors, decrypt failures, acknowledgements, proxy, TLS, gRPC, OIDC, and state.',
href: docsUrl('docs/TROUBLESHOOTING.md'),
label: 'Diagnose',
},
{
title: 'Development',
description: 'Work with shared contracts, tests, builds, generated protocol bindings, and change checks.',
href: docsUrl('docs/DEVELOPMENT.md'),
label: 'Contribute',
},
{
title: 'Verification evidence',
description: 'Trace implementation decisions to tests, live qualifications, and cleanup records.',
href: docsUrl('docs/REMEDIATION_TRACEABILITY.md'),
label: 'Review evidence',
},
]

function CopyButton({ value }: { value: string }) {
const [status, setStatus] = useState<'idle' | 'copied' | 'blocked'>('idle')

Expand Down Expand Up @@ -211,6 +137,10 @@ function SectionHeading({
}

function App() {
const guideId = new URLSearchParams(window.location.search).get('guide')
const selectedGuide = guideId ? DOCUMENTATION_BY_ID.get(guideId) : undefined
if (selectedGuide) return <DocumentationPage entry={selectedGuide} />

return (
<div className="site-shell">
<header className="topbar">
Expand Down Expand Up @@ -341,7 +271,7 @@ function App() {
<strong>Dashboard address</strong>
<p>Vite binds to <code>127.0.0.1:5173</code>. The controller’s HTTPS and gRPC listeners remain disabled until their explicit enable flags and TLS material are present.</p>
</div>
<a href={docsUrl('docs/QUICKSTART.md')}>Open the complete quickstart →</a>
<a href={documentationUrl('quickstart')}>Open the complete quickstart →</a>
</div>
</section>

Expand Down Expand Up @@ -392,7 +322,7 @@ function App() {
<li><span>✓</span> Delivery leases and replay state survive restarts.</li>
<li><span>✓</span> Unsigned remote modules are rejected.</li>
</ul>
<a href={docsUrl('docs/PRODUCTION.md')}>Read the operating model →</a>
<a href={documentationUrl('operations')}>Read the operating model →</a>
</div>
</section>

Expand All @@ -408,7 +338,7 @@ function App() {
<div><span>1</span><p><strong>Policy</strong><small>Dependencies, workflows, toolchains, generated proto, and documentation stay aligned.</small></p></div>
<div><span>2</span><p><strong>Behavior</strong><small>Bun tests cover signatures, replay handling, delivery ownership, persistence, and transport behavior.</small></p></div>
<div><span>3</span><p><strong>Artifacts</strong><small>Builds and smoke tests verify the dashboard, CLI, controller, proxy, and target beacon binaries.</small></p></div>
<a className="text-link" href={docsUrl('docs/REMEDIATION_TRACEABILITY.md')}>Review verification traceability →</a>
<a className="text-link" href={documentationUrl('verification')}>Review verification traceability →</a>
</div>
</div>
</section>
Expand All @@ -420,8 +350,8 @@ function App() {
description="Follow the first-run path or jump directly to setup, channels, configuration, operations, recovery, troubleshooting, and development references."
/>
<div className="guide-grid">
{guides.map((guide) => (
<a className="guide-card" href={guide.href} key={guide.title}>
{DOCUMENTATION.map((guide) => (
<a className="guide-card" href={documentationUrl(guide.id)} key={guide.title}>
<span className="guide-label">{guide.label}</span>
<h3>{guide.title}</h3>
<p>{guide.description}</p>
Expand All @@ -438,7 +368,7 @@ function App() {
<div><strong>OctoC2</strong><span>Encrypted multi-channel operations</span></div>
</div>
<div className="footer-links">
<a href={docsUrl('docs/README.md')}>Documentation index</a>
<a href={documentationUrl('documentation')}>Documentation index</a>
<a href="#quickstart">Quickstart</a>
<a href="#security">Security</a>
<a href="#verification">Verification</a>
Expand Down
197 changes: 197 additions & 0 deletions docs-site/src/DocumentationPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { useEffect, useState, type ReactNode } from 'react'
import ReactMarkdown, { type Components } from 'react-markdown'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import {
DOCUMENTATION,
DOCUMENTATION_ID_BY_SOURCE,
documentationUrl,
type DocumentationEntry,
} from './docs'

const owner = import.meta.env.VITE_GITHUB_OWNER || 'dstours'
const repo = import.meta.env.VITE_GITHUB_REPO || 'OctoC2'
const repositoryUrl = `https://github.com/${owner}/${repo}`
const logoUrl = `${import.meta.env.BASE_URL}logo.png`

function normalizePath(path: string): string {
const output: string[] = []
for (const part of path.replaceAll('\\', '/').split('/')) {
if (!part || part === '.') continue
if (part === '..') output.pop()
else output.push(part)
}
return output.join('/')
}

function resolveArticleLink(
current: DocumentationEntry,
href: string,
): { href: string; external: boolean } {
if (!href || href.startsWith('#')) return { href, external: false }
if (/^(https?:|mailto:)/i.test(href)) return { href, external: true }

const hashAt = href.indexOf('#')
const path = hashAt >= 0 ? href.slice(0, hashAt) : href
const anchor = hashAt >= 0 ? href.slice(hashAt) : ''
const directory = current.sourcePath.split('/').slice(0, -1).join('/')
const sourcePath = normalizePath(`${directory}/${path}`)
const articleId = DOCUMENTATION_ID_BY_SOURCE.get(sourcePath)

if (articleId) return { href: documentationUrl(articleId, anchor), external: false }
return {
href: `${repositoryUrl}/blob/main/${sourcePath}${anchor}`,
external: true,
}
}

function plainHeading(markdown: string): string {
return markdown
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
.replace(/[`*_~]/g, '')
.trim()
}

function headingSlug(markdown: string): string {
return plainHeading(markdown)
.toLowerCase()
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
}

function tableOfContents(content: string) {
return content
.split('\n')
.flatMap((line) => {
const match = /^(#{2,3})\s+(.+?)\s*$/.exec(line)
if (!match?.[1] || !match[2]) return []
const title = plainHeading(match[2])
return [{ level: match[1].length, title, id: headingSlug(match[2]) }]
})
}

function DocumentationHeader() {
return (
<header className="topbar">
<div className="topbar-inner">
<a className="brand" href={import.meta.env.BASE_URL} aria-label="OctoC2 documentation home">
<img src={logoUrl} alt="" />
<span>OctoC2</span>
<span className="brand-divider" />
<span className="brand-context">Docs</span>
</a>
<nav className="topnav docs-topnav" aria-label="Documentation navigation">
<a href={import.meta.env.BASE_URL}>Overview</a>
<a href={documentationUrl('documentation')}>All guides</a>
</nav>
<a className="repo-link" href={repositoryUrl}>GitHub <span aria-hidden="true">↗</span></a>
</div>
</header>
)
}

function GuideNavigation({ currentId }: { currentId: string }) {
const categories = ['Start', 'Operate', 'Resilience', 'Engineering'] as const
return (
<nav className="docs-sidebar" aria-label="Documentation guides">
<a className="docs-overview-link" href={import.meta.env.BASE_URL}>← Documentation home</a>
<div className="docs-nav-links">
{categories.map((category) => (
<section key={category}>
<h2>{category}</h2>
{DOCUMENTATION.filter((entry) => entry.category === category).map((entry) => (
<a
className={entry.id === currentId ? 'active' : undefined}
href={documentationUrl(entry.id)}
key={entry.id}
aria-current={entry.id === currentId ? 'page' : undefined}
>
{entry.title}
</a>
))}
</section>
))}
</div>
</nav>
)
}

function ArticleTableOfContents({ content }: { content: string }) {
const headings = tableOfContents(content)
return (
<aside className="docs-toc" aria-label="On this page">
<strong>On this page</strong>
{headings.map((heading) => (
<a className={heading.level === 3 ? 'nested' : undefined} href={`#${heading.id}`} key={`${heading.level}-${heading.id}`}>
{heading.title}
</a>
))}
</aside>
)
}

export function DocumentationPage({ entry }: { entry: DocumentationEntry }) {
const [content, setContent] = useState('')
const [loadError, setLoadError] = useState(false)

useEffect(() => {
document.title = `${entry.title} — OctoC2 Documentation`
let active = true
entry.load()
.then((markdown) => {
if (active) setContent(markdown)
})
.catch(() => {
if (active) setLoadError(true)
})
return () => { active = false }
}, [entry])

const components: Components = {
a({ href = '', children }: { href?: string; children?: ReactNode }) {
const resolved = resolveArticleLink(entry, href)
return (
<a
href={resolved.href}
{...(resolved.external && { target: '_blank', rel: 'noreferrer' })}
>
{children}
</a>
)
},
}
const renderedContent = content.replace(/^> \[!IMPORTANT\]\r?\n/gm, '')

return (
<div className="site-shell docs-reader">
<DocumentationHeader />
<aside className="notice" role="note">
<div>
<span className="notice-mark" aria-hidden="true">i</span>
<p><strong>Authorized use only.</strong> Run OctoC2 only on systems and repositories you own or have explicit permission to test.</p>
</div>
</aside>
<main className="docs-layout">
<GuideNavigation currentId={entry.id} />
<article className="docs-article">
<div className="docs-article-meta">
<span>{entry.category}</span>
<a href={`${repositoryUrl}/blob/main/${entry.sourcePath}`} target="_blank" rel="noreferrer">View source ↗</a>
</div>
{loadError ? (
<p className="docs-load-error">This guide could not be loaded. Return to the documentation index and try again.</p>
) : content ? (
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]} components={components}>
{renderedContent}
</ReactMarkdown>
) : (
<div className="docs-loading" role="status">Loading guide…</div>
)}
</article>
<ArticleTableOfContents content={content} />
</main>
</div>
)
}
Loading
Loading