All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.13

Grey-box agentic pentest — one credential per role, and the matrix it fills

C2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/C2.13.mp4
Risk

Object-level authorisation is assumed correct because the endpoint list was covered, and BOLA lives in the cells nobody enumerated.

Control

A roles-by-objects-by-verbs matrix with every cell marked tested, assumed or unreachable, and the assumed cells ranked by blast radius.

The lab

Fill the matrix for CyberTravels with three roles and find the untested cells that matter.

◉Use case relevance

The engagement touched every endpoint and called itself complete. Authorisation does not live in endpoints — it lives in cells, one per role per object per verb — and the two that would have mattered here were never sent a single request.

At CyberTravels. pentest.Matrix — roles by objects by verbs, every cell tested, assumed or unreachable. assumed means nobody looked, and it is exactly where rows 1, 2 and 4 of LABELS.md have been sitting while the endpoint list showed full coverage.

◴What this lesson is — Day 0, Day 1, Day 2

Why endpoint coverage is the wrong denominator for object-level authorisation, and the grid that is the right one: roles by objects by verbs, with the untested cells ranked by blast radius rather than listed.

Day 0 — why

Object-level authorisation is assumed correct because the endpoint list was covered, and broken object access lives in the cells nobody enumerated.

Day 1 — how

Fill a roles-by-objects-by-verbs matrix from one credential per role, and mark every cell tested, mismatched or untested.

Day 2 — measure

Cell coverage, not endpoint coverage: 12 of 30 cells on the sample, with two mismatches and the two highest-cost cells never tested.

Measure this

what to measuredenominator or target
authorisation cell coverage, not endpoint coverage12 of 30 cells on the sample
mismatches, and untested cells ranked by blast radiuscount, and the two highest-cost cells

▦The framework, and how it works

   authorisation is a CELL, not an endpoint

              booking(own)  booking(other)  refund  audit_log
   traveller     ok            ALLOW(!)      deny      ?
   agent-svc     ok               ?          allow     ?
   finance       ok            allow         allow     allow

   ALLOW(!) = design says deny, estate says allow -> finding
   ?        = never tested. ranked by blast radius, not schema order.

   endpoint coverage: 100%.   cell coverage: 40%.
   the two audit_log cells nobody sent a request to are the ? that matter.

Grey box is the mode that can actually find broken object-level authorisation, because it holds more than one identity. It is also the mode most often reported wrong, because the engagement touches every endpoint, calls itself complete, and never notices that authorisation is a property of the cell — this role, this object, this verb — and not of the endpoint.

The matrix is the whole method. Roles down one axis, objects and verbs across the other, and every cell in one of three states: tested, with what came back; mismatched, where the estate disagrees with the design; or untested, which is data rather than a gap to hide.

Two numbers fall out that an endpoint-coverage report cannot produce. The mismatches are the findings. The untested cells, ranked by what a wrong answer would cost rather than by the order the endpoints appear in the schema, are the backlog — and the coverage fraction is what stops "we tested everything" standing when 40% of the cells were never sent a request.

One credential per role is the minimum and the enabling fact. With one account you can test that a feature works; the second account is what turns a functionality test into a security test.

2 · Demo — three roles, five objects, two verbs, thirty cells

3 · Where it breaks — every endpoint touched, most cells untested

4 · The control — the matrix, its mismatches, and the ranked gaps

5 · The procedure, as a skill

The skill fills in a grid: every role, against every object, against every verb. Then it compares the grid against what the design says should happen.

It flags the cells where the running estate disagrees with the design.

Then it ranks the cells nobody tested by blast radius — by how much damage each one could do — rather than in the order they happen to appear in the schema.

▶Real time execution as skill

The skill — skills/redteam/greybox-authorization-matrix/SKILL.md

name: greybox-authorization-matrix
description: >-
  With one credential per role and a schema but no source, fill a
  roles-by-objects-by-verbs matrix, flag design mismatches, and rank the cells
  nobody tested by blast radius. Use for a grey-box engagement, or when
  endpoint coverage is being reported as authorisation coverage.
allowed-tools: Read, Grep, Glob

Authorisation lives in cells, not endpoints

Grey box is the mode that can actually find broken object-level authorisation, because it has more than one identity. It is also the mode most often reported wrong: the engagement touches every endpoint, calls itself complete, and never notices that authorisation is a property of the cell — this role, this object, this verb — not of the endpoint.

The matrix makes the untested cells visible, which is the whole point.

When to use this

An engagement with low-privilege accounts and a schema, a BOLA/BFLA hunt, and any report whose coverage claim is a count of endpoints.

Procedure

1 — List what you were handed. One credential per role is the minimum and the enabling fact; also the schema, a couple of ids you own and a couple you must not, the matrix as designed, and the scope so the run is not an incident.

2 — Build the full matrix. Roles × objects × verbs. Write down what the design says each cell should be, before touching anything.

3 — Exercise cells and record what came back. Every cell you do not exercise stays untested — that is data, not a gap to hide.

4 — Report three things. Mismatches, where the estate disagrees with the design. Untested cells, ranked by what a wrong answer would cost, not by schema order. And the coverage fraction, so "we tested everything" cannot stand when 40% of the cells were never sent a request.

Example

tested 12 of 30 cells (40%)
2 mismatch(es):
  ! traveller can read booking(other): designed deny, observed allow

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "data_sources": [{"source": "str", "gives": "str"}],
  "cells": [{"role": "str", "object": "str", "verb": "str",
             "design": "allow|deny", "observed": "allow|deny|null"}],
  "mismatches": [{"role": "str", "object": "str", "verb": "str",
                  "design": "str", "observed": "str"}],
  "untested": [{"role": "str", "object": "str", "verb": "str", "blast": 0}],
  "coverage": 0.0
}

Failure modes

  • Reporting endpoint coverage as authorisation coverage. Every endpoint touched, most cells untested.
  • Ranking untested cells by schema order. The audit-log write matters more than the profile read; the order they appear in the spec is noise.
  • Testing only your own objects. One id is a functionality test; the second id is the security test.
  • Trusting the design column. It is what the team believes is enforced, which is the thing under test.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons C2.13

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick c2-13-grey-box-agentic-pentest:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.13type /c2-13-grey-box-agentic-pentest
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.13type /c2-13-grey-box-agentic-pentest in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.13type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.13type $c2-13-grey-box-agentic-pentest, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py C2.13

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at C2.13 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.13 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The engagement exercises 12 of 30 cells (40%) and finds two mismatches — a traveller reading another traveller's booking and the agent service writing one — while the two highest-cost cells, both on the audit log, were never sent a request. Endpoint coverage would have called this complete.

✎Your turn

Build the matrix for one service you own with two real accounts. The cell you least want to test — an admin verb on another tenant's object — is the one whose result you most need to know.

Tools used

OpenAPI

Skill source: skills/redteam/greybox-authorization-matrix

← PreviousC2.12 · Black-box agentic pentest — inference, and refusing to report it as fact