Security scan
Runs the scanners, then does the part the scanners can’t: works out which findings actually matter in this codebase and what to do about them.
Most scanner output is noise. A tool that flags 400 issues where 6 are real is worse than useless, because the 6 get buried. The value here is in the triage, so don’t just reformat the SARIF and hand it back.
Workflow
1. Check what’s installed
Report which scanners are missing before scanning, not after. The preflight also prints the installed version of each tool. If any version looks significantly outdated, warn the user — but do not update tools as part of this skill. Updating scanners changes rule sets and output formats, which makes triage results harder to compare across runs. If there are missing items, report to the user and offer to auto install.
2. Install missing scanners
If the user accepted your offer to auto-install, install the missing tools. Otherwise this skill is done. Don’t install anything without explicit user approval — these are system-level tools.
Installation commands (Debian/Ubuntu, which is the Forge container’s base):
| Scanner | Install command |
|---|---|
| Semgrep (SAST) | pip install semgrep |
| Trivy (deps/IaC/SBOM) | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin |
| Gitleaks (secrets) | curl -sSL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks-linux-amd64 -o /usr/local/bin/gitleaks && chmod +x /usr/local/bin/gitleaks |
| Checkov (IaC) | pip install checkov |
| Nuclei (DAST) | Already on the Forgentic image. Otherwise: download the nuclei_*_linux_amd64.zip asset from projectdiscovery/nuclei releases, unzip to /usr/local/bin, then nuclei -update-templates. On macOS: brew install nuclei. |
| ZAP (DAST) | Already on the Forgentic image (zap.sh, zap-baseline.py, zap-full-scan.py, Java 17). No Docker-in-Docker. On macOS/local without native ZAP, scan-live.sh falls back to docker run ghcr.io/zaproxy/zaproxy:stable. If neither native ZAP nor Docker is available, ZAP is skipped — note it as a coverage gap in the report. |
On macOS (for local runs): brew install semgrep trivy gitleaks checkov nuclei. ZAP uses Docker when not installed natively.
After installing, re-run scripts/preflight.sh all to confirm.
3. Ask the user what to scan
a. Ask the user which checked out repo to scan or if they want to check out another repo
b. Ask the user for the url of a staging server to scan. Warn them that the scan may be destructive.
4. Static scans
Create directory in Forge/Output with name Security_Scan_YYYY-MM-DD_HH-MM
(replace placeholders with actual date/time).
Run the bash command, replacing repo and out with the actual values
Writes SARIF from Semgrep, Trivy, Gitleaks and Checkov. It doesn’t fail on findings; findings are input to triage.
5. Live scans (only when there’s a running target)
Create the scope file at Forge/Output/Security_Scan_YYYY-MM-DD_HH-MM/scope.txt
if it does not already exist. Then display it to the user using a markdown file
artifact so they can edit it directly:
The scope file lists hosts the user is authorised to scan — one hostname per
line. The script refuses anything not listed. Lines starting with # are
comments. Pre-populate the file with a comment header explaining the format:
Ask the user to add the target hostname to this file. The script reads the
hostname from the --target URL and checks it against the scope file, so the
hostname must match exactly (no protocol, no path).
Do not add the host to the scope file yourself — the user must do it. The friction is the feature: scanning a host you don’t control is unlawful, and the scope file makes that a deliberate act rather than a typo.
Then run:
Before running this, confirm two things with the user:
- the target is theirs
- it is not production
Default is a passive baseline. --active sends real payloads and can corrupt
data, so it’s only for a disposable environment with seeded data. Don’t pass it
without the user explicitly asking for an active scan.
6. Triage
Read references/triage.md before interpreting results. It covers the
false-positive patterns each tool has and how to map findings onto the OWASP
Top 10:2025 categories.
The core question for every finding is reachability: is there a path from untrusted input to this code, with no authorisation check in between? Open the files. A hardcoded credential in a test fixture and one in a production config loader are the same SARIF rule and completely different problems.
7. Look for what the scanners missed
Static tools are weakest exactly where real breaches happen. After triaging the automated findings, read the code for these directly:
- Broken access control — for each endpoint, what’s the authorisation check, what object does it scope to, and can an ID in the request reference another tenant’s row? This is A01 and the scanners will not find it.
- Business logic — can a refund be claimed twice, a discount stacked, a paid feature reached by skipping a step? No tool knows the rules of this app.
- Failure modes — what happens on malformed input, a timeout, a dependency outage? A02:2025 added a category for failing open.
Flag these as “manual review” findings so it’s clear they didn’t come from a tool.
8. Write the report
Save to report.md in the folder you created. The file must have frontmatter
before the body — without it the Page Manager treats it as an unfinished page
and may overwrite it. Use this structure:
Severity should reflect exploitability in this application, not the scanner’s default rating. Say so when you’re overriding it.
Suggested fixes
Write the fix as a diff or a concrete snippet whenever you can. Offer to apply them, but apply one finding at a time and let the user review each — a batch of unreviewed security “fixes” is its own risk.
What this skill does not do is write proof-of-concept exploits. To confirm a finding, read the code path and explain the reachability, or re-run the specific Nuclei template that flagged it — that’s what verifies a fix without leaving a working attack script in the repo.
9. Register the report page
Once the report is written and has frontmatter, spawn the Page Manager to register it in the sidebar:
Tell the Page Manager explicitly that the file already exists and must not be overwritten. Do not say “create” — say “register” or “add to sidebar”.
CI
For running this in a pipeline, see references/ci.md. The short version: run
the static scans on every pull request, upload the SARIF to the code-scanning
tab, and keep the live scan on a schedule against staging rather than on PRs.