The monorepo your scanner thinks is one app
On this page
Point a scanner at a monorepo and it answers the question you asked. One number, one repository, one verdict.
Now try to use it. Which of those findings are the payments team's? Which are in the public checkout service and which in an internal batch job nobody outside can reach? If the build goes red, whose merges stop? The report cannot say, because the tool was never told the repository holds several applications. It was told it is one directory.
That is the monorepo problem, and it has nothing to do with repository size.
One repository, one application, one owner, one verdict
That is the model almost every security tool is built on, and a monorepo breaks all four at once.
| What the tool assumes | What a monorepo does | What it costs |
|---|---|---|
| A repository is an application | Many applications share a checkout | One verdict describes nothing anybody owns |
| An application is a whole tree | An application is a subtree | A scan scoped to one cannot see the rest |
| A flaw is a finding | A shared library is imported everywhere | One flaw arrives as many findings |
| A commit produces one analysis | A commit produces one per team | The last result replaces the others |
The first three are annoying. The fourth is dangerous, and we will come back to it.
None of this is fixed by a smarter scanner. A scanner reads code and reports what it finds. Which part of the tree that code belongs to, who owns it, and whether it faces the internet are facts that are not in the code, so no amount of analysis recovers them. They have to be declared.
Describe the application, not the directory
Draugr takes a descriptor that names the parts:
project: acme
components:
- name: storefront
exposure: public
criticality: important
labels:
team: web
repositories:
- url: .
paths:
- services/storefront
- name: ledger
exposure: internal
criticality: critical
labels:
team: payments
repositories:
- url: .
paths:
- services/ledger
Nothing in that file is about scanners. It is a description of an application: what the parts are, how exposed each one is, how much depends on it, and who answers for it. The scanners are inferred from it.
Three things a monorepo needs follow immediately.
A finding belongs to a part, so the report is about parts.
DRAUGR FAIL draugr-demo 1.0 (scope: sca) 4.696s
18 P1 17 P2 2 P3 0 P4
CONTROLS
sca FAIL 18 P1 17 P2 2 P3
COMPONENTS
api FAIL 14 P1 13 P2 1 P3
storefront FAIL 4 P1 4 P2 1 P3
platform pass no findingsThirty-seven findings, and the question "whose" already has an answer. Two components ship the same vulnerable package and that is two findings with two owners, not one finding somebody has to route.
The band is the part's, not the repository's. exposure and criticality are declared per
component, so the same CVE is P1 in the public storefront and a backlog item in something internal.
A tool that sees one repository has to give every finding the same context, which means giving most
of them the wrong one.
A team can scan what it owns. A label on the component, and a pipeline that asks for it:
draugr scan draugr.saga.yaml --labels team=web
DRAUGR FAIL draugr-demo 1.0 (scope: 1 of 3 components) 6.061s
4 P1 4 P2 1 P3 0 P4
COMPONENTS
storefront FAIL 4 P1 4 P2 1 P3
api not scanned
platform not scannedA label rather than a list of component names, because a team knows the label it files under and a list in a pipeline file goes stale the first time somebody adds a service. The run says what it covered and what it did not, so a narrowed run cannot be mistaken for a whole one.
The fourth breakage, which is the dangerous one
Splitting scans across teams is the obvious move once the parts are named, and it is where tools that were not told about the parts do real damage.
On GitHub, code scanning decides what an upload replaces by category:
If you upload a second SARIF file for a commit with the same category and from the same tool, the earlier results are overwritten.
A tool that sets no category files everything under the empty one, so the second pipeline against a commit erases the first team's alerts. Both runs report success. GitHub publishes an action specifically for monorepos whose job is to assign a category per project.
It is not a GitHub quirk. Semgrep documents the same hazard against its own backend, and its version is worse:
if
--includeand--excludeare used in asemgrep ciscan without settingSEMGREP_REPO_DISPLAY_NAME, that scan might close findings that aren't detected because that part of the repo was not scanned.
The verb is close, not miss.
A missed finding
The scan did not look there. Bad, and the report says so: nothing claims that subtree was clean.
A closed finding
The scan did not look there, and something recorded that the problem is gone. Indistinguishable from a fix, made on your behalf.
Anything that receives results and keys them on "the repository" has this shape of bug available to it. A descriptor that names the parts is what closes it: Draugr derives each run's analysis category from the project and what the run covered, so two teams against one commit land in different categories and keep both sets of alerts. On GitLab and Azure, where the results land as pipeline artifacts rather than a merged store, the same declaration is what keeps a team's report about that team.
The part that stays hard
Carving a repository by ownership costs analysis across the boundary. A taint flow that starts in one team's service and ends in a shared library is invisible to a scan handed only the first. For Go, Draugr runs gosec and govulncheck over the whole module the parts share, and gives each finding to the part whose paths hold its file, so a flow into a shared package is still seen and is ranked by the part that owns it, while a scanner that reads files sees only the part it was handed.
Worth saying because the alternative is not a better tool, it is a different question. Scanning a tree as one application and scanning it as six owned parts are two things to want, and a part that genuinely needs the whole tree analyzed together is one component with a wider path. What nobody should accept is a tool that quietly does the second while reporting as though it did the first.
If you have a monorepo
- Make the parts explicit somewhere a tool can read. Everything else depends on it, and it has to live beside the code so it moves when the code does.
- Give each part its exposure and criticality. Without them every finding is ranked against the same assumption, and on a repository holding a public gateway and an internal cron job that assumption is wrong most of the time.
- Check what happens when two pipelines touch one commit. If the platform keeps only the last, you will not find out from either run.
- Gate on what a change introduced. A monorepo has a backlog by definition, and a gate on all of it blocks every merge until somebody turns it off.
Draugr does the first three from one file and the fourth with
draugr diff. The
monorepo guide covers the setup, including the decision that comes
first: whether the repository is one project with many components or several projects sharing a
checkout, which depends on who gates rather than on how the directories are arranged. The output
above came from the demo sandbox, which you can re-run.