NEWThe Bravos CLI is outInstall Bravos

Your code changed.Did your pentest?

Built for developers and security teams.

Your code keeps changing.
Bravos keeps testing.

Bravos writes a threat model into your code, attacks a running copy of your app with real logins, and writes what it proves back into the source.

bravos

pipx install bravos
  • Runs on your machine
  • No account
  • Python 3.11+

Teams we’ve worked with

  • Augnito
  • Digitata Networks
  • ScribeTech
  • KooApps
  • Pancake
  • Futurotec
  • DAC

Acknowledged our vulnerability reports

  • IBM
  • Dell
  • Philips
  • Volkswagen
  • Fortinet
  • Portainer
  • Temporal
  • Mattermost
  • MIT
  • Axway
  • Fortanix
  • GeoComply
  • Ruckus
  • Keepnet Labs

Organisations that acknowledged vulnerabilities we reported. Not clients, and not endorsements. Our responsible disclosure pledge

What is Bravos?

Every function in your repo. Narrowed to the ones an attacker can reach.

Most security tools hand you a list of what might be wrong, and most of it can never be reached. Bravos starts from your repository instead. It writes the threat model into your code with GuardLink, maps what each route can actually reach, and runs SAST only where a request can arrive. Then an agent attacks a running copy with real logins until each suspect is proven or ruled out, and keeps the request that decided it: proven GET /uploads/avatar.html → 200 text/html Every proof becomes a security test in your CI/CD pipeline, so a fixed bug stays fixed.

The Bravos loop on one repositoryYour repo is annotated into a threat model, traced into a code graph of what each entry point reaches, scanned with SAST only on reachable paths, attacked on a running copy until each suspect is proven or ruled out, and each proof is kept as a Cert-X-Gen test in CI. The loop runs again on every commit.every commitYour repo84 functionsannotateThreat modelGuardLink annotationstraceCode graph23 reachable from 4 entry pointsscanSAST3 suspects on a pathattackProven exploitable■ 2 proven■ 1 ruled outkeepTests in CICert-X-Gen · every pull request

The story, part 1: is this upload handler safe?

Part 1 of 3: the questionSkip to part 2

Loading Maya and Alex’s story…

Open the full story on its own page

Part 2 of 3

Write down the risk.
Get a test you can run again.

Instead of re-filing the ticket, Maya writes the risk into avatar.ts as a GuardLink annotation. Cert-X-Gen turns it into a test anyone can run against a local copy. Pick a risk and write it down. This is a scripted replay of the acme-shop sample; nothing here sends traffic.

src/api/avatar.ts

1const TYPES: Record<string, string> = {2  '.png': 'image/png',3  '.jpg': 'image/jpeg',4  '.html': 'text/html',5};6 7export async function handleAvatarUpload(req, res) {8  const name = path.basename(String(req.headers['x-filename']));9  const body = await readBody(req);10  await writeFile(path.join(UPLOAD_DIR, name), body);11  res.writeHead(201).end(JSON.stringify({ url: `/uploads/${name}` }));12}13 1415export async function serveUpload(req, res) {16  const name = path.basename(req.url ?? '');17  const type = TYPES[path.extname(name)];18  if (!type) return void res.writeHead(404).end();19  res.writeHead(200, { 'content-type': type });20  res.end(await readFile(path.join(UPLOAD_DIR, name)));21}

templates/acme-avatar-html-upload.yaml

The test appears once the annotation is written.

# @id: acme-avatar-html-upload# @severity: high# @tags: stored-xss, avatar-api, cwe-79id: acme-avatar-html-uploadlanguage: yamlhttp:  - method: POST    path: ["{{BaseURL}}/api/avatar"]    headers: { x-filename: marker.html }    body: "<p>verification marker</p>"  - method: GET    path: ["{{BaseURL}}/uploads/marker.html"]    matchers-condition: and    matchers:      - { type: status, status: [200] }      - { type: word, part: header, words: ["text/html"] }

Nothing written down yet. Pick a risk, or start with the first.

Part 3 of 3

Same request.
A different answer.

Maya merged a fix to src/api/avatar.ts. Bravos replays Alex’s test against the new commit, byte for byte, and records what the application says now.

Choose which run to show
RequestIdentical in both runs
ResponseRun 1, before the fix

1. Upload

POST /api/avatar HTTP/1.1
Host: 127.0.0.1:4100
x-filename: avatar.html
Content-Length: 36

<p>acme-shop verification marker</p>
HTTP/1.1 201 Created
content-type: application/json

{"url":"/uploads/avatar.html"}

Expects 201: got 201

2. Fetch it back

GET /uploads/avatar.html HTTP/1.1
Host: 127.0.0.1:4100
HTTP/1.1 200 OK
cache-control: public, max-age=3600
content-type: text/html

<p>acme-shop verification marker</p>

Expects 200, text/html and the marker: all three matched

Abridged: Date and connection headers are left out.

Ledger entry

Finding
#stored-xss on #avatar-api, high, CWE-79
Test
acme-avatar-html-upload
Status
Confirmedat ff09f3f

The upload was stored and served back as text/html from the shop’s own origin.

Run 1, before the fix, commit ff09f3f. Verdict: Confirmed. The upload was stored and served back as text/html from the shop’s own origin.

What changed between the runs

e656a86 Validate avatar uploads by file signature

@@ handleAvatarUpload()
-  const name = path.basename(String(req.headers['x-filename'] ?? 'avatar.png'));
-  const body = await readBody(req);
+  const body = await readBody(req, MAX_BYTES);
+  const match = body && SIGNATURES.find((s) => body.subarray(0, s.magic.length).equals(s.magic));
+  if (!body || !match) {
+    res.writeHead(415, { 'content-type': 'application/json' });
+    return res.end(JSON.stringify({ error: 'Avatar must be a PNG or JPEG under 2 MB' }));
+  }
+  const name = `${randomUUID()}${match.ext}`;
@@ serveUpload()
+  const match = SIGNATURES.find((s) => path.extname(name) === s.ext);
+  if (!match) return void res.writeHead(404).end();
-      'content-type': TYPES[path.extname(name)] ?? 'application/octet-stream',
+      'content-type': match.type,
+      'x-content-type-options': 'nosniff',

Choose your starting point@flows <source> -> <target> [via <mechanism>]Documents data moving from one component to another, and what carries it.@flows in the GuardLink reference

Open at the foundation.
One loop with Bravos.

Use the open-source tools on their own. Or let Bravos run the handoffs between code, pentest, evidence and review, for one developer, a team or a whole company.

Open source / build your workflow

Write security assumptions into the code, then test them. You decide how the tools fit your workflow.

  • GuardLink: threat model as code, from annotations.
  • Cert-X-Gen: pentest as code, with executable templates.
  • You run the environments, authentication and evidence.
Bravos / run the whole loop

bravos

The same open-source foundation, run by AI agents from annotation to re-test. Runs on your own machine with no account; licensed per developer, for a team, or on an enterprise contract.

Make the next change
a more informed one.

See how the loop fits your repositories, your team and your security policy.