added product page

This commit is contained in:
Felix Klement committed 2026-09-18 16:56:44 +02:00
1 parent 4cda1ca41b
commit d97f541738
27 files changed
+1004 -69

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+2
View File
@@ -1,3 +1,5 @@
node_modules/
dist/
.astro/
# The product source is mounted here for reference only; it carries its own repository and .env files.
product_repo/
+2 -1
View File
@@ -5,7 +5,8 @@ security research and works with the University of Passau, the xG-ALOE alliance
with funding from the Federal Ministry of Research, Technology and Space. Eight people: two founders
(Dr. Felix Klement, CEO; Dr. Florian Frank, CTO), engineers, business development, working students.
**What they sell.** Bespoke security for complex infrastructure in four stages: threat analysis and
**What they sell.** Security engineering for software, mainly: the software behind complex
infrastructure. Four stages: threat analysis and
risk assessment (automated, in CI/CD), security engineering (zero trust, key management, secure APIs),
security verification (independent review, hardening, testing) and monitoring and lifecycle.
Sectors: Open RAN and private 5G, critical network infrastructure, cloud-native systems.
+27 -3
View File
@@ -25,6 +25,8 @@ npm run check # Astro + TypeScript check
| `src/components/home/*` | One file per home section, in page order: Hero, Manifesto, Method (with `MethodDiagram`), Stages (with `GridObject`), Sectors, Proof, Quote, Insights, Contact. |
| `src/components/home/GridObject.astro` | The four wireframe objects on the stage cards (sphere, cube, plane, wave). Geometry is computed at build time; single stroke in `--ink`. |
| `src/components/home/MethodDiagram.astro` | The four-stage loop, wide and tall compositions. Labels come from `ui.method.stages`. |
| `src/components/verifier/*` | The CipherVerifier product page, in page order: Hero (line burst), Intro (image band), HowItWorks (tabs, one schematic per group), Cards, Facts, Outro. |
| `src/components/verifier/diagrams.ts` | The five schematics as data: a hand-placed wide layout and an auto-generated tall spine per group. `Schematic.astro` renders either. Labels come from `ui.verifier.how.groups`; the tuple shape is in `types.ts`. |
| `scripts/*.mjs` | Review tooling: `shot.mjs` (full page), `scroll-shots.mjs` (viewport by viewport), `tabs-test.mjs` (keyboard test for the sectors tabs), `contact-sheet.mjs`. Need `npx astro preview --port 4322`. |
| `src/pages/_*.astro` | Shared page bodies. `index.astro` and `de/index.astro` render `_Home.astro` with a `lang` prop, and so on. |
| `src/assets/images/` | Source images. Astro emits AVIF with a WebP fallback and explicit dimensions. |
@@ -32,7 +34,7 @@ npm run check # Astro + TypeScript check
## Routes
`/`, `/team`, `/insights`, `/insights/<slug>`, `/imprint`, `/privacy`, `/404` and the same under `/de`.
`/`, `/cipherverifier`, `/team`, `/insights`, `/insights/<slug>`, `/imprint`, `/privacy`, `/404` and the same under `/de`.
`hreflang` alternates and a sitemap are generated. The language switch sits in the footer.
## Things the host has to provide
@@ -70,8 +72,30 @@ objects on the stage cards were prototyped as a Magnific sheet
(https://www.magnific.com/app/creation/aFsD5crfSh) and then rebuilt as procedural SVG so the
stroke matches the rest of the site.
## Product page
`/cipherverifier` describes the pipeline in `product_repo/monocipher` (mounted for reference, ignored
by git). Everything on the page was read from that repository on 18 September 2026:
- The five stages and their order come from `orchestrator/pipeline_plan.py`.
- The seventeen analysers are rayfusion's built-in plugins plus converters; the fourteen languages
are the `Technology` enum in `rayfusion/plugins/base.py`. The security profile is described in
`docs/rayfusion-rule-policy.md`.
- The two-stage classification, escalation rules, budgets and replay-validated cache are from
`docs/polygraph-classification-pipeline.md`.
- 970 is the number of entries in `sarifcombiner/cwe_information.json`.
- SBOM merging (Syft and cdxgen, package-URL deduplication, CycloneDX 1.6) is `spectograph/backend`,
Dependency-Track upload and the ephemeral container are `deptrace/commands`.
- Git providers, archive upload, CI webhook, scoped settings, live pipeline, PDF report and artefacts
are the `cipherverifier` Django app; the installer is `cvm`.
When the product changes, update `ui.verifier` (copy and diagram labels) and, if a stage or tool is
added, the layouts in `diagrams.ts`. Run `node scripts/tabs-shots.mjs <url> <prefix> '[data-how]'`
to render every schematic for review.
## Interaction
Two islands of script: the mobile navigation toggle in `Header.astro` and the sectors tabs in
`Sectors.astro` (ARIA tablist, arrow keys, Home and End). Without JavaScript every sector panel
Two islands of script: the mobile navigation toggle in `Header.astro` and the tab lists in
`Sectors.astro` and `HowItWorks.astro`, which share `src/scripts/tabs.ts` (ARIA tablist, arrow
keys, Home and End). Without JavaScript every sector panel
and its image is shown in full; the `html.js` class set in `Base.astro` switches to one at a time.
+18
View File
@@ -0,0 +1,18 @@
import { chromium } from 'playwright';
const [,, url, prefix, selector, width = '390'] = process.argv;
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: Number(width), height: 900 } });
await page.goto(url, { waitUntil: 'networkidle' });
const root = page.locator(selector);
const tabs = root.locator('[role="tab"]');
const n = await tabs.count();
for (let i = 0; i < n; i++) {
await tabs.nth(i).click();
await page.waitForTimeout(500);
const panel = root.locator('[role="tabpanel"][data-open]').first();
await panel.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
await panel.screenshot({ path: `${prefix}-${i}.png` });
}
console.log('panels', n);
await browser.close();
+18
View File
@@ -0,0 +1,18 @@
import { chromium } from 'playwright';
const [,, url, prefix, selector, width = '1440'] = process.argv;
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: Number(width), height: 1100 } });
await page.goto(url, { waitUntil: 'networkidle' });
const root = page.locator(selector);
const tabs = root.locator('[role="tab"]');
const n = await tabs.count();
for (let i = 0; i < n; i++) {
await tabs.nth(i).click();
await page.waitForTimeout(450);
const stage = root.locator('[data-stage-item][data-active]').first();
await stage.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
await stage.screenshot({ path: `${prefix}-${i}.png` });
}
console.log('tabs', n);
await browser.close();
+1 -1
View File
@@ -8,7 +8,7 @@ await tabs.nth(0).focus();
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(500);
const selected = await page.evaluate(() => Array.from(document.querySelectorAll('[role="tab"]')).map(t => t.getAttribute('aria-selected')));
const activeImg = await page.evaluate(() => Array.from(document.querySelectorAll('[data-image]')).map(p => p.hasAttribute('data-active')));
const activeImg = await page.evaluate(() => Array.from(document.querySelectorAll('[data-stage-item]')).map(p => p.hasAttribute('data-active')));
const focused = await page.evaluate(() => document.activeElement?.textContent?.trim());
console.log({ selected, activeImg, focused });
await page.locator('#sectors').scrollIntoViewIfNeeded();
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

+1
View File
@@ -20,6 +20,7 @@ const columns = [
{
title: ui.footer.company,
links: [
{ href: localePath(lang, '/cipherverifier'), label: 'CipherVerifier' },
{ href: localePath(lang, '/team'), label: ui.nav.team },
{ href: localePath(lang, '/insights'), label: ui.nav.insights },
{ href: `${home}#sectors`, label: ui.nav.sectors },
+1
View File
@@ -8,6 +8,7 @@ const ui = t(lang);
const home = localePath(lang, '/');
const items = [
{ href: `${home}#capabilities`, label: ui.nav.capabilities },
{ href: localePath(lang, '/cipherverifier'), label: ui.nav.product },
{ href: `${home}#sectors`, label: ui.nav.sectors },
{ href: localePath(lang, '/insights'), label: ui.nav.insights },
{ href: localePath(lang, '/team'), label: ui.nav.team },
+6 -3
View File
@@ -2,7 +2,7 @@
import { Picture } from 'astro:assets';
import MethodDiagram from './MethodDiagram.astro';
import engineer from '../../assets/images/engineer-glass.jpg';
import { t, type Lang } from '../../i18n/ui';
import { localePath, t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
@@ -25,7 +25,10 @@ const ui = t(lang);
<h2 id="method-title" class="h2 method__title">
{ui.method.lines.map((line, i) => <span class:list={['line', { 'line--muted': i > 0 }]}>{line}</span>)}
</h2>
<p class="method__body">{ui.method.body}</p>
<div class="method__body">
<p>{ui.method.body}</p>
<a class="button" href={localePath(lang, '/cipherverifier')}>{ui.method.cta}</a>
</div>
</div>
<div class="container method__figure">
@@ -48,7 +51,7 @@ const ui = t(lang);
align-items: start;
}
.method__title { grid-column: 1 / -1; }
.method__body { grid-column: 1 / -1; max-width: 30em; }
.method__body { grid-column: 1 / -1; max-width: 30em; display: grid; gap: var(--space-5); justify-items: start; }
.method__figure { padding-bottom: var(--section); }
.method__figure :global(.diagram) { max-width: 78rem; margin-inline: auto; }
@media (min-width: 48rem) {
+3 -27
View File
@@ -61,7 +61,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
loading="lazy"
decoding="async"
class="sectors__img img-doc"
pictureAttributes={{ class: 'sectors__picture sectors__picture--stage', 'data-image': String(i), 'data-active': i === 0 ? '' : undefined }}
pictureAttributes={{ class: 'sectors__picture sectors__picture--stage', 'data-stage-item': '', 'data-active': i === 0 ? '' : undefined }}
/>
))}
</div>
@@ -136,31 +136,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
</style>
<script>
import { initTabs } from '../../scripts/tabs';
const root = document.querySelector<HTMLElement>('[data-sectors]');
if (root) {
const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
const panels = tabs.map((t) => document.getElementById(t.getAttribute('aria-controls')!)!);
const images = Array.from(root.querySelectorAll<HTMLElement>('[data-image]'));
const select = (index: number, focus = false) => {
tabs.forEach((tab, i) => {
const on = i === index;
tab.setAttribute('aria-selected', String(on));
if (on) tab.removeAttribute('tabindex'); else tab.setAttribute('tabindex', '-1');
if (on) panels[i].setAttribute('data-open', ''); else panels[i].removeAttribute('data-open');
if (on) images[i]?.setAttribute('data-active', ''); else images[i]?.removeAttribute('data-active');
});
if (focus) tabs[index].focus();
};
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(i));
tab.addEventListener('keydown', (e) => {
const n = tabs.length;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); select((i + 1) % n, true); }
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); select((i - 1 + n) % n, true); }
if (e.key === 'Home') { e.preventDefault(); select(0, true); }
if (e.key === 'End') { e.preventDefault(); select(n - 1, true); }
});
});
select(0);
}
if (root) initTabs(root);
</script>
+48
View File
@@ -0,0 +1,48 @@
---
import GridObject from '../home/GridObject.astro';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const objects = ['plane', 'cube', 'wave'] as const;
---
<section class="section vcards" aria-label={lang === 'de' ? 'Eigenschaften' : 'Properties'}>
<div class="container">
<ul class="vcards__list" role="list">
{ui.verifier.cards.map((card, i) => (
<li class="vcards__item">
<h2 class="vcards__title">{card.title}</h2>
<GridObject kind={objects[i]} class="vcards__object" />
<p class="muted vcards__body">{card.body}</p>
</li>
))}
</ul>
</div>
</section>
<style>
.vcards__list { display: grid; gap: var(--gap); }
.vcards__item {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto 1fr;
align-items: start;
gap: var(--space-6);
padding: var(--space-6) var(--space-6) var(--space-7);
background: var(--paper-deep);
}
.vcards__title {
font-size: var(--step-title);
line-height: var(--leading-title);
letter-spacing: var(--track-h2);
font-weight: var(--weight-regular);
min-height: calc(2 * var(--leading-title) * 1em);
text-wrap: balance;
hyphens: auto;
}
.vcards__item :global(.vcards__object) { width: 100%; max-width: 14rem; justify-self: center; }
.vcards__body { max-width: 26em; }
@media (min-width: 48rem) {
.vcards__list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
</style>
+47
View File
@@ -0,0 +1,47 @@
---
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const f = ui.verifier.facts;
---
<section class="section vfacts" aria-labelledby="vfacts-title">
<div class="container grid">
<h2 id="vfacts-title" class="h2 vfacts__title">{f.title}</h2>
<dl class="vfacts__list">
{f.items.map((item) => (
<div class="vfacts__row">
<dt class="vfacts__value">{item.value}</dt>
<dd class="muted vfacts__label">{item.label}</dd>
</div>
))}
</dl>
</div>
</section>
<style>
.vfacts { border-top: var(--hairline) solid var(--line); }
.vfacts__title { grid-column: 1 / -1; }
.vfacts__list { grid-column: 1 / -1; }
.vfacts__row {
display: grid;
grid-template-columns: 4.5em minmax(0, 1fr);
column-gap: var(--gap);
align-items: baseline;
padding-block: var(--space-5);
border-top: var(--hairline) solid var(--line);
}
.vfacts__row:last-child { border-bottom: var(--hairline) solid var(--line); }
.vfacts__value {
font-size: var(--step-h2);
line-height: 1;
letter-spacing: var(--track-h2);
font-weight: var(--weight-medium);
font-variant-numeric: tabular-nums;
}
.vfacts__label { max-width: 28em; }
@media (min-width: 48rem) {
.vfacts__title { grid-column: 1 / span 5; }
.vfacts__list { grid-column: 6 / span 7; }
}
</style>
+87
View File
@@ -0,0 +1,87 @@
---
import { localePath, t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const v = ui.verifier;
/* Seventeen lines from the left edge meet in one point and leave it as one line. */
const W = 1200, H = 600, F: [number, number] = [780, 300];
const n = 17;
const ins = Array.from({ length: n }, (_, i) => {
const y = 20 + (i * (H - 40)) / (n - 1);
const x = i % 2 === 0 ? 0 : 60;
return `${x},${Math.round(y)}`;
});
---
<section class="vhero night" aria-labelledby="vhero-title">
<div class="container vhero__inner">
<svg class="vhero__burst" viewBox={`0 0 ${W} ${H}`} role="img" aria-label={v.burstDesc} xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke-width="1" vector-effect="non-scaling-stroke">
<g class="vhero__in">
{ins.map((p) => <line x1={p.split(',')[0]} y1={p.split(',')[1]} x2={F[0]} y2={F[1]} />)}
</g>
<line class="vhero__out" x1={F[0]} y1={F[1]} x2={W} y2={F[1]} stroke-width="1.25" />
<circle class="vhero__ring" cx={F[0]} cy={F[1]} r="40" stroke-dasharray="2 5" stroke-width="1.25" />
</g>
<circle cx={F[0]} cy={F[1]} r="5" fill="currentColor" />
</svg>
<div class="vhero__text">
<p class="eyebrow">{v.eyebrow}</p>
<h1 id="vhero-title" class="display">
{v.title.map((line, i) => <span class:list={['line', { 'line--muted': i > 0 }]}>{line}</span>)}
</h1>
<a class="button" href={`${localePath(lang, '/')}#contact`}>{v.cta}</a>
</div>
</div>
</section>
<style>
.line { display: block; }
.line--muted { color: var(--ink-muted); }
.vhero { position: relative; overflow: hidden; }
.vhero__inner {
position: relative;
display: grid;
gap: var(--space-6);
min-height: clamp(30rem, calc(100svh - var(--header-height)), 54rem);
align-content: end;
padding-block: var(--space-6) var(--space-9);
}
.vhero__burst {
width: calc(100% + 2 * var(--gutter));
margin-inline: calc(var(--gutter) * -1);
height: auto;
color: var(--ink);
pointer-events: none;
}
.vhero__in { stroke: var(--ink-muted); opacity: 0.55; }
.vhero__out, .vhero__ring { stroke: currentColor; }
.vhero__text {
display: grid;
gap: var(--space-5);
justify-items: start;
max-width: 14em;
}
.vhero__text .eyebrow { margin-bottom: var(--space-1); }
.vhero__text .button { margin-top: var(--space-3); }
/* Wide: the headline keeps the lower left, the burst the right half, bleeding off the edge. */
@media (min-width: 64rem) {
.vhero__inner {
grid-template-columns: 7fr 5fr;
column-gap: var(--gap);
align-items: end;
}
.vhero__text { grid-column: 1; grid-row: 1; }
.vhero__burst {
grid-column: 2;
grid-row: 1;
align-self: center;
width: calc(100% + var(--gutter) + 6vw);
margin-inline: 0 calc(var(--gutter) * -1 - 6vw);
margin-block: var(--space-8) 0;
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
---
import Schematic from './Schematic.astro';
import { builders } from './diagrams';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const v = ui.verifier.how;
const pairs = [builders[0](v.groups[0]), builders[1](v.groups[1]), builders[2](v.groups[2]), builders[3](v.groups[3]), builders[4](v.groups[4])];
const groups = v.groups.map((g, i) => ({ title: g.title, body: g.body, desc: g.desc, id: `how-${i + 1}`, pair: pairs[i] }));
---
<section class="section night how" id="how" aria-labelledby="how-title">
<div class="container how__grid" data-how>
<div class="how__text">
<h2 id="how-title" class="h2">{v.title}</h2>
<div class="how__tabs" role="tablist" aria-orientation="vertical" aria-labelledby="how-title">
{groups.map((g, i) => (
<div class="how__tab">
<h3 class="how__heading">
<button class="how__button" type="button" role="tab" id={`${g.id}-tab`} aria-controls={`${g.id}-panel`} aria-selected={i === 0 ? 'true' : 'false'} tabindex={i === 0 ? undefined : -1}>{g.title}</button>
</h3>
<div class="how__panel" role="tabpanel" id={`${g.id}-panel`} aria-labelledby={`${g.id}-tab`} tabindex="0">
<div class="how__panel-inner">
<p class="muted">{g.body}</p>
<div class="how__tall">
<Schematic layout={g.pair.tall} title={g.title} desc={g.desc} variant="tall" id={`${g.id}-tall`} />
</div>
</div>
</div>
</div>
))}
</div>
</div>
<div class="how__stage">
{groups.map((g, i) => (
<div class="how__figure" data-stage-item data-active={i === 0 ? '' : undefined}>
<Schematic layout={g.pair.wide} title={g.title} desc={g.desc} variant="wide" id={`${g.id}-wide`} />
</div>
))}
</div>
</div>
</section>
<style>
.how__grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: var(--gap);
row-gap: var(--space-8);
}
.how__text {
grid-column: 1 / -1;
display: grid;
gap: var(--space-8);
}
.how__tab + .how__tab { margin-top: var(--space-5); }
.how__heading { font-size: inherit; font-weight: inherit; }
.how__button {
display: block;
text-align: left;
font-size: var(--step-h3);
line-height: var(--leading-h3);
letter-spacing: var(--track-h3);
font-weight: var(--weight-medium);
color: var(--ink);
transition: color var(--dur-fast) var(--ease);
}
.how__button[aria-selected='false'] { color: var(--ink-muted); }
.how__button[aria-selected='false']:hover { color: var(--ink); }
.how__panel-inner { display: grid; gap: var(--space-6); padding-top: var(--space-3); }
.how__panel p { max-width: 30em; }
.how__tall { max-width: 26rem; }
/* The stage carries the wide drawings for wide screens; the panels carry the tall ones. */
.how__stage { display: none; }
:global(html.js) .how__panel {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows var(--dur-slow) var(--ease);
}
:global(html.js) .how__panel-inner { min-height: 0; }
:global(html.js) .how__panel[data-open] { grid-template-rows: 1fr; }
@media (min-width: 64rem) {
/* Title left, the list right; the drawing takes the full width underneath. */
.how__text {
grid-column: 1 / -1;
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: var(--gap);
align-items: start;
}
.how__text > h2 { grid-column: 1 / span 5; }
.how__tabs { grid-column: 6 / span 7; }
.how__stage {
grid-column: 1 / -1;
display: grid;
border: var(--hairline) solid var(--line);
padding: var(--space-7) var(--space-7);
}
.how__figure {
grid-area: 1 / 1;
align-self: center;
opacity: 0;
transition: opacity var(--dur-slow) var(--ease);
}
.how__figure[data-active] { opacity: 1; }
.how__tall { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.how__panel, .how__figure { transition: none; }
}
</style>
<script>
import { initTabs } from '../../scripts/tabs';
const root = document.querySelector<HTMLElement>('[data-how]');
if (root) initTabs(root);
</script>
+53
View File
@@ -0,0 +1,53 @@
---
import { Picture } from 'astro:assets';
import desk from '../../assets/images/desk-at-night.jpg';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const v = ui.verifier;
---
<section class="night vintro" aria-labelledby="vintro-title">
<div class="container grid vintro__text">
<h2 id="vintro-title" class="h2 vintro__title">
{v.intro.lines.map((line) => <span class="line">{line}</span>)}
</h2>
<div class="vintro__body">
<p>{v.intro.p1}</p>
<p>{v.intro.p2}</p>
</div>
</div>
<Picture
src={desk}
alt={v.imageAlt}
formats={['avif']}
fallbackFormat="webp"
widths={[640, 960, 1440, 1920, 2560, 3024]}
sizes="100vw"
loading="lazy"
decoding="async"
class="vintro__img img-cover img-doc"
pictureAttributes={{ class: 'vintro__picture' }}
/>
</section>
<style>
.line { display: block; }
.vintro__text {
padding-block: var(--section);
align-items: start;
}
.vintro__title { grid-column: 1 / -1; }
.vintro__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); max-width: var(--measure); }
:global(.vintro__picture) { display: block; width: 100%; }
.vintro__img, :global(.vintro__img) {
width: 100%;
height: clamp(16rem, 38vw, 34rem);
object-fit: cover;
object-position: 30% 50%;
}
@media (min-width: 48rem) {
.vintro__title { grid-column: 1 / span 6; }
.vintro__body { grid-column: 7 / span 6; padding-top: var(--space-2); }
}
</style>
+27
View File
@@ -0,0 +1,27 @@
---
import { localePath, t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const o = ui.verifier.outro;
---
<section class="section voutro" aria-labelledby="voutro-title">
<div class="container grid">
<h2 id="voutro-title" class="h2 voutro__title">{o.title}</h2>
<div class="voutro__body">
<p>{o.body}</p>
<a class="button" href={`${localePath(lang, '/')}#contact`}>{o.cta}</a>
</div>
</div>
</section>
<style>
.voutro { border-top: var(--hairline) solid var(--line); }
.voutro__title { grid-column: 1 / -1; max-width: 12em; }
.voutro__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); justify-items: start; max-width: 30em; }
.voutro__body .button { margin-top: var(--space-2); }
@media (min-width: 48rem) {
.voutro__title { grid-column: 1 / span 6; }
.voutro__body { grid-column: 7 / span 5; padding-top: var(--space-2); }
}
</style>
+71
View File
@@ -0,0 +1,71 @@
---
/**
* A spec-sheet schematic rendered from a layout: nodes (main with dotted ring, or small
* satellites), edges (polylines, optional direction tick), and free text. One drawing per
* variant; the caller decides which variant shows where.
*/
import type { Layout } from './diagrams';
interface Props { layout: Layout; title: string; desc: string; variant: 'wide' | 'tall'; id: string }
const { layout, title, desc, variant, id } = Astro.props;
const stroke = 1.25;
const tick = (x: number, y: number, dx: number, dy: number, s = 7) => {
const len = Math.hypot(dx, dy) || 1; const ux = dx / len, uy = dy / len; const px = -uy, py = ux;
return `${x - ux * s + px * s},${y - uy * s + py * s} ${x},${y} ${x - ux * s - px * s},${y - uy * s - py * s}`;
};
/* A tick sits at the midpoint of the edge's last segment, pointing along it. */
const edgeTick = (pts: [number, number][]) => {
const [a, b] = [pts[pts.length - 2], pts[pts.length - 1]];
return tick((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, b[0] - a[0], b[1] - a[1]);
};
const lines = (t: string | string[]) => (Array.isArray(t) ? t : [t]);
---
<svg class:list={['schematic', `schematic--${variant}`]} viewBox={`0 0 ${layout.w} ${layout.h}`} role="img" aria-labelledby={`${id}-title ${id}-desc`} xmlns="http://www.w3.org/2000/svg">
<title id={`${id}-title`}>{title}</title>
<desc id={`${id}-desc`}>{desc}</desc>
<g fill="none" stroke="currentColor" stroke-width={stroke} stroke-linecap="square" stroke-linejoin="round" vector-effect="non-scaling-stroke">
{layout.edges.map((e) => (
<g class:list={{ 'schematic__faint': e.faint }}>
<polyline points={e.points.map(([x, y]) => `${x},${y}`).join(' ')} stroke-dasharray={e.dashed ? '2 5' : undefined} />
{e.tick && <polyline points={edgeTick(e.points)} />}
</g>
))}
{layout.nodes.filter((n) => n.ring).map((n) => <circle cx={n.x} cy={n.y} r={n.ring} stroke-dasharray="2 5" />)}
{layout.boxes?.map((b) => <rect x={b.x} y={b.y} width={b.w} height={b.h} stroke-dasharray={b.dashed ? '2 5' : undefined} />)}
</g>
<g fill="currentColor">
{layout.nodes.map((n) => n.dot === 'none' ? null : <circle cx={n.x} cy={n.y} r={n.dot === 'sub' ? 3 : 5} class:list={{ 'schematic__dot--sub': n.dot === 'sub' }} />)}
</g>
<g class="schematic__labels" font-size={layout.fontMain ?? 17} font-weight="500">
{layout.nodes.filter((n) => n.label && n.dot !== 'sub').map((n) => (
<text x={n.x + (n.dx ?? 0)} y={n.y + (n.dy ?? 6)} text-anchor={n.anchor ?? 'start'}>
{lines(n.label!).map((l, i) => <tspan x={n.x + (n.dx ?? 0)} dy={i === 0 ? 0 : '1.25em'}>{l}</tspan>)}
</text>
))}
</g>
<g class="schematic__meta" font-size={layout.fontSub ?? 15}>
{layout.nodes.filter((n) => n.label && n.dot === 'sub').map((n) => (
<text x={n.x + (n.dx ?? 12)} y={n.y + (n.dy ?? 5)} text-anchor={n.anchor ?? 'start'}>
{lines(n.label!).map((l, i) => <tspan x={n.x + (n.dx ?? 12)} dy={i === 0 ? 0 : '1.25em'}>{l}</tspan>)}
</text>
))}
{layout.texts?.map((t) => (
<text x={t.x} y={t.y} text-anchor={t.anchor ?? 'start'} class:list={{ 'schematic__strong': t.strong }}>
{lines(t.text).map((l, i) => <tspan x={t.x} dy={i === 0 ? 0 : '1.25em'}>{l}</tspan>)}
</text>
))}
</g>
</svg>
<style>
.schematic {
width: 100%;
height: auto;
color: var(--ink);
font-family: var(--font-sans);
}
.schematic__labels { fill: var(--ink); }
.schematic__meta { fill: var(--ink-muted); }
.schematic__strong { fill: var(--ink); font-weight: var(--weight-medium); }
.schematic__faint { stroke: var(--line); }
.schematic__dot--sub { fill: var(--ink-muted); }
</style>
+210
View File
@@ -0,0 +1,210 @@
/**
* Layouts of the CipherVerifier schematics. Each builder takes the labels of its group from the
* copy file and returns a wide composition (hand-placed) and a tall one (a spine list), both in
* the same primitives that Schematic.astro renders.
*/
import type { HowGroups } from './types';
type Groups = HowGroups;
type Pt = [number, number];
type Label = string | string[];
export interface Node { x: number; y: number; label?: Label; dot?: 'main' | 'sub' | 'none'; ring?: number; anchor?: 'start' | 'end' | 'middle'; dx?: number; dy?: number }
export interface Edge { points: Pt[]; tick?: boolean; faint?: boolean; dashed?: boolean }
export interface Box { x: number; y: number; w: number; h: number; dashed?: boolean }
export interface Text { x: number; y: number; text: Label; anchor?: 'start' | 'end' | 'middle'; strong?: boolean }
export interface Layout { w: number; h: number; nodes: Node[]; edges: Edge[]; boxes?: Box[]; texts?: Text[]; fontMain?: number; fontSub?: number }
export interface Pair { wide: Layout; tall: Layout }
const RING = 36;
const LINE = 21; // one line of a 17px label, in viewBox units
/* A ringed main node with its label centred below it and an optional muted line under that. */
function main(x: number, y: number, label: Label, sub?: Label, ring = RING): { nodes: Node[]; texts: Text[] } {
const lines = Array.isArray(label) ? label.length : 1;
const nodes: Node[] = [{ x, y, label, dot: 'main', ring, anchor: 'middle', dx: 0, dy: ring + 22 }];
const texts: Text[] = sub ? [{ x, y: y + ring + 22 + lines * LINE + 2, text: sub, anchor: 'middle' }] : [];
return { nodes, texts };
}
/* A plain dot with its label centred below. */
function dot(x: number, y: number, label: Label): Node {
return { x, y, label, dot: 'main', anchor: 'middle', dx: 0, dy: 28 };
}
/* Satellites fanning from a node to a column of dots. */
function fan(from: Pt, xs: number, ys: number[], labels: string[], anchor: 'start' | 'end'): { nodes: Node[]; edges: Edge[] } {
const nodes: Node[] = ys.map((y, i) => ({ x: xs, y, label: labels[i], dot: 'sub', anchor, dx: anchor === 'end' ? -12 : 12 }));
const edges: Edge[] = ys.map((y) => ({ points: [from, [xs, y]], faint: true }));
return { nodes, edges };
}
const chain = (pts: Pt[], tick = true): Edge[] => pts.slice(1).map((p, i) => ({ points: [pts[i], p], tick }));
/* Tall composition: a spine of ringed nodes, each with its instruments hanging below its title. */
function spine(groups: { title: Label; items?: string[]; note?: string }[]): Layout {
const x = 60, ring = 28, labelX = 40, satX = 44, satY = 36, dy = 26;
const nodes: Node[] = [];
const edges: Edge[] = [];
const texts: Text[] = [];
let y = 56;
const ys: number[] = [];
groups.forEach((g) => {
ys.push(y);
const lines = Array.isArray(g.title) ? g.title.length : 1;
nodes.push({ x, y, label: g.title, dot: 'main', ring, dx: labelX, dy: 6 });
let bottom = y + (lines - 1) * 20;
if (g.note) { texts.push({ x: x + labelX, y: bottom + 22, text: g.note }); bottom += 22; }
(g.items ?? []).forEach((item, k) => {
const sy = bottom + satY + k * dy;
nodes.push({ x: x + satX, y: sy, label: item, dot: 'sub', dx: 12 });
edges.push({ points: [[x, y], [x + satX, sy]], faint: true });
});
const items = g.items?.length ?? 0;
bottom = items ? bottom + satY + (items - 1) * dy : bottom;
y = Math.max(bottom + 70, y + 110);
});
ys.slice(1).forEach((yy, i) => edges.unshift({ points: [[x, ys[i]], [x, yy]], tick: true }));
return { w: 400, h: y - 40, nodes, edges, texts, fontMain: 16, fontSub: 14 };
}
export function overview(g: Groups[0]): Pair {
const L = g.labels;
const y1 = 110, y2 = 330, ym = 220, fork = 190, join = 1050;
const sbom = main(340, y1, L.sbom, L.sbomSub), dt = main(640, y1, L.dt, L.dtSub);
const sast = main(340, y2, L.sast, L.sastSub), poly = main(640, y2, L.poly, L.polySub), prism = main(900, y2, L.prism, L.prismSub);
const wide: Layout = {
w: 1200, h: 470,
nodes: [dot(80, ym, L.repo), ...sbom.nodes, ...dt.nodes, ...sast.nodes, ...poly.nodes, ...prism.nodes, dot(1120, ym, L.report)],
edges: [
{ points: [[80, ym], [fork, ym], [fork, y1], [340, y1]], tick: true },
{ points: [[fork, ym], [fork, y2], [340, y2]], tick: true },
...chain([[340, y1], [640, y1]]),
{ points: [[640, y1], [join, y1], [join, ym], [1120, ym]], tick: true },
...chain([[340, y2], [640, y2], [900, y2]]),
{ points: [[900, y2], [join, y2], [join, ym]] },
],
texts: [...sbom.texts, ...dt.texts, ...sast.texts, ...poly.texts, ...prism.texts],
};
const tall = spine([
{ title: L.repo },
{ title: L.sbom, note: L.sbomSub }, { title: L.dt, note: L.dtSub },
{ title: L.sast, note: L.sastSub }, { title: L.poly, note: L.polySub }, { title: L.prism, note: L.prismSub },
{ title: L.report },
]);
return { wide, tall };
}
export function scanners(g: Groups[1]): Pair {
const L = g.labels;
const focal: Pt = [500, 280];
const ys = g.tools.map((_, i) => 60 + i * 27.5);
const tools = fan(focal, 210, ys, g.tools, 'end');
const one = main(500, 280, L.one, L.oneSub), cwe = main(700, 280, L.cwe, L.cweSub), merge = main(900, 280, L.merge, L.mergeSub), resolve = main(1090, 280, L.resolve, L.resolveSub);
const wide: Layout = {
w: 1200, h: 560,
nodes: [...tools.nodes, ...one.nodes, ...cwe.nodes, ...merge.nodes, ...resolve.nodes],
edges: [...tools.edges, ...chain([[500, 280], [700, 280], [900, 280], [1090, 280]])],
texts: [...one.texts, ...cwe.texts, ...merge.texts, ...resolve.texts],
};
const tall = spine([
{ title: g.title.split(',')[0], items: g.tools },
{ title: L.one, note: L.oneSub }, { title: L.cwe, note: L.cweSub }, { title: L.merge, note: L.mergeSub }, { title: L.resolve, note: L.resolveSub },
]);
return { wide, tall };
}
export function verdict(g: Groups[2]): Pair {
const L = g.labels;
const y = 300, top = 120;
const ctx = fan([380, y], 230, [100, 140, 180, 220], L.ctx, 'end');
const tools = fan([920, y], 1040, [150, 190, 230, 270], L.tools, 'start');
const outcomes = fan([1220, y], 1330, [240, 280, 320], L.outcomes, 'start');
const context = main(380, y, L.context), s1 = main(640, y, L.stage1), s2 = main(920, y, L.stage2), v = main(1220, y, L.verdict, L.carries);
const wide: Layout = {
w: 1460, h: 420,
nodes: [dot(140, y, L.finding), ...context.nodes, ...ctx.nodes, ...s1.nodes, ...s2.nodes, ...tools.nodes, ...v.nodes, ...outcomes.nodes],
edges: [
...ctx.edges, ...tools.edges, ...outcomes.edges,
...chain([[140, y], [380, y], [640, y], [920, y], [1220, y]]),
{ points: [[640, y], [640, top], [1220, top], [1220, y]], tick: true },
],
texts: [...v.texts, { x: 930, y: top - 12, text: L.certain, anchor: 'middle' }, { x: 780, y: y + 30, text: splitTwo(L.unsure), anchor: 'middle' }],
fontMain: 19, fontSub: 17,
};
const tall = spine([
{ title: L.finding },
{ title: L.context, items: L.ctx },
{ title: L.stage1, items: [L.certain, L.unsure] },
{ title: L.stage2, items: L.tools },
{ title: L.verdict, note: L.carries, items: L.outcomes },
]);
return { wide, tall };
}
export function dependencies(g: Groups[3]): Pair {
const L = g.labels;
const y = 220, ya = 130, yb = 310, mergeX = 300;
const sbom = main(430, y, L.sbom, L.sbomSub), dt = main(700, y, L.dt, L.dtSub), vdr = main(960, y, L.vdr, L.vdrSub);
const wide: Layout = {
w: 1200, h: 400,
nodes: [
{ x: 150, y: ya, label: L.syft, dot: 'main', anchor: 'end', dx: -16, dy: 6 },
{ x: 150, y: yb, label: L.cdxgen, dot: 'main', anchor: 'end', dx: -16, dy: 6 },
...sbom.nodes, ...dt.nodes, ...vdr.nodes, dot(1130, y, L.report),
],
edges: [
{ points: [[150, ya], [mergeX, ya], [mergeX, y]] },
{ points: [[150, yb], [mergeX, yb], [mergeX, y]] },
{ points: [[mergeX, y], [430, y]], tick: true },
...chain([[430, y], [700, y], [960, y], [1130, y]]),
],
texts: [...sbom.texts, ...dt.texts, ...vdr.texts],
};
const tall = spine([
{ title: `${L.syft}, ${L.cdxgen}` },
{ title: L.sbom, items: toList(L.sbomSub) }, { title: L.dt, items: toList(L.dtSub) }, { title: L.vdr, items: toList(L.vdrSub) },
{ title: L.report },
]);
return { wide, tall };
}
export function operate(g: Groups[4]): Pair {
const L = g.labels;
const hub: Pt = [560, 220];
const ys = [100, 160, 220, 280, 340];
const sources = fan(hub, 200, ys, L.sources, 'end');
const outputs = fan(hub, 920, ys, L.outputs, 'start');
const wide: Layout = {
w: 1200, h: 540,
nodes: [
{ x: hub[0], y: hub[1], label: L.hub, dot: 'main', ring: 44, anchor: 'middle', dx: 0, dy: -58 },
...sources.nodes, ...outputs.nodes,
dot(420, 460, L.scopes[0]), dot(560, 460, L.scopes[1]), dot(700, 460, L.scopes[2]),
],
edges: [
...sources.edges, ...outputs.edges,
{ points: [[560, 264], [560, 460]], faint: true },
...chain([[420, 460], [560, 460], [700, 460]]),
],
texts: [{ x: 540, y: 425, text: L.scopesTitle, anchor: 'end' }],
};
const tall = spine([
{ title: L.hub, items: L.sources },
{ title: g.title, items: L.outputs },
{ title: L.scopesTitle, items: L.scopes },
]);
return { wide, tall };
}
function toList(l: Label): string[] { return Array.isArray(l) ? l : [l]; }
/* Break a sentence into two roughly equal lines at a word boundary. */
function splitTwo(s: string): string[] {
const words = s.split(' ');
if (words.length < 3) return [s];
let best = 1, diff = Infinity;
for (let i = 1; i < words.length; i++) {
const a = words.slice(0, i).join(' ').length, b = words.slice(i).join(' ').length;
if (Math.abs(a - b) < diff) { diff = Math.abs(a - b); best = i; }
}
return [words.slice(0, best).join(' '), words.slice(best).join(' ')];
}
export const builders = [overview, scanners, verdict, dependencies, operate] as const;
+16
View File
@@ -0,0 +1,16 @@
/** Shapes of the "How CipherVerifier works" groups, so the copy file and the diagram builders agree. */
export interface Group<L> { title: string; body: string; desc: string; labels: L }
export interface OverviewLabels { repo: string; sbom: string[]; sbomSub: string; dt: string[]; dtSub: string; sast: string[]; sastSub: string; poly: string[]; polySub: string; prism: string[]; prismSub: string; report: string }
export interface ScannersLabels { one: string; oneSub: string; cwe: string[]; cweSub: string; merge: string[]; mergeSub: string; resolve: string[]; resolveSub: string }
export interface VerdictLabels { finding: string; context: string; ctx: string[]; stage1: string[]; stage2: string; tools: string[]; verdict: string; outcomes: string[]; carries: string; certain: string; unsure: string }
export interface DependenciesLabels { syft: string; cdxgen: string; sbom: string; sbomSub: string[]; dt: string; dtSub: string[]; vdr: string[]; vdrSub: string; report: string }
export interface OperateLabels { hub: string; sources: string[]; outputs: string[]; scopes: string[]; scopesTitle: string }
export type HowGroups = [
Group<OverviewLabels>,
Group<ScannersLabels> & { tools: string[] },
Group<VerdictLabels>,
Group<DependenciesLabels>,
Group<OperateLabels>,
];
+176 -32
View File
@@ -1,3 +1,5 @@
import type { HowGroups } from '../components/verifier/types';
export type Lang = 'en' | 'de';
export const langs: Lang[] = ['en', 'de'];
export const defaultLang: Lang = 'en';
@@ -28,15 +30,17 @@ export function formatDate(date: Date, lang: Lang): string {
const en = {
meta: {
siteName: 'CipherCell',
home: { title: 'CipherCell. Security engineering for complex infrastructure.', description: 'CipherCell is a security engineering company in Passau. Threat analysis, security engineering, verification and monitoring for private 5G, critical network infrastructure and cloud-native systems.' },
home: { title: 'CipherCell. Security engineering for software.', description: 'CipherCell is a security engineering company in Passau. Threat analysis, security engineering, verification and monitoring for the software behind private 5G, critical network infrastructure and cloud-native systems.' },
team: { title: 'Team', description: 'The security researchers, engineers and architects behind CipherCell.' },
insights: { title: 'Insights', description: 'Notes on the security of private 5G, critical networks and the systems that run them.' },
verifier: { title: 'CipherVerifier', description: 'CipherVerifier is a self-hosted verification pipeline for software: seventeen static analysers and two SBOM generators, one vocabulary, a two-stage review of every finding, and a ranked list an engineer can act on.' },
insights: { title: 'Insights', description: 'Notes on software security in private 5G, critical networks and the systems that run them.' },
imprint: { title: 'Legal notice', description: 'Legal notice for the CipherCell website.' },
privacy: { title: 'Privacy', description: 'Privacy policy for the CipherCell website.' },
},
nav: {
skip: 'Skip to content',
capabilities: 'Capabilities',
product: 'Product',
sectors: 'Sectors',
insights: 'Insights',
team: 'Team',
@@ -46,19 +50,20 @@ const en = {
homeLabel: 'CipherCell, home',
},
hero: {
lines: ['Security for the networks', 'everything else runs on.'],
lines: ['Security for the software', 'everything else runs on.'],
imageAlt: 'An industrial plant at dusk: pipe racks, storage tanks and a lattice mast carrying 5G antennas under an overcast sky.',
},
manifesto: {
lines: ['Security is built in,', 'not bolted on.'],
p1: 'Private 5G networks, industrial control systems and cloud platforms are assembled from the components of many vendors. Every interface between them is an attack surface, and off-the-shelf tools were not built for that.',
p2: 'CipherCell is a security engineering company. We model real adversaries, build zero trust into the architecture rather than around it, and prove that the posture holds. From the first threat model to the running system.',
p1: 'Infrastructure is software now. Private 5G networks, industrial control systems and cloud platforms are assembled from the code and components of many vendors. Every interface between them is an attack surface, and off-the-shelf tools were not built for that.',
p2: 'CipherCell is a security engineering company for software. We model real adversaries, build zero trust into the architecture rather than around it, and prove that the code holds. From the first threat model to the running system.',
cta: 'About CipherCell',
},
method: {
lines: ['Four stages.', 'One method.'],
body: 'Threat analysis, engineering, verification and monitoring, run as one continuous method rather than four separate projects. Modular, integrable and built for operational reality.',
body: 'Threat analysis, engineering, verification and monitoring, applied to your software as one continuous method rather than four separate projects. It runs alongside your development, modular and integrable.',
imageAlt: 'A security engineer seen through glass, the reflections of monitoring screens layered across her face.',
cta: 'Explore CipherVerifier',
diagramTitle: 'The CipherCell method',
diagramDesc: 'A closed loop of four stages: threat analysis, engineering, verification and monitoring. Each stage carries its own instruments: threat model, attack paths and CI/CD integration; zero trust, key management and secure APIs; independent review, hardening and testing; intrusion detection, security logging and resilience checks. Monitoring feeds back into threat analysis.',
stages: [
@@ -71,17 +76,17 @@ const en = {
stages: {
eyebrow: 'What we do.',
items: [
{ title: 'Threat analysis', body: 'Automated, empirical analysis of real attack paths. It runs inside your CI/CD pipeline and delivers continuous assurance.' },
{ title: 'Engineering', body: 'Zero-trust enforcement, key management and secure API design as modular, composable tools for complex deployments.' },
{ title: 'Verification', body: 'Independent review of security measures, hardening checklists and application-level testing across all components.' },
{ title: 'Monitoring', body: 'Intrusion detection and security logging, from the first deployment through ongoing resilience verification.' },
{ title: 'Threat analysis', body: 'Automated, empirical analysis of real attack paths through your software. It runs inside your CI/CD pipeline and delivers continuous assurance.' },
{ title: 'Engineering', body: 'Zero-trust enforcement, key management and secure API design as modular, composable components for your codebase and deployments.' },
{ title: 'Verification', body: 'Independent review of code and security measures, hardening checklists and application-level testing across all components.' },
{ title: 'Monitoring', body: 'Intrusion detection and security logging built into the software, from the first deployment through ongoing resilience verification.' },
],
},
sectors: {
title: 'Who we work with',
items: [
{ title: 'Private 5G and campus networks', body: 'Disaggregated networks with open interfaces and a multi-vendor supply chain. Our origin, and still the hardest case.', imageAlt: 'A private 5G antenna and radio cabinet on the parapet of a factory roof, cranes in the distance.' },
{ title: 'Critical network infrastructure', body: 'Networks where a breach stops production, damages equipment or endangers people.', imageAlt: 'An electrical substation seen straight from above: transformers, switchgear and a service road.' },
{ title: 'Private 5G and campus networks', body: 'Software-defined networks with open interfaces and a multi-vendor supply chain. Our origin, and still the hardest case.', imageAlt: 'A private 5G antenna and radio cabinet on the parapet of a factory roof, cranes in the distance.' },
{ title: 'Critical network infrastructure', body: 'Control and network software where a breach stops production, damages equipment or endangers people.', imageAlt: 'An electrical substation seen straight from above: transformers, switchgear and a service road.' },
{ title: 'Cloud-native systems', body: 'Containers, service meshes and CI/CD pipelines that carry infrastructure workloads. Security that runs as code, alongside yours.', imageAlt: 'A cold aisle between server racks in a data centre, a technician far in the distance.' },
],
},
@@ -104,7 +109,7 @@ const en = {
insights: {
title: 'Insights',
all: 'All insights',
intro: 'Notes on the security of private 5G, critical networks and the systems that run them.',
intro: 'Notes on software security in private 5G, critical networks and the systems that run them.',
readingTime: (min: number) => `${min} min read`,
event: { category: 'Event', when: 'September 2026', text: 'Meet us at the xG‑ALOE Plugfest 2026.', link: 'Event details', href: 'https://xg-aloe.de/xg-aloe-plugfest-2026/' },
back: 'All insights',
@@ -113,7 +118,7 @@ const en = {
},
contact: {
title: 'Talk to us.',
body: 'Complex infrastructure, a tight compliance deadline, or a problem no one has solved yet. We respond within 24 hours.',
body: 'Complex software, a tight compliance deadline, or a problem no one has solved yet. We respond within 24 hours.',
address: ['Innstr. 43', '94032 Passau, Germany'],
phone: '+49 (0)851 / 509-3218',
phoneHref: 'tel:+498515093218',
@@ -129,7 +134,7 @@ const en = {
},
},
footer: {
mission: 'Security engineering for infrastructure where failure has consequences.',
mission: 'Security engineering for software where failure has consequences.',
company: 'Company',
capabilities: 'Capabilities',
contact: 'Contact',
@@ -144,9 +149,77 @@ const en = {
en: 'English',
de: 'Deutsch',
},
verifier: {
eyebrow: 'Product',
title: ['CipherVerifier.', 'Every finding, verified.'],
cta: 'Talk to us',
burstDesc: 'Seventeen thin lines converge on one point and leave it as a single line.',
intro: {
lines: ['Seventeen scanners.', 'One list.'],
p1: 'CipherVerifier is a self-hosted pipeline. It takes a repository, runs seventeen static analysers and two bill-of-materials generators over it, and merges everything they report into one vocabulary.',
p2: 'Then it does what scanners cannot. It reads each finding in its context, decides whether it is real, and ranks what remains by security impact. The result is a list an engineer can work through, not a pile of alerts.',
},
imageAlt: 'A software engineer at a desk in the evening, source code reflected in the dark window beside the monitors.',
how: {
title: 'How CipherVerifier works.',
groups: [
{
title: 'One run, five stages.',
body: 'A scan starts from a commit, a branch or an uploaded archive. Bill-of-materials generation and dependency analysis run in one lane, static analysis, classification and ranking in the other. Each stage is its own tool with its own status, and each can be switched off per repository.',
desc: 'A repository forks into two lanes. The upper lane runs SBOM generation and then Dependency-Track analysis. The lower lane runs SAST scanning, then Polygraph classification, then Prism ranking. Both lanes join in the report.',
labels: { repo: 'Repository', sbom: ['SBOM', 'generation'], sbomSub: 'Syft and cdxgen', dt: ['Dependency-Track', 'analysis'], dtSub: 'Vulnerability report', sast: ['SAST', 'scanning'], sastSub: 'Seventeen analysers', poly: ['Polygraph', 'classification'], polySub: 'Two-stage verdict', prism: ['Prism', 'ranking'], prismSub: 'By security impact', report: 'Report' },
},
{
title: 'Seventeen analysers, one vocabulary.',
body: 'Rayfusion detects the languages in the tree and runs the analysers that apply, each in its own container, in parallel. A security profile narrows every tool to rules that describe a weakness rather than a style. SarifCombiner then merges the reports: one CWE vocabulary of 970 entries, overlapping findings for the same weakness merged into one, and rules without a CWE resolved once and remembered.',
desc: 'Seventeen analysers converge on one report. From there three steps follow: one CWE vocabulary, one finding per weakness, and unmapped rules resolved.',
tools: ['Bandit', 'Checkov', 'Cppcheck', 'Fallow', 'Fallow Security', 'Gitleaks', 'Gosec', 'Hadolint', 'KubeLinter', 'mypy', 'Pylint', 'Roslyn Analyzers', 'Safety', 'Semgrep', 'Staticcheck', 'Trivy', 'Vulture'],
labels: { one: 'One report', oneSub: 'SARIF, validated', cwe: ['One CWE', 'vocabulary'], cweSub: '970 entries', merge: ['One finding', 'per weakness'], mergeSub: 'Overlaps merged', resolve: ['Unmapped rules', 'resolved'], resolveSub: 'Once, then cached' },
},
{
title: 'Two stages of judgement.',
body: 'Polygraph reads every finding with its code, its provenance and the file around it, and returns a verdict with a certainty and a priority. A finding it is unsure about, or a false-positive call it is not certain of, goes to a second agent that can read the repository itself, within a fixed budget of requests and tokens. Every verdict carries its reasoning. A cached verdict is reused only when the code it was based on is unchanged.',
desc: 'A finding is assembled with its context: provenance, code snippet, file header and an optional dataflow trace. A first stage returns a static verdict. Certain verdicts go straight to the result. Uncertain verdicts, and false-positive verdicts below high certainty, are escalated to a second agent that reads files, lists directories and searches the tree within a bounded budget. The result is true positive, false positive or uncertain, with certainty, priority and reasoning.',
labels: { finding: 'Finding', context: 'Context', ctx: ['Provenance', 'Code snippet', 'File header', 'Dataflow trace, optional'], stage1: ['Static', 'verdict'], stage2: 'Escalation', tools: ['Reads files', 'Lists directories', 'Searches the tree', 'Bounded budget'], verdict: 'Verdict', outcomes: ['True positive', 'False positive', 'Uncertain'], carries: 'Certainty, priority, reasoning', certain: 'Certain', unsure: 'Uncertain, or an unsure false positive' },
},
{
title: 'The dependency side.',
body: 'Two generators, Syft and cdxgen, each produce a bill of materials. Spectograph merges them by package URL into one CycloneDX document that is more complete than either. Deptrace hands it to Dependency-Track, a hosted instance or a container started for the scan, and brings back the vulnerability report per component.',
desc: 'Syft and cdxgen each produce an SBOM. They merge into one CycloneDX document deduplicated by package URL, which goes to Dependency-Track, hosted or ephemeral, and comes back as a vulnerability report per component that joins the scan report.',
labels: { syft: 'Syft', cdxgen: 'cdxgen', sbom: 'One SBOM', sbomSub: ['CycloneDX 1.6', 'Merged by package URL'], dt: 'Dependency-Track', dtSub: ['Hosted instance', 'or ephemeral container'], vdr: ['Vulnerability', 'report'], vdrSub: 'Per component, with severity', report: 'Report' },
},
{
title: 'Built to be operated.',
body: 'Repositories come in from GitHub, GitLab and Gitea, as an uploaded archive, or from your CI over a webhook with a token. Settings cascade from global to project to repository, so one default serves many teams and a single repository can still differ. Scans queue, run in parallel and report each stage live. What comes out is kept: the combined report, the vulnerability report, a PDF, and every artefact in your own storage.',
desc: 'Sources on the left, GitHub, GitLab, Gitea, archive upload and CI webhook, feed CipherVerifier in the centre. Outputs on the right: live pipeline, combined report, vulnerability report, PDF report and artefacts in your storage. Below, settings cascade from global to project to repository.',
labels: { hub: 'CipherVerifier', sources: ['GitHub App', 'GitLab', 'Gitea', 'Archive upload', 'CI webhook'], outputs: ['Live pipeline', 'Combined report', 'Vulnerability report', 'PDF report', 'Artefacts in your storage'], scopes: ['Global', 'Project', 'Repository'], scopesTitle: 'Settings cascade' },
},
] as HowGroups,
},
cards: [
{ title: 'Fix these first.', body: 'Prism ranks the verified findings by security impact and puts the ranked list at the top of the scan page. Findings classified as false positives can be left out entirely.' },
{ title: 'Runs where your code is.', body: 'One installer sets up the server, the database, the object store and the scanner images. The language model and the Dependency-Track instance are endpoints you choose.' },
{ title: 'Every verdict carries its reasoning.', body: 'Each classified finding gets a line in the audit log with its verdict, its certainty and the reasoning behind it. Each stage records when it started and how it ended. The PDF report and the artefacts stay for as long as the scan exists.' },
],
facts: {
title: 'In numbers.',
items: [
{ value: '17', label: 'Static analysers, each in its own container.' },
{ value: '14', label: 'Programming languages, plus Docker, Kubernetes, Helm, Terraform and CloudFormation.' },
{ value: '2', label: 'Bill-of-materials generators, merged into one document.' },
{ value: '970', label: 'CWE entries in one vocabulary.' },
{ value: '5', label: 'Pipeline stages, each switchable per repository.' },
],
},
outro: {
title: 'Talk to us about your repositories.',
body: 'Bring a repository you know well, and we will show you what CipherVerifier makes of it: what it finds, what it rules out, and why.',
cta: 'Talk to us',
},
},
team: {
title: ['Researchers,', 'engineers,', 'architects.'],
intro: 'The people who go deep on the problems that matter, from 5G and private networks to hardware, software and cloud-native systems.',
intro: 'The people who go deep on the problems that matter, from network software and private 5G to hardware and cloud-native systems.',
photoAlt: 'The CipherCell team on a staircase in the offices in Passau.',
links: { github: 'GitHub', linkedin: 'LinkedIn', email: 'Email' },
roles: {
@@ -175,15 +248,17 @@ const en = {
const de: typeof en = {
meta: {
siteName: 'CipherCell',
home: { title: 'CipherCell. Security Engineering für komplexe Infrastruktur.', description: 'CipherCell ist ein Security-Engineering-Unternehmen in Passau. Bedrohungsanalyse, Security Engineering, Verifikation und Monitoring für private 5G-Netze, kritische Netzinfrastruktur und Cloud-native Systeme.' },
home: { title: 'CipherCell. Security Engineering für Software.', description: 'CipherCell ist ein Security-Engineering-Unternehmen in Passau. Bedrohungsanalyse, Security Engineering, Verifikation und Monitoring für die Software hinter privaten 5G-Netzen, kritischer Netzinfrastruktur und Cloud-nativen Systemen.' },
team: { title: 'Team', description: 'Die Sicherheitsforschenden, Ingenieurinnen und Architekten hinter CipherCell.' },
insights: { title: 'Einblicke', description: 'Notizen zur Sicherheit privater 5G-Netze, kritischer Netze und der Systeme, die sie betreiben.' },
verifier: { title: 'CipherVerifier', description: 'CipherVerifier ist eine selbst gehostete Verifikations-Pipeline für Software: siebzehn statische Analysewerkzeuge und zwei SBOM-Generatoren, ein Vokabular, ein zweistufiges Urteil zu jedem Fund und eine Rangliste, die ein Engineer abarbeiten kann.' },
insights: { title: 'Einblicke', description: 'Notizen zur Softwaresicherheit in privaten 5G-Netzen, kritischen Netzen und den Systemen, die sie betreiben.' },
imprint: { title: 'Impressum', description: 'Impressum der CipherCell-Website.' },
privacy: { title: 'Datenschutz', description: 'Datenschutzerklärung der CipherCell-Website.' },
},
nav: {
skip: 'Zum Inhalt springen',
capabilities: 'Leistungen',
product: 'Produkt',
sectors: 'Einsatzfelder',
insights: 'Einblicke',
team: 'Team',
@@ -193,19 +268,20 @@ const de: typeof en = {
homeLabel: 'CipherCell, Startseite',
},
hero: {
lines: ['Sicherheit für die Netze,', 'auf denen alles andere läuft.'],
lines: ['Sicherheit für die Software,', 'auf der alles andere läuft.'],
imageAlt: 'Eine Industrieanlage in der Dämmerung: Rohrbrücken, Tanks und ein Gittermast mit 5G-Antennen unter bedecktem Himmel.',
},
manifesto: {
lines: ['Sicherheit wird eingebaut,', 'nicht angeschraubt.'],
p1: 'Private 5G-Netze, industrielle Steuerungssysteme und Cloud-Plattformen werden aus Komponenten vieler Hersteller zusammengesetzt. Jede Schnittstelle dazwischen ist eine Angriffsfläche, und Standardwerkzeuge sind dafür nicht gebaut.',
p2: 'CipherCell ist ein Security-Engineering-Unternehmen. Wir modellieren reale Angreifer, verankern Zero Trust in der Architektur statt daneben und weisen nach, dass sie standhält. Vom ersten Bedrohungsmodell bis zum laufenden System.',
p1: 'Infrastruktur ist heute Software. Private 5G-Netze, industrielle Steuerungssysteme und Cloud-Plattformen werden aus Code und Komponenten vieler Hersteller zusammengesetzt. Jede Schnittstelle dazwischen ist eine Angriffsfläche, und Standardwerkzeuge sind dafür nicht gebaut.',
p2: 'CipherCell ist ein Security-Engineering-Unternehmen für Software. Wir modellieren reale Angreifer, verankern Zero Trust in der Architektur statt daneben und weisen nach, dass der Code standhält. Vom ersten Bedrohungsmodell bis zum laufenden System.',
cta: 'Über CipherCell',
},
method: {
lines: ['Vier Stufen.', 'Eine Methode.'],
body: 'Bedrohungsanalyse, Engineering, Verifikation und Monitoring, als eine durchgehende Methode statt vier getrennter Projekte. Modular, integrierbar und gebaut für den operativen Alltag.',
body: 'Bedrohungsanalyse, Engineering, Verifikation und Monitoring, auf Ihre Software angewandt als eine durchgehende Methode statt vier getrennter Projekte. Sie läuft neben Ihrer Entwicklung, modular und integrierbar.',
imageAlt: 'Eine Sicherheitsingenieurin hinter Glas, die Spiegelungen von Monitoring-Bildschirmen liegen über ihrem Gesicht.',
cta: 'CipherVerifier ansehen',
diagramTitle: 'Die CipherCell-Methode',
diagramDesc: 'Ein geschlossener Kreislauf aus vier Stufen: Bedrohungsanalyse, Engineering, Verifikation und Monitoring. Jede Stufe hat ihre Instrumente: Bedrohungsmodell, Angriffspfade und CI/CD-Integration; Zero Trust, Schlüsselmanagement und sichere APIs; unabhängige Prüfung, Härtung und Tests; Angriffserkennung, Security-Logging und Resilienzprüfung. Das Monitoring fließt zurück in die Bedrohungsanalyse.',
stages: [
@@ -218,17 +294,17 @@ const de: typeof en = {
stages: {
eyebrow: 'Was wir tun.',
items: [
{ title: 'Bedrohungs\u00ADanalyse', body: 'Automatisierte, empirische Analyse realer Angriffspfade. Sie läuft in Ihrer CI/CD-Pipeline und liefert kontinuierliche Absicherung.' },
{ title: 'Engineering', body: 'Zero-Trust-Durchsetzung, Schlüsselmanagement und sicheres API-Design als modulare, kombinierbare Werkzeuge für komplexe Deployments.' },
{ title: 'Verifikation', body: 'Unabhängige Prüfung von Sicherheitsmaßnahmen, Härtungs-Checklisten und anwendungsnahe Tests über alle Komponenten.' },
{ title: 'Monitoring', body: 'Angriffserkennung und Security-Logging, vom ersten Deployment bis zur laufenden Resilienzprüfung.' },
{ title: 'Bedrohungs\u00ADanalyse', body: 'Automatisierte, empirische Analyse realer Angriffspfade durch Ihre Software. Sie läuft in Ihrer CI/CD-Pipeline und liefert kontinuierliche Absicherung.' },
{ title: 'Engineering', body: 'Zero-Trust-Durchsetzung, Schlüsselmanagement und sicheres API-Design als modulare, kombinierbare Komponenten für Ihre Codebasis und Deployments.' },
{ title: 'Verifikation', body: 'Unabhängige Prüfung von Code und Sicherheitsmaßnahmen, Härtungs-Checklisten und anwendungsnahe Tests über alle Komponenten.' },
{ title: 'Monitoring', body: 'Angriffserkennung und Security-Logging, in die Software eingebaut, vom ersten Deployment bis zur laufenden Resilienzprüfung.' },
],
},
sectors: {
title: 'Für wen wir arbeiten',
items: [
{ title: 'Private 5G- und Campusnetze', body: 'Disaggregierte Netze mit offenen Schnittstellen und einer Lieferkette aus vielen Herstellern. Unser Ursprung, und noch immer der schwerste Fall.', imageAlt: 'Eine private 5G-Antenne mit Funkschrank auf der Attika eines Fabrikdachs, Kräne im Hintergrund.' },
{ title: 'Kritische Netzinfrastruktur', body: 'Netze, in denen ein Einbruch die Produktion stoppt, Anlagen beschädigt oder Menschen gefährdet.', imageAlt: 'Ein Umspannwerk senkrecht von oben: Transformatoren, Schaltanlagen und eine Zufahrt.' },
{ title: 'Private 5G- und Campusnetze', body: 'Softwaredefinierte Netze mit offenen Schnittstellen und einer Lieferkette aus vielen Herstellern. Unser Ursprung, und noch immer der schwerste Fall.', imageAlt: 'Eine private 5G-Antenne mit Funkschrank auf der Attika eines Fabrikdachs, Kräne im Hintergrund.' },
{ title: 'Kritische Netzinfrastruktur', body: 'Steuerungs- und Netzsoftware, bei der ein Einbruch die Produktion stoppt, Anlagen beschädigt oder Menschen gefährdet.', imageAlt: 'Ein Umspannwerk senkrecht von oben: Transformatoren, Schaltanlagen und eine Zufahrt.' },
{ title: 'Cloud-native Systeme', body: 'Container, Service Meshes und CI/CD-Pipelines, die Infrastruktur tragen. Sicherheit als Code, neben Ihrem.', imageAlt: 'Ein Kaltgang zwischen Serverschränken in einem Rechenzentrum, ein Techniker weit hinten.' },
],
},
@@ -251,7 +327,7 @@ const de: typeof en = {
insights: {
title: 'Einblicke',
all: 'Alle Einblicke',
intro: 'Notizen zur Sicherheit privater 5G-Netze, kritischer Netze und der Systeme, die sie betreiben.',
intro: 'Notizen zur Softwaresicherheit in privaten 5G-Netzen, kritischen Netzen und den Systemen, die sie betreiben.',
readingTime: (min: number) => `${min} Min. Lesezeit`,
event: { category: 'Veranstaltung', when: 'September 2026', text: 'Treffen Sie uns beim xG‑ALOE Plugfest 2026.', link: 'Zur Veranstaltung', href: 'https://xg-aloe.de/xg-aloe-plugfest-2026/' },
back: 'Alle Einblicke',
@@ -260,7 +336,7 @@ const de: typeof en = {
},
contact: {
title: 'Sprechen Sie mit uns.',
body: 'Komplexe Infrastruktur, eine knappe Compliance-Frist oder ein Problem, das noch niemand gelöst hat. Wir antworten innerhalb von 24 Stunden.',
body: 'Komplexe Software, eine knappe Compliance-Frist oder ein Problem, das noch niemand gelöst hat. Wir antworten innerhalb von 24 Stunden.',
address: ['Innstr. 43', '94032 Passau, Deutschland'],
phone: '+49 (0)851 / 509-3218',
phoneHref: 'tel:+498515093218',
@@ -276,7 +352,7 @@ const de: typeof en = {
},
},
footer: {
mission: 'Security Engineering für Infrastruktur, in der Ausfälle Folgen haben.',
mission: 'Security Engineering für Software, bei der Ausfälle Folgen haben.',
company: 'Unternehmen',
capabilities: 'Leistungen',
contact: 'Kontakt',
@@ -291,9 +367,77 @@ const de: typeof en = {
en: 'English',
de: 'Deutsch',
},
verifier: {
eyebrow: 'Produkt',
title: ['CipherVerifier.', 'Jeder Fund, verifiziert.'],
cta: 'Sprechen Sie mit uns',
burstDesc: 'Siebzehn dünne Linien laufen in einem Punkt zusammen und verlassen ihn als eine Linie.',
intro: {
lines: ['Siebzehn Scanner.', 'Eine Liste.'],
p1: 'CipherVerifier ist eine selbst gehostete Pipeline. Sie nimmt ein Repository, lässt siebzehn statische Analysewerkzeuge und zwei Stücklisten-Generatoren darüber laufen und führt alles, was sie melden, in einem Vokabular zusammen.',
p2: 'Dann tut sie, was Scanner nicht können. Sie liest jeden Fund in seinem Kontext, entscheidet, ob er echt ist, und ordnet den Rest nach Sicherheitsrelevanz. Das Ergebnis ist eine Liste, die ein Engineer abarbeiten kann, kein Stapel Meldungen.',
},
imageAlt: 'Ein Software-Engineer am Schreibtisch am Abend, der Quellcode spiegelt sich im dunklen Fenster neben den Monitoren.',
how: {
title: 'So arbeitet CipherVerifier.',
groups: [
{
title: 'Ein Lauf, fünf Stufen.',
body: 'Ein Scan beginnt bei einem Commit, einem Branch oder einem hochgeladenen Archiv. Stücklisten-Erzeugung und Abhängigkeitsanalyse laufen in der einen Bahn, statische Analyse, Klassifikation und Rangfolge in der anderen. Jede Stufe ist ein eigenes Werkzeug mit eigenem Status und lässt sich pro Repository abschalten.',
desc: 'Ein Repository teilt sich in zwei Bahnen. Die obere Bahn erzeugt die SBOM und lässt sie von Dependency-Track analysieren. Die untere Bahn führt SAST-Scans aus, dann die Polygraph-Klassifikation, dann die Prism-Rangfolge. Beide Bahnen münden im Bericht.',
labels: { repo: 'Repository', sbom: ['SBOM-', 'Erzeugung'], sbomSub: 'Syft und cdxgen', dt: ['Dependency-Track-', 'Analyse'], dtSub: 'Schwachstellenbericht', sast: ['SAST-', 'Scan'], sastSub: 'Siebzehn Analysewerkzeuge', poly: ['Polygraph-', 'Klassifikation'], polySub: 'Urteil in zwei Stufen', prism: ['Prism-', 'Rangfolge'], prismSub: 'Nach Sicherheitsrelevanz', report: 'Bericht' },
},
{
title: 'Siebzehn Werkzeuge, ein Vokabular.',
body: 'Rayfusion erkennt die Sprachen im Baum und startet die passenden Analysewerkzeuge, jedes in einem eigenen Container, parallel. Ein Sicherheitsprofil beschränkt jedes Werkzeug auf Regeln, die eine Schwachstelle beschreiben, nicht einen Stil. SarifCombiner führt die Berichte dann zusammen: ein CWE-Vokabular mit 970 Einträgen, überlappende Funde derselben Schwachstelle zu einem verschmolzen, Regeln ohne CWE einmal aufgelöst und gemerkt.',
desc: 'Siebzehn Analysewerkzeuge laufen in einem Bericht zusammen. Darauf folgen drei Schritte: ein CWE-Vokabular, ein Fund pro Schwachstelle und aufgelöste Regeln ohne Zuordnung.',
tools: ['Bandit', 'Checkov', 'Cppcheck', 'Fallow', 'Fallow Security', 'Gitleaks', 'Gosec', 'Hadolint', 'KubeLinter', 'mypy', 'Pylint', 'Roslyn Analyzers', 'Safety', 'Semgrep', 'Staticcheck', 'Trivy', 'Vulture'],
labels: { one: 'Ein Bericht', oneSub: 'SARIF, validiert', cwe: ['Ein CWE-', 'Vokabular'], cweSub: '970 Einträge', merge: ['Ein Fund pro', 'Schwachstelle'], mergeSub: 'Überlappungen verschmolzen', resolve: ['Regeln ohne CWE', 'aufgelöst'], resolveSub: 'Einmal, dann gemerkt' },
},
{
title: 'Urteil in zwei Stufen.',
body: 'Polygraph liest jeden Fund mit seinem Code, seiner Herkunft und der Datei darum herum und gibt ein Urteil mit Sicherheit und Priorität zurück. Ein Fund, bei dem es unsicher ist, oder ein Fehlalarm-Urteil, dessen es sich nicht sicher ist, geht an einen zweiten Agenten, der das Repository selbst lesen kann, innerhalb eines festen Budgets an Anfragen und Tokens. Jedes Urteil trägt seine Begründung. Ein gemerktes Urteil wird nur wiederverwendet, wenn der Code, auf dem es beruht, unverändert ist.',
desc: 'Ein Fund wird mit seinem Kontext zusammengestellt: Herkunft, Code-Ausschnitt, Dateikopf und optional eine Datenfluss-Spur. Eine erste Stufe liefert ein statisches Urteil. Sichere Urteile gehen direkt ins Ergebnis. Unsichere Urteile und Fehlalarm-Urteile unterhalb hoher Sicherheit werden an einen zweiten Agenten eskaliert, der Dateien liest, Verzeichnisse auflistet und den Baum durchsucht, innerhalb eines begrenzten Budgets. Das Ergebnis ist echt, Fehlalarm oder unsicher, mit Sicherheit, Priorität und Begründung.',
labels: { finding: 'Fund', context: 'Kontext', ctx: ['Herkunft', 'Code-Ausschnitt', 'Dateikopf', 'Datenfluss-Spur, optional'], stage1: ['Statisches', 'Urteil'], stage2: 'Eskalation', tools: ['Liest Dateien', 'Listet Verzeichnisse', 'Durchsucht den Baum', 'Begrenztes Budget'], verdict: 'Urteil', outcomes: ['Echt', 'Fehlalarm', 'Unsicher'], carries: 'Sicherheit, Priorität, Begründung', certain: 'Sicher', unsure: 'Unsicher oder unsicherer Fehlalarm' },
},
{
title: 'Die Seite der Abhängigkeiten.',
body: 'Zwei Generatoren, Syft und cdxgen, erzeugen je eine Stückliste. Spectograph führt sie anhand der Package-URL zu einem CycloneDX-Dokument zusammen, das vollständiger ist als jedes einzelne. Deptrace übergibt es an Dependency-Track, eine gehostete Instanz oder einen für den Scan gestarteten Container, und holt den Schwachstellenbericht je Komponente zurück.',
desc: 'Syft und cdxgen erzeugen je eine SBOM. Sie verschmelzen zu einem CycloneDX-Dokument, dedupliziert nach Package-URL, das an Dependency-Track geht, gehostet oder kurzlebig, und als Schwachstellenbericht je Komponente in den Scan-Bericht zurückkehrt.',
labels: { syft: 'Syft', cdxgen: 'cdxgen', sbom: 'Eine SBOM', sbomSub: ['CycloneDX 1.6', 'Zusammengeführt nach Package-URL'], dt: 'Dependency-Track', dtSub: ['Gehostete Instanz', 'oder kurzlebiger Container'], vdr: ['Schwachstellen-', 'bericht'], vdrSub: 'Je Komponente, mit Schweregrad', report: 'Bericht' },
},
{
title: 'Gebaut, um betrieben zu werden.',
body: 'Repositories kommen von GitHub, GitLab und Gitea, als hochgeladenes Archiv oder aus Ihrer CI über einen Webhook mit Token. Einstellungen vererben sich von global über Projekt zu Repository, sodass ein Standard vielen Teams dient und ein einzelnes Repository trotzdem abweichen kann. Scans warten in der Reihe, laufen parallel und melden jede Stufe live. Was herauskommt, bleibt: der zusammengeführte Bericht, der Schwachstellenbericht, ein PDF und jedes Artefakt in Ihrem eigenen Speicher.',
desc: 'Quellen links, GitHub, GitLab, Gitea, Archiv-Upload und CI-Webhook, speisen CipherVerifier in der Mitte. Ausgaben rechts: Live-Pipeline, zusammengeführter Bericht, Schwachstellenbericht, PDF-Bericht und Artefakte in Ihrem Speicher. Darunter vererben sich Einstellungen von global über Projekt zu Repository.',
labels: { hub: 'CipherVerifier', sources: ['GitHub App', 'GitLab', 'Gitea', 'Archiv-Upload', 'CI-Webhook'], outputs: ['Live-Pipeline', 'Zusammengeführter Bericht', 'Schwachstellenbericht', 'PDF-Bericht', 'Artefakte in Ihrem Speicher'], scopes: ['Global', 'Projekt', 'Repository'], scopesTitle: 'Einstellungen vererben sich' },
},
] as HowGroups,
},
cards: [
{ title: 'Das zuerst beheben.', body: 'Prism ordnet die verifizierten Funde nach Sicherheitsrelevanz und stellt die Rangliste an den Anfang der Scan-Seite. Als Fehlalarm eingestufte Funde können ganz entfallen.' },
{ title: 'Läuft dort, wo Ihr Code ist.', body: 'Ein Installer richtet Server, Datenbank, Objektspeicher und die Scanner-Images ein. Sprachmodell und Dependency-Track-Instanz sind Endpunkte, die Sie wählen.' },
{ title: 'Jedes Urteil trägt seine Begründung.', body: 'Jeder klassifizierte Fund bekommt eine Zeile im Audit-Log mit Urteil, Sicherheit und Begründung. Jede Stufe hält fest, wann sie begann und wie sie endete. PDF-Bericht und Artefakte bleiben, solange der Scan existiert.' },
],
facts: {
title: 'In Zahlen.',
items: [
{ value: '17', label: 'Statische Analysewerkzeuge, jedes im eigenen Container.' },
{ value: '14', label: 'Programmiersprachen, dazu Docker, Kubernetes, Helm, Terraform und CloudFormation.' },
{ value: '2', label: 'Stücklisten-Generatoren, zu einem Dokument zusammengeführt.' },
{ value: '970', label: 'CWE-Einträge in einem Vokabular.' },
{ value: '5', label: 'Pipeline-Stufen, jede pro Repository abschaltbar.' },
],
},
outro: {
title: 'Sprechen Sie mit uns über Ihre Repositories.',
body: 'Bringen Sie ein Repository mit, das Sie gut kennen, und wir zeigen Ihnen, was CipherVerifier daraus macht: was es findet, was es ausschließt, und warum.',
cta: 'Sprechen Sie mit uns',
},
},
team: {
title: ['Forschende,', 'Ingenieure,', 'Architekten.'],
intro: 'Die Menschen, die sich in die Probleme vertiefen, auf die es ankommt: von 5G und privaten Netzen bis zu Hardware, Software und Cloud-nativen Systemen.',
intro: 'Die Menschen, die sich in die Probleme vertiefen, auf die es ankommt: von Netzsoftware und privaten 5G-Netzen bis zu Hardware und Cloud-nativen Systemen.',
photoAlt: 'Das CipherCell-Team auf einer Treppe in den Büros in Passau.',
links: { github: 'GitHub', linkedin: 'LinkedIn', email: 'E-Mail' },
roles: {
+22
View File
@@ -0,0 +1,22 @@
---
import Base from '../layouts/Base.astro';
import Hero from '../components/verifier/Hero.astro';
import Intro from '../components/verifier/Intro.astro';
import HowItWorks from '../components/verifier/HowItWorks.astro';
import Cards from '../components/verifier/Cards.astro';
import Facts from '../components/verifier/Facts.astro';
import Outro from '../components/verifier/Outro.astro';
import { t, type Lang } from '../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
---
<Base lang={lang} title={ui.meta.verifier.title} description={ui.meta.verifier.description}>
<Hero lang={lang} />
<Intro lang={lang} />
<HowItWorks lang={lang} />
<Cards lang={lang} />
<Facts lang={lang} />
<Outro lang={lang} />
</Base>
+4
View File
@@ -0,0 +1,4 @@
---
import Verifier from './_Verifier.astro';
---
<Verifier lang="en" />
+4
View File
@@ -0,0 +1,4 @@
---
import Verifier from '../_Verifier.astro';
---
<Verifier lang="de" />
+31
View File
@@ -0,0 +1,31 @@
/**
* A vertical tab list: buttons with role="tab", each controlling a panel, plus optional
* stage elements ([data-stage-item]) of which only the selected one is shown.
* Arrow keys, Home and End move the selection; a click selects.
*/
export function initTabs(root: HTMLElement): void {
const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
const panels = tabs.map((t) => document.getElementById(t.getAttribute('aria-controls')!)!);
const stages = Array.from(root.querySelectorAll<HTMLElement>('[data-stage-item]'));
const select = (index: number, focus = false) => {
tabs.forEach((tab, i) => {
const on = i === index;
tab.setAttribute('aria-selected', String(on));
if (on) tab.removeAttribute('tabindex'); else tab.setAttribute('tabindex', '-1');
if (on) panels[i].setAttribute('data-open', ''); else panels[i].removeAttribute('data-open');
if (on) stages[i]?.setAttribute('data-active', ''); else stages[i]?.removeAttribute('data-active');
});
if (focus) tabs[index].focus();
};
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(i));
tab.addEventListener('keydown', (e) => {
const n = tabs.length;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); select((i + 1) % n, true); }
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); select((i - 1 + n) % n, true); }
if (e.key === 'Home') { e.preventDefault(); select(0, true); }
if (e.key === 'End') { e.preventDefault(); select(n - 1, true); }
});
});
select(0);
}
+9 -2
View File
@@ -1,5 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist",
"node_modules",
"product_repo"
]
}