Inspiration
Teams switch on SAST, secret detection and container scanning, see a green pipeline, and assume they're covered. Nobody measures what actually gets through. We wanted the pipeline itself to be the thing under test: plant a realistic flaw in an ordinary-looking merge request, see which layer catches it, and make every miss teach the pipeline something.
What it does
RnB runs rounds against a small multi-user task tracker that we wrote to be secure:
- A reviewed flaw is planted in a merge request that reads like a normal feature. Each flaw comes from a 13-entry catalog (
arena/flaws.yaml) with its target files and a prediction of which defence should catch it. - The pipeline runs every layer: unit tests with a security baseline, typecheck, dependency audit, GitLab SAST, IaC scanning, secret detection, container scanning on both images, our own Semgrep rules, a security gate and a scope check.
- The blue team agent (a GitLab Duo flow) reads the finished pipeline and the diff, finds the flaw, commits a fix with a regression test, and posts a report. It never runs the MR's code.
- The referee agent (another Duo flow) reads the sealed round record that blue can't see, scores the round from evidence (the jobs that failed and why, blue's fix on the final pipeline), updates a scoreboard issue, and opens a
Harden:issue for anything the pipeline missed. - A person turns each
Harden:issue into a rule or test and squash-merges the round, so only the lesson (blue's regression test) lands onmain.
The headline metric is the escape rate: the share of planted flaws the pipeline lets through on its own.
Results
- Calibration (5 flaws, planted by hand): scanners alone would have shipped 3 of 5. An access-control flaw was caught only by a unit test. An ORDER BY injection and a hardcoded JWT secret escaped everything, although we had predicted that secret detection would catch the secret. Both escapes went through the learning loop: a project Semgrep rule and new regression tests. The same planted commits then failed. Escape rate: 2 of 5 → 0 of 5.
- Round 1 with the agents: the pipeline caught the planted access-control flaw with two tests, one of them the regression test blue wrote in calibration. Blue named the flaw, its CWE and its line, fixed it and added a test. The referee verified the fix from the code and the green pipeline and scored the round. After the squash-merge, the same plant now fails three tests: one round's fix is the next round's defence.
- Every reviewed plant is a standing check. Each one lives on its own branch, rebased onto
main, and its pipeline must stay red at the predicted layer.
How we built it
- GitLab Duo Agent Platform: two custom flows (blue team and referee), registered in the AI Catalog and started by mention and reviewer triggers. Blue has no command execution and no tool that reads issues, so it can't run attacker code or read the answer key. The referee files issues and keeps the scoreboard.
- GitLab CI/CD: a 13-job pipeline. GitLab SAST, IaC (KICS), secret detection and container scanning feed a security gate that fails on any new code-scanner finding at Medium or above, on a missing report, or on report files committed to the branch. A custom
rnb-rules-sastjob runs the rules the learning loop added. - Trust boundaries in CI: the gate, rules, catalog and scope check load from the trusted
mainbranch, never from the branch being judged.arena-scope-checkholds every agent commit to its allowed files using git history. It judges merge commits by what they add beyond a clean merge, and it fails closed on spoofed identities. - GitLab as the game board: round issues hold the sealed record and verdict, MRs hold the plant, fix and pipelines, labels mark
hardenandscoreboard, and squash merges keep plants out ofmain.
Challenges we ran into
- GitLab Duo declined the red team's role. In round 1 its model stopped with
finish_reason: content_filterwhen asked to write a flaw. In round 2 it stopped again when only asked to deploy a reviewed one. We kept agents where they work well, defending and judging, and people prepare each round. That's a real finding about agent guardrails, and we report it as one. - Real agent identities. The scope check rejected blue's first, correct fix: GitLab writes flow commits under the service account's own noreply address, which our config didn't list. It failed closed, we added the address, and the same commits passed. GitLab's own Duo Fix Pipeline flow, which ran on the failure, declined to work around the check.
- Hardening our own guard. Two adversarial reviews of the scope check found real bypasses: a change hidden in a merge commit, and tab characters in an author name shifting the committer field. Both are fixed and covered by a 40-case regression suite.
- Platform details:
notify_me_whenis IDE-only, so blue reports and the referee verifies the fix pipeline. The flow schema rejectedmax_cycles, so we now validate the YAML against GitLab's own schema before registering.
What we learned
Measuring beats assuming. Our own prediction was wrong once (the hardcoded secret). The cheapest defence for access control was a test that knows who owns what, not a scanner. And an agent that judges from evidence, not from the other agents' claims, is what makes the loop trustworthy.
What's next
Replay every past plant as a JUnit suite in GitLab's Tests tab. Add a merge request approval policy and a pipeline execution policy, so no one can push past the checks. Add a layer leaderboard that scores GitLab's own AI reviewers on the same plants, plus control MRs with no flaw to measure false alarms. And deploy the app with live production checks.
Built With
- docker
- express.js
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- gitlab-sast
- kics
- next.js
- node.js
- semgrep
- sqlite
- typescript
- vitest
Log in or sign up for Devpost to join the conversation.