NEWBravos is availableCreate your account

Your code changed.Did your pentest?

Built for developers and security teams.

Your code keeps changing.
Bravos keeps testing.

Threat model as code. Pentest as code.
AI-agentic orchestration.

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, so developers and security work from shared context.

Fix a finding, replay the test, and check what changed.

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
  • Tyler Technologies

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

Follow one upload handler from “is this safe?” to a verified fix.

Maya wrote the avatar upload. Alex tested it last week. In three parts: the question nobody could answer, the risk written into the code, and the same test replayed against the fix.

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. Free on your own machine; 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.