This is a real walkthrough of applying the Free Security Scanning Guide to a production codebase. The app is a PHP web application with a React frontend, running on AWS (EC2, RDS, S3, ALB). We started with no Docker installed and no prior security scanning.
1. Starting Point
- macOS (Apple Silicon, arm64)
- Docker: not installed
- jq: already installed (1.7.1 via Homebrew)
- Codebase: PHP app with React frontend, Apache on EC2
2. Installing Docker
Docker Desktop was not present. Installed via Homebrew:
brew install --cask docker
This installed Docker Desktop 4.67.0 for Mac (arm64). After install, launched the daemon:
open -a Docker
First launch took ~45 seconds to initialise. Verified with docker info.
Note: Docker Desktop must be running (the whale icon in the menu bar) before any docker run commands will work. On first install, it asks you to accept the EULA.
3. Running the Scans
Gotcha: Source Code Location
The guide assumes your code is in the repo root. This app's code lives under app/ and its subdirectories:
app/
config/
src/
the_app/
public_html/ <- PHP app root
class/ <- PHP classes, vendor libs
admin/ <- admin UI
client/ <- client-facing pages
react-app/ <- React frontend
Mounting the repo root would scan deploy scripts, infra templates, and docs — not useful. We mounted app/ directly.
Gotcha: Scanner CLI Syntax Change
The guide's original command failed with v6.18.0 of the Semgrep image. The scanner now expects --target-dir and --artifact-dir flags:
# This failed:
docker run --rm --pull always -v "$(pwd):/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run /code
# This worked:
docker run --rm --pull always -v "$(pwd)/app:/code" \
registry.gitlab.com/security-products/semgrep \
/analyzer run --target-dir /code --artifact-dir /code
Gotcha: Gemnasium Depth Limit
The dependency scanner only looks 2 directories deep by default. It warned that the_app/public_html subdirectories were skipped, meaning lockfiles in the React app directory were missed. Fix with DS_MAX_DEPTH:
docker run --rm --pull always \
-e DS_MAX_DEPTH=5 \
-v "$(pwd)/app:/code" \
registry.gitlab.com/security-products/gemnasium \
/analyzer run --target-dir /code --artifact-dir /code
Gotcha: Minified JS Timeouts
The SAST scanner timed out on large minified JavaScript files (jQuery, Highcharts, TinyMCE). These are third-party vendor files — timeouts are expected and harmless. Use SAST_EXCLUDED_PATHS to skip them in future scans.
4. Results Summary
SAST (Source Code Vulnerabilities)
| Severity | Count |
|---|---|
| Critical | 12 |
| High | 16 |
| Medium | 121 |
| Total | 149 |
Dependency Scanning (Known CVEs)
| Severity | Count |
|---|---|
| Critical | 3 |
| High | 10 |
| Medium | 5 |
| Total | 18 |
167 total findings. Sounds alarming. But most are noise.
5. Triaging the Findings
Critical SAST: 12 findings, only 2 real
10 in PHPMailer library — the scanner flagged escapeshellcmd() usage in vendored PHPMailer. These are false positives in context; PHPMailer handles this internally.
2 in file export — a real command injection vulnerability. User-derived values were passed directly to a PHP shell function without sanitisation.
High SAST: 16 findings, all vendor
All 16 were dynamic code evaluation in third-party JavaScript libraries (jQuery, Underscore, TinyMCE). These use dynamic evaluation as part of their normal operation. Low real-world risk.
Medium SAST: 121 findings
| Finding Type | Count | Assessment |
|---|---|---|
| Non-literal regex | 52 | Vendor JS. Low risk |
| Weak hash (MD5/SHA1) | 29 | PHP checksums, not security. Review needed |
| Incorrect regex | 28 | Vendor JS. Low risk |
| Cross-site Scripting | 9 | React innerHTML — admin content, acceptable |
| Dangerous function | 2 | Needs context review |
| Info exposure | 1 | Likely phpinfo() |
XSS: All Acceptable
All 9 XSS findings were React rendering admin-authored HTML from the database (custom messages and footers). The content is controlled by administrators, not end users. Acceptable risk.
Dependency Findings: Build-time Only
All Critical and High dependency findings were in Node.js build-time packages (webpack, minimist, braces). They don't ship to production — they only run during the build process.
6. Fixing the Vulnerabilities
Command Injection Fix
The file export endpoint was passing user-derived values straight into a PHP shell function. Fixed with escapeshellarg() to wrap each value in safe single quotes:
$safeTempSvg = escapeshellarg("temp/$tempName.svg");
$safeOutfile = escapeshellarg($outfile);
$command = "java -jar " . escapeshellarg(CONVERTER_PATH)
. " $typeString -d $safeOutfile $width $safeTempInput";
$typeString and $width were already safe — $typeString is set from a hardcoded whitelist, and $width is cast to (int).
Dependency Fix
Ran npm audit fix in the React app directory:
- Fixed: 2 vulnerabilities (non-breaking updates)
- Remaining: 17 vulnerabilities requiring webpack 4 to 5 upgrade (breaking change, tracked separately)
Housekeeping
Added scan artifacts to .gitignore so reports never get committed:
# Security scan artifacts
gl-*.json
*.sarif
*sbom*.json
.semgrepignore
7. Automating Future Scans
Set up a Claude Code PostToolUse hook that triggers after every git commit. The hook:
- Runs the Semgrep SAST scanner (with vendor exclusions)
- Parses the JSON report
- Feeds Critical/High findings back to Claude Code as context
- Claude Code automatically triages and suggests fixes
Every commit now gets automatic security scanning. No manual intervention needed.
8. Lessons Learned
| Issue | Resolution |
|---|---|
| Docker not installed | brew install --cask docker + open -a Docker |
| Guide's CLI syntax outdated | Use --target-dir and --artifact-dir flags |
| Scanner mounted wrong directory | Mount app/ not repo root |
| Gemnasium missed deep lockfiles | Set DS_MAX_DEPTH=5 |
| Timeouts on minified vendor JS | Expected; use SAST_EXCLUDED_PATHS to skip |
| 149 findings look alarming | Only 2 real Criticals in custom code; rest are vendor |
| React innerHTML XSS flags | Admin-authored DB content, not user input |
| npm audit fix only partial | Remaining vulns need webpack 4 to 5 (separate task) |
| Scan artifacts in working tree | Added to .gitignore |
9. The Bottom Line
167 findings. 2 real vulnerabilities. 1 hour from zero to automated scanning.
The scanners are free, the Docker images are public, and Claude Code turns a wall of JSON into a prioritised, actionable fix list. The hardest part was getting the Docker command syntax right — and this case study saves you that trouble.
Read the companion guide for the step-by-step instructions without the narrative.