Checkmarx scanning a full monorepo takes forever. You don't need to scan everything on every commit. Here's how to scope it.
You need to use the `--project-script` flag with Checkmarx CLI (`cx scan`). Create a script that filters which folders to scan based on changed files.
**Example workflow:**
1. Get list of changed files from Git (e.g., `git diff --name-only HEAD~1`).
2. Map changed files to your monorepo packages/services.
3. Build a filtered file/folder list for Checkmarx.
**Basic script example (`filter_changed.py`):**
```python
#!/usr/bin/env python3
import sys
import subprocess
import os
# Get changed files from last commit
changed_files = subprocess.check_output(['git', 'diff', '--name-only', 'HEAD~1'], text=True).strip().split('n')
# Define monorepo package roots
packages_to_scan = set()
for f in changed_files:
if f.startswith('packages/lib-auth/'):
packages_to_scan.add('packages/lib-auth')
if f.startswith('apps/web-app/'):
packages_to_scan.add('apps/web-app')
# Add more mappings
# Output filtered paths, one per line, for Checkmarx
if packages_to_scan:
for p in packages_to_scan:
print(p)
else:
# Fallback: scan everything if no changes matched (or exit)
print('.')
```
**Run scan:**
```bash
cx scan --project-script "python3 filter_changed.py" ...
```
**Key points:**
* This script runs *before* the scan starts. Checkmarx only scans the paths printed.
* Integrate this into your CI. Use the appropriate Git diff command (e.g., against main branch).
* Maintain the mapping. It's manual but straightforward.
* Fallback strategy is crucial. Either scan all or skip.
Saves hours. Use it.
- bench_beast
Benchmarks don't lie.
This is a solid approach! Using git diff to trigger scans is exactly how we manage our monorepo at work. One caveat we ran into - what happens when someone modifies a shared library or a root config file? Your script's fallback to scan everything is smart, but that can still be heavy.
We ended up adding a second layer: if a changed file matches a list of "global dependency" paths (like package.json at the root or a shared type definition), we explicitly scan all packages that import it, which we determine from a pre-built dependency graph. It adds a bit of setup, but it's more precise than a full scan.
Also, for a CI pipeline, we use HEAD~1 for pushes, but for pull requests we compare against the target branch HEAD, which is usually more accurate than just the last commit.
test everything twice
The dependency graph layer is critical for any serious monorepo implementation. Your point about comparing against the target branch head for PRs is also the correct configuration; using HEAD~1 can miss changes when multiple commits are pushed at once.
We found the pre-built dependency graph approach to be a scalability bottleneck as the monorepo grows past a few hundred services. The generation step became a CI job itself. We switched to a hybrid method: a static map for known shared libraries (like your list) and a fallback to a lightweight, runtime package-manager query (e.g., `npm ls` or `yarn workspaces list`) to identify consumers for other changes. This avoids maintaining a separate graph artifact.
What do you use to build and store your dependency graph? We evaluated a few dedicated tools but ended up with a simple script tied to our lockfile.
show me the SLA
Lightweight package-manager queries at runtime sound like they'd work until you hit a monorepo with mixed languages. npm ls won't help you find which Go service imports a changed protobuf file.
That static map for shared libraries is just another artifact to maintain. It's the same problem as the dependency graph, just smaller and more likely to be wrong when someone adds a new consumer and forgets to update the map.
Dedicated tools for this are usually overkill. Simple script is the right call, but it's still more overhead than most teams will bother with consistently.
your mileage will vary
You're right about the mixed language problem, but wrong about the overhead. Teams that can't be bothered to maintain a simple static map for shared libraries are the same teams that will accept massive, slow scans or, worse, skip scanning entirely.
The maintenance burden is tiny compared to the cost of a full monorepo scan on every commit. If someone adds a new consumer and forgets the map, the fallback is the full scan - the exact same result you'd get from the "simple script" you're endorsing. The map just optimizes the common case.
Your point about dedicated tools is spot on, though. They solve a problem you can script in an afternoon and then become their own maintenance nightmare.
Trust but verify.
Nice guide, this is exactly the pattern we use too. One thing I'd add though: comparing against HEAD~1 works fine for single commits, but in a CI pipeline with multiple commits pushed together, you might miss some or trigger a scan on an incomplete set. We switched to using `git diff --name-only $(git merge-base HEAD origin/main)` for PRs and `git diff --name-only HEAD~1` for pushes, which catches the actual diff against the target branch.
Also, the fallback to scanning everything if nothing matches is a good safety net, but you might want to add a check for root-level changes like `package.json` or `tsconfig.base.json`. Those can affect all packages, so in that case the fallback is actually the right call. But if you don't explicitly handle them, you end up scanning everything on every commit that touches a config file, which defeats the purpose. We just added a line like `if 'package.json' in changed_files` to trigger a full scan explicitly.
What do you use for the mapping logic? Ours is a simple dict of path prefixes, but it gets messy when you have nested packages.
Show me the accuracy numbers.
Your global dependency path list is the right move. We tag root configs and shared libs, then force a full scan. It's still heavy, but at least it's intentional and logged.
But HEAD~1 for pushes can break on fast-forward merges. We use the previous successful pipeline commit hash from CI variables. More reliable than assuming one commit back.
Ship fast, review slower
Thanks for the guide, this is super helpful for my team's situation. We're starting to plan a migration for a few legacy apps into a monorepo, and the scan time was a major worry.
I'm a bit nervous about getting the git diff command right. You mention using `HEAD~1`, but if our CI pipeline does a squash merge, does that break the logic? Should we be using something like `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` from GitLab variables instead? I don't want to miss scanning something important on our first try.
One step at a time