Video walkthrough (about 1.5 minutes)
How it works, installing on Mac and Windows, running a scan, reading the report, and scanning every repo in your company.
What it checks
Each scanner looks for a different kind of problem. Together they cover most of the common risks in the OWASP Top 10.
Gitleaks
API keys, passwords, tokens and private keys committed to your code or anywhere in git history.
Semgrep
Insecure code patterns: SQL injection, XSS, command injection, weak crypto, unsafe deserialization.
Trivy
Libraries with known CVEs (npm, pip, Maven, Go, Composer…) and Dockerfile, Terraform, Kubernetes misconfigurations.
OWASP ZAP
Scans your running app from outside: missing security headers, insecure cookies, information leaks. Runs only when you give it a URL.
How it works
Every scanner runs inside Docker, so you don't install any of them. Each writes a JSON result. summarize.py merges them into one report sorted by severity, with a "How to fix" column. The run fails if anything at or above your chosen severity (default high) is found, which lets you block risky code from merging.
When it runs on GitHub
| Trigger | When |
|---|---|
| Manual | Actions → Security Scan → Run workflow (the "few clicks" option) |
| Push | Every push to main / master |
| Pull request | Every PR, so issues are caught before merging |
| Schedule | Every Monday 09:00 IST, which catches newly published CVEs in libraries you already use |
Setup (one time, about 2 minutes)
- Download and unzipUse the download button at the top of this page.
- Copy the files into your project's root folder
# run from the folder where you unzipped cp -r security-testing-toolkit-main/.github security-testing-toolkit-main/security /path/to/your-project/On Windows, copy the
.githubandsecurityfolders with File Explorer. Turn on "Show hidden items" to see.github. - Ignore the report folder
cd /path/to/your-project echo "security-reports/" >> .gitignore chmod +x security/scan.sh
- Commit and push
git add .github security .gitignore git commit -m "Add automated security scanning" git push
The first scan starts automatically after the push.
Run a scan
- Open your repo on GitHub → Actions tab
- Click "Security Scan" in the left sidebar → "Run workflow"
- Optional: paste your staging URLThis also scans the live app. Leave it blank to scan code only.
- Pick "fail on" severity and click the green Run workflow button
- View resultsThe summary table appears on the run page. Scroll down to Artifacts, download security-reports, and open
report.html.
You need Docker Desktop (running) and Python 3. Run these from your project's root folder.
# Scan code, secrets and dependencies
./security/scan.sh# Also scan your app while it's running locally
./security/scan.sh http://localhost:3000# Only fail on critical issues for this run
SECURITY_FAIL_ON=critical ./security/scan.shThe report opens automatically from security-reports/report.html. On Windows, start Docker Desktop first, then run the commands in Git Bash (installed with Git for Windows) or WSL.
Start a GitHub scan without opening the browser, using the GitHub CLI:
# code-only scan gh workflow run security-scan.yml # with a live app scan gh workflow run security-scan.yml -f target_url=https://staging.myapp.com -f fail_on=high # watch it, then download the reports gh run watch gh run download --name security-reports
Scan your whole company GitHub
Check every repository in your organization in one go. You get a company report with the counts for each repo, and each repo links to its full report.
- Install the GitHub CLI and sign in once
# Mac brew install gh # Windows (PowerShell) winget install GitHub.cli gh auth login
- Run the org scanReplace
your-companywith your GitHub organization name. It clones each repo, scans it, then deletes the copy../security/scan-org.sh your-company # only the 20 most recently updated repos ./security/scan-org.sh your-company 20 - Open the company report
org-reports/index.htmllists every repo with its critical, high, medium and low counts. - Keep every repo covered automaticallyAdd
.github/workflows/security-scan.ymland thesecurity/folder to each repo, so every push and pull request is scanned too.
Private repos are included when your GitHub account can access them. Nothing is uploaded anywhere: the scans run on your computer.
Settings (optional)
On GitHub: Repo → Settings → Secrets and variables → Actions → Variables tab → New repository variable.
| Variable | Example | What it does |
|---|---|---|
SECURITY_TARGET_URL | https://staging.myapp.com | Runs the live ZAP scan on every automatic run |
SECURITY_FAIL_ON | high · critical · medium · none | Lowest severity that fails the run (default high) |
Block risky pull requests
Settings → Branches → Add rule for main → turn on "Require status checks to pass" → select security-scan. A PR with high or critical findings then can't be merged.
The same variables work locally as environment variables: SECURITY_TARGET_URL and SECURITY_FAIL_ON.
Reading the results
report.html shows a count per severity, then every finding with the tool, the issue, the file or URL, and how to fix it. You can filter by severity or tool and search.
| Severity | Meaning | What to do |
|---|---|---|
| CRITICAL | Easy to exploit, serious impact | Fix immediately, before anything ships |
| HIGH | Real risk; leaked secrets are always high | Fix before the next release |
| MEDIUM | Needs specific conditions to exploit | Fix this sprint |
| LOW / INFO | Hardening suggestions | Review; fix if cheap |
Files in security-reports/
| File | Contents |
|---|---|
report.html | Combined, filterable report. Start here. |
summary.md | Short summary (also shown on the GitHub run page) |
gitleaks.json · semgrep.json · trivy.json · zap.json | Raw output from each tool |
zap.html | ZAP's own detailed report (only if a URL was scanned) |
Fixing common issues
Secret found (Gitleaks)
Rotate the key first. Once it's in git history, assume it's compromised. Deleting the line isn't enough. Then move it to an environment variable or a secrets manager:
// before const apiKey = "sk_live_abc123..."; // after const apiKey = process.env.API_KEY;
Vulnerable dependency (Trivy)
Upgrade to the version shown in "How to fix", then run the scan again.
npm install lodash@4.17.21 # Node npm audit fix # Node, fix all safe upgrades pip install -U "django>=4.2.16" # Python
SQL injection (Semgrep)
Never build queries by joining strings. Use parameters:
// bad db.query("SELECT * FROM users WHERE id = " + req.params.id); // good db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
Missing security headers (ZAP)
// Express npm install helmet app.use(require("helmet")()); # Django settings.py SECURE_HSTS_SECONDS = 31536000 SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True X_FRAME_OPTIONS = "DENY"
False positive: how to ignore a finding
| Tool | How to ignore |
|---|---|
| Semgrep | Add // nosemgrep (or # nosemgrep) on that line |
| Gitleaks | Add the finding's fingerprint to .gitleaksignore in the repo root |
| Trivy | Add the CVE ID, e.g. CVE-2024-1234, to .trivyignore |
Only ignore after checking it really doesn't apply, and add a comment explaining why.
What the tools can't catch: test these yourself
Automated scanners miss business-logic bugs. These are among the most common real-world vulnerabilities, so add tests for them to your normal test suite:
| Test | Expected result |
|---|---|
| Call a protected API without logging in | 401 Unauthorized |
| Log in as User A, request User B's record (change the ID in the URL or body) | 403 / 404, never B's data |
| Normal user calls an admin endpoint | 403 Forbidden |
Send ' OR 1=1-- or <script>alert(1)</script> in inputs | Rejected or safely escaped |
| Wrong password 20 times in a row | Rate-limited or locked out |
Upload a .php / .exe file disguised as an image | Rejected |
// Example (Jest + supertest)
test("user cannot read another user's order", async () => {
const res = await request(app)
.get("/api/orders/" + orderOfUserB.id)
.set("Authorization", "Bearer " + tokenOfUserA);
expect([403, 404]).toContain(res.status);
});To practice finding these, use PortSwigger Web Security Academy (free labs) and OWASP Juice Shop (a deliberately vulnerable app you run locally).
FAQ
Is the live (ZAP) scan safe?
The baseline scan is passive: it crawls pages and inspects responses without sending attacks. Only scan apps you own or have permission to test, and use staging or local rather than production.
How long does a scan take?
Usually 2–6 minutes on GitHub. The first local run takes longer while Docker downloads the scanner images.
The run failed. Is that broken?
Usually not. A failed run means findings at or above your fail_on level were found. Open the summary to see them. To see results without failing, set SECURITY_FAIL_ON=none.
Does it work with GitLab / Bitbucket?
The local script security/scan.sh works anywhere Docker runs, so you can call it from any CI. The ready-made pipeline file is for GitHub Actions.
My app on localhost isn't reachable by ZAP
The script automatically rewrites localhost to host.docker.internal. Make sure your app listens on 0.0.0.0 rather than only 127.0.0.1, and that Docker Desktop is running.
Does it cost anything?
No. All four tools are free and open source. GitHub Actions is free for public repos and includes free minutes for private repos.