Skip to content
Visibility internal Owner _ Approver _ Created _ Updated _

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

scripts/preflight.sh all

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):

ScannerInstall 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

scripts/scan-static.sh --repo repo --out out

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:

artifacts({
  type: "application/vnd.markdown-file",
  content: "Forge/Output/Security_Scan_YYYY-MM-DD_HH-MM/scope.txt"
})

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:

# Scope file for security scan
# List one hostname per line (e.g. staging.example.com)
# Lines starting with # are comments
# Add the hostname only, not the full URL:
#   staging.example.com      ← correct
#   https://staging.example.com  ← wrong
# Multiple hosts:
#   staging.example.com
#   preview.example.com

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:

scripts/scan-live.sh --target https://staging.example.com --out Forge/Output/Security_Scan_YYYY-MM-DD_HH-MM

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:

---
title: "Security Scan YYYY-MM-DD"
visibility: internal
status: published
owner: "erik@uvilo.com"
approver: ""
lastUpdated: YYYY-MM-DD
---

# Security review -- <repo> -- <date>

## Summary

Two or three sentences. What's the actual risk posture, what needs
fixing first, what coverage gaps exist.

## Coverage

Which scanners ran, which didn't, what wasn't examined.

## Findings

For each, in severity order:

### [SEV] Short title

- **Where**: file:line, or endpoint
- **Category**: OWASP A0X:2025
- **What**: what an attacker could do, in plain terms
- **Why it's real**: the reachability argument -- how untrusted input gets here
- **Fix**: concrete change, with a code snippet where useful

## Dismissed

Findings the scanners raised that aren't real, one line each on why.
This section matters -- it stops the same false positives being
re-litigated next run.

## Manual review notes

Access control, business logic and failure-mode observations.

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:

spawnAgent({
  botGroup: "forge",
  botHandle: "page-manager",
  message: "Register the existing page at Forge/Output/Security_Scan_YYYY-MM-DD_HH-MM/report.md in the sidebar under Forge > Output. The file already exists with full content and frontmatter — do NOT modify or overwrite it. Add a collapsible sidebar group named 'Security Scan YYYY-MM-DD' containing a child item labeled 'Report' linking to the report page."
})

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.