shipwright-security · diff
git:20260728.d373ac4 to git:20260805.a9d6010
1 added, 3 removed. Audit A to A.
---
name: shipwright-security
description: Security scanning with automated remediation. Supports two backends — OSS (Semgrep + Trivy + Gitleaks, local) or Aikido (cloud SaaS). Findings flow back to the coding agent for fixes. Use after /shipwright-build or standalone. Trigger: 'security scan', 'aikido', 'semgrep', 'vulnerabilities'.
license: MIT
compatibility: Requires uv (Python 3.11+). OSS backend needs semgrep/trivy/gitleaks on PATH. Aikido backend needs API credentials.
---
# Shipwright Security Skill
Security scanning with automated remediation. Pluggable scanner backend:
- **OSS** (default): Semgrep (SAST) + Trivy (SCA) + Gitleaks (Secrets) — local, free
- **Aikido**: Cloud SaaS with SAST, SCA, secrets, IaC scanning
---
## Target Operating Model
Security runs **out-of-band** — it is not a pipeline phase. `/shipwright-run` does NOT invoke security automatically (decoupled in iterate `sec-report-and-orchestrator-decouple`, 2026-04).
Two activation paths:
- **Manual / local:** invoke `/shipwright-security` ad hoc, typically after `/shipwright-test`.
- **CI / GitHub Actions:** the active scanner chain lives at [`.github/workflows/security.yml`](../../../../.github/workflows/security.yml). It ships dormant — only `workflow_dispatch` is enabled out of the box. The `pull_request` and weekly `schedule` triggers are commented out and activated deliberately at Phase B / Go-Live. SARIF uploads, PR comments, fork-PR guards, and the critical-findings gate are fully wired.
Pipeline state machine, hooks, and config files do not auto-insert a security phase. `runConditions.securityEnabled` exists for diagnostic purposes only and gates nothing.
---
## CRITICAL: First Actions
**Governing rules:** Read and follow `shared/constitution.md` (ALWAYS / ASK FIRST / NEVER boundaries).
### A. Print Intro Banner
```
================================================================================
SHIPWRIGHT-SECURITY: Security Scanner
================================================================================
Scans projects for vulnerabilities with automated remediation.
Backends: OSS (Semgrep + Trivy + Gitleaks) or Aikido (cloud SaaS).
Usage: /shipwright-security
or: /shipwright-security issues --repo owner/repo (Aikido only)
or: /shipwright-security summary (Aikido only)
or: /shipwright-security report --repo owner/repo (Aikido only)
(Security is out-of-band — /shipwright-run does NOT invoke it; run manually or via CI)
Modes:
Pipeline mode: Inside Shipwright project → full remediation loop
Standalone mode: Any project → scan + report
================================================================================
```
### B. Detect Mode
Check if `shipwright_project_config.json` exists in the project root:
- **Exists** → Pipeline mode (full remediation loop with security-fixer)
- **Does not exist** → Standalone mode (scan + report only)
If Pipeline mode, read profile:
```json
{
"profile": "supabase-nextjs",
...
}
```
Load profile from `{plugin_root}/../../shared/profiles/{profile}.json`.
### C. Select Scanner Backend
**Resolution order:**
1. `SHIPWRIGHT_SCANNER_BACKEND` env var (`oss` or `aikido`)
2. Profile `testing.security.provider` field
3. Auto-detect:
- `AIKIDO_CLIENT_ID` set → Aikido backend
- `semgrep` / `trivy` / `gitleaks` on PATH → OSS backend
- Neither → show setup instructions and stop
Print detected backend:
```
Backend: OSS (Semgrep + Trivy + Gitleaks)
Available: SAST ✓ SCA ✓ Secrets ✓
```
Or for Aikido:
```
Backend: Aikido (Cloud SaaS)
Available: SAST ✓ SCA ✓ Secrets ✓ IaC ✓
```
See `references/oss-scanners.md` for OSS tool installation.
See `references/setup-guide.md` for Aikido setup.
### D. Check Prerequisites
Run: `uv run "{plugin_root}/scripts/checks/validate_security.py"`
If prerequisites missing → print setup instructions and stop.
---
## Step 0: Phase Session Context Recovery
If the orchestrator handed you a `phaseTaskId` — i.e. `/shipwright-run` dispatched
you as a phase-runner subagent — you are part of an active pipeline. Run this as your
very first action:
```bash
uv run "${SHIPWRIGHT_PLUGIN_ROOT}/../../shared/scripts/tools/get_phase_context.py" \
--phase-task-id <phaseTaskId-from-context>
```
The tool prints structured JSON with `runId`, `phase`, `splitId`, `prerequisites`,
`runConditions`, and a `skill_artifacts_to_read` list. Read those artifacts
before proceeding so this phase session has full context for what came before.
If NO `phaseTaskId` was handed to you, this is a standalone invocation —
continue with Step 1 below as normal.
---
## Step 1: Run Security Scan
**For OSS backend:**
The OSS backend runs available tools via subprocess and normalizes the output.
Use the scanner_backend API:
```python
# In the plugin's Python scripts:
from scanner_backend import get_backend
backend = get_backend()
findings = backend.scan(target_dir)
```
Each scanner gets a per-tool exclusion list:
- **Semgrep** — empty plugin list. Semgrep ships its own `.semgrepignore`
(covers `node_modules`, `build`, `dist`, `vendor`, `.venv`, `.tox`,
`.npm`, `.yarn`, …) and respects the project `.gitignore` for untracked
files. The project gitignore is the source of truth.
- **Trivy** and **Gitleaks** — same conservative cross-language build/dep
list (Python `.venv` / `__pycache__` / `.tox` / `.mypy_cache` /
`.ruff_cache`, JS `node_modules` / `.next`, polyglot `target` / `bin` /
`obj` / `vendor` / `.gradle` / `.terraform` / `.direnv`, generic
`dist` / `build` / `.git` / `.cache`, coverage `coverage` / `htmlcov`).
Both tools ignore `.gitignore` natively, so the plugin keeps a minimum
list to prevent Trivy crawling `node_modules` and Gitleaks blowing up
on third-party history.
`.shipwright/` is **no longer** in any list — projects decide via
`.gitignore` (Semgrep) or `SHIPWRIGHT_SCAN_EXCLUDES` (Trivy/Gitleaks).
See `references/oss-scanners.md` for the full per-scanner truth table,
the migration notice, and known edge cases (symlinks, nested gitignore,
tracked-files-in-gitignored-paths).
- Semgrep: `semgrep scan --json --config auto {target}` (env extras add `--exclude` flags)
- Trivy: `trivy fs --format json --scanners vuln --skip-dirs <each-default> {target}`
- Gitleaks: `gitleaks detect --report-format json -s {target} --report-path <temp-json-report> --config <temp-toml-with-allowlist>` — report goes to a temp FILE the plugin reads back (gitleaks has no stdout-report mode; `--report-path -` writes a literal file named `-`, not stdout)
**Accepted findings are answered once per repository.** With a `.gitleaks.toml` at
the scanned root the generated config **extends** it (`[extend] path`) rather than
replacing it, so the local scan honours the same accepted findings as the host.
Never emit both `extend.useDefault` and `extend.path` — gitleaks aborts. A project
config bringing no rules marks `secrets` `degraded`, not `covered`.
**Coverage manifest — name what was NOT checked.** A crashing tool already fails
the run; one that was never installed used to be invisible, so a machine with one
scanner read clean for every class. Every scan records a row per weakness class
(`covered`, `degraded`, `not_requested`, `not_available`) into `findings.json` and
the sidecar. Report the unchecked classes with the findings — nothing examined
them, so they are not clean.
**For Aikido backend:**
Run the aikido_client script:
```bash
uv run --project {plugin_root} {plugin_root}/scripts/lib/aikido_client.py issues --repo {repo} --severity critical,high,medium
```
Parse the JSON response. If `success: false`, show the error and follow alternatives.
**Both backends** return findings in the same normalized schema.
Present findings as a table:
| # | Severity | Type | Rule | File | Line |
|---|----------|------|------|------|------|
| 1 | high | sast | hardcoded-credentials | scripts/api.py | 42 |
| 2 | critical | sca | CVE-2024-1234 | package.json | — |
---
## Step 2: Classify Findings (Pipeline Mode Only)
Each finding is automatically classified by `aikido_client.py`:
| Class | Examples | Action |
|-------|----------|--------|
| `auto-fixable` | Dependency update, known CVE with patch | Agent fixes directly |
| `agent-fixable` | Hardcoded credentials, XSS, missing sanitization | `security-fixer` subagent |
| `needs-review` | Architecture issues, business logic flaws | User interview |
| `informational` | Low-severity, best practices | Log only |
Show the classification summary — `Findings: 12 total` plus the per-class counts
(auto-fixable → fixed directly, agent-fixable → subagent, needs-review → asked,
informational → logged).
---
## Step 2.5: Scope Gate — state the counts, ask how far to go (MANDATORY)
**Never start fixing without asking how far to go** — deciding silently that the
low-severity findings do not matter is the tool making the user's call. Runs in
BOTH modes, from a triage card or a direct invocation alike.
Before Step 3, print the per-severity counts plus the unchecked classes and ask via
**AskUserQuestion**, offering only tiers that actually contain findings, each with
its real count. Findings outside the chosen scope become `deferred`. Prompt shape,
tier rules and the non-interactive default: `references/remediation-loop.md`.
---
## Step 3: Auto-Fix (Pipeline Mode Only)
For each `auto-fixable` finding:
1. Identify the fix (e.g., update dependency version in package.json)
2. Apply the fix
3. Re-run relevant tests
4. If tests pass → mark finding as `fixed`
5. If tests fail after 3 attempts → escalate to user
**Max 3 retries per finding.**
---
## Step 4: Agent-Fix via security-fixer (Pipeline Mode Only)
For each `agent-fixable` finding, invoke the `security-fixer` subagent:
```
Agent: security-fixer
Input: {"severity": "high", "type": "sast", "cwe": "CWE-798",
"rule": "python.lang.security.hardcoded-credentials",
"file": "scripts/api.py", "line": 42,
"description": "Hardcoded API key",
"remediation_hint": "Move to environment variable"}
```
Process the subagent's response:
- If `fix_description` is not null → apply fix, re-run tests
- If `escalation_reason` → move to `needs-review` category
- **Max 3 retries per finding.**
- **Suppression syntax (`# nosemgrep:`):** read `references/suppression-syntax.md`
- first — the suppression must sit on the matched line or *immediately* above it,
- and any intervening comment silently breaks the attribution.
+ **Suppression syntax (`# nosemgrep:`):** read `references/suppression-syntax.md` first — it must sit on the matched line or *immediately* above it, an intervening comment silently breaks attribution, and the new site must be recorded in the ratcheted `shipwright_inline_suppressions.json` in the same commit or CI blocks.
---
## Step 5: User Interview (Pipeline Mode Only)
For each `needs-review` finding, present to user via AskUserQuestion:
**Question:** "Security finding: {severity} — {rule} in {file}:{line}"
**Options:**
- **Fix** — Agent attempts to fix this finding
- **Decline** — Skip this finding (log reason)
- **Defer** — Add TODO comment, fix later
For accepted findings → run security-fixer subagent → re-run tests.
---
## Step 6: Generate Report
**For OSS backend (standalone or pipeline):**
Run the wrapper. It handles scan + redaction + report + history archiving + best-effort `.gitignore`:
```bash
uv run "{plugin_root}/scripts/tools/run_scan_and_report.py" --project-root {project_root} --repo {repo}
```
Output:
- `{project_root}/.shipwright/securityreports/latest.md` — human-readable Markdown report
- `{project_root}/.shipwright/securityreports/latest.json` — machine-readable sidecar (`schema_version: 1`, `scan_id`, full normalized findings)
- `{project_root}/.shipwright/securityreports/history/scan-YYYYMMDD-HHMMSS-{6hex}.{md,json}` — archived (last 20 pairs retained)
- `{project_root}/.gitignore` — `/.shipwright/` appended if file exists and entry missing (legacy `/securityreports/` recognised as already-present so we don't double-write during migration)
The wrapper redacts secret evidence by default (Gitleaks `match`/`secret`/`commit`/`author`/`email` fields; high-entropy strings in `description` / `remediation_hint`). Use `--full-evidence` to retain raw values for explicit local debugging — refused when `CI` env is set.
After the wrapper exits, read `{project_root}/.shipwright/securityreports/latest.json` for the structured scan summary (total_findings, by_severity, by_source, risk_level, **coverage**). The wrapper also emits ONE `security-scan:{repo}` triage card carrying the severity split, the unchecked classes, and the scope question.
**Compare against the previous scan (optional):**
```bash
uv run "{plugin_root}/scripts/tools/compare_scans.py" --project-root {project_root}
```
Reports fixed / new / still-open **only for classes both runs covered**; anything
else is not-comparable, because a finding that vanished when its tool was
uninstalled was never fixed. Exit 2 = no previous scan.
**Migration note:** projects from before this iterate may have a `securityreports/` directory at project root. The wrapper detects it and emits a one-time stderr notice on the first run pointing at the new location; the old folder is gitignored, stale, and safe to delete (or `git mv securityreports .shipwright/` if you want to preserve archived scans).
**For Aikido backend (path preserved, untouched by v0.3 restructuring):**
```bash
uv run --project {plugin_root} {plugin_root}/scripts/lib/aikido_client.py report --repo {repo}
```
---
## Step 7: Persist Results (Pipeline Mode Only)
Write results to `shipwright_security_config.json` in the project root:
```json
{
"scan_date": "2026-03-26T10:00:00Z", "repo": "owner/name", "scanner": "aikido",
"total_findings": 12,
"by_severity": {"critical": 1, "high": 3, "medium": 5, "low": 3},
"coverage": [{"class": "sast", "tool": "semgrep", "status": "covered", "detail": null}],
"remediation": {"fixed": 5, "declined": 1, "deferred": 2, "open": 4},
"findings": [], "session_id": "..."
}
```
Findings outside the Step 2.5 scope are `deferred`, not `open` and not dropped.
This config is consumed by `/shipwright-compliance` for traceability.
---
## Step 7.5: Compliance Snapshot Refresh (Pipeline Mode Only)
After Step 7 persists `shipwright_security_config.json`, refresh the
compliance MDs so the next snapshot audit sees the post-security state
AND the resulting commit qualifies as a baseline for
`audit_staleness.find_snapshot_commit` (per
iterate-2026-05-23-security-adopt-compliance-snapshots).
Run the helper:
```bash
scan_id=$(jq -r .scan_id .shipwright/securityreports/latest.json 2>/dev/null || echo "unknown")
uv run "{plugin_root}/scripts/tools/finalize_security_compliance.py" \
--project-root "{project_root}" \
--scan-id "${scan_id}"
```
Output is structured JSON: `{"committed": true, "reason", "commit_sha",
"regenerated"}`, or `committed: false` with the reason — compliance unchanged,
standalone mode, or a CI / non-interactive environment.
**Skip Step 7.5 entirely** (the helper does this internally — listed here for
operator awareness) when `shipwright_project_config.json` is absent (standalone —
Step 8 hands off to `/shipwright-iterate`), or when `CI` /
`SHIPWRIGHT_NON_INTERACTIVE` is truthy.
When `committed=true`, the new commit's body carries
`Run-ID: security-<scan_id>` so the audit recognizes it as the new
snapshot baseline. **Idempotent**: a re-invocation with no new
compliance drift produces no commit.
---
## Step 8: Iterate Handoff (OSS standalone mode only)
After Step 6 completes for the OSS backend in standalone mode, offer the user a one-question handoff into `/shipwright-iterate` so they can work through fixes.
**Skip Step 8 entirely if any of:**
- `total_findings == 0` in `.shipwright/securityreports/latest.json`
- `os.environ.get("CI")` is set (any truthy value)
- `os.environ.get("SHIPWRIGHT_NON_INTERACTIVE")` is set
- `sys.stdin.isatty()` returns False
- Pipeline mode is active (`shipwright_project_config.json` exists in project root) — the remediation loop in Steps 2-5 already handled it
**Pre-flight check:** verify `shipwright_run_config.json` exists in `project_root`.
- If missing → print: `"To fix these findings, open /shipwright-iterate in a Shipwright-managed project and point it at .shipwright/securityreports/latest.md"`, then exit 0.
- If present → proceed.
**Ask the user via AskUserQuestion:**
> Scan complete: {total_findings} findings ({by_severity summary}).
> Start an iterate to work through fixes?
>
> - **YES** — start `/shipwright-iterate` (the report path is passed as context)
> - **NO** — done, just the report
**On YES:** invoke the `/shipwright-iterate` skill with this generic brief (no scanner prose interpolated, no prompt-injection surface):
> Review and fix security findings from the most recent scan.
> Report: `.shipwright/securityreports/latest.md` (machine-readable sidecar: `.shipwright/securityreports/latest.json`).
> Work through findings with the user — pick what to fix, what to suppress, what to defer. Favor small iterate scopes (one rule-family or one fix category per iterate) to keep review tight.
**Failure handling:** if the `/shipwright-iterate` invocation raises or exits non-zero, print the same brief verbatim to the terminal, log the error to stderr, and exit 0. The report (`.shipwright/securityreports/latest.*`) remains written regardless of handoff success.
---
## Standalone Mode Commands (Aikido)
Outside a Shipwright pipeline these work directly. All are
`uv run --project {plugin_root} {plugin_root}/scripts/lib/aikido_client.py <cmd>`:
| `<cmd>` | Options | Present as |
|---|---|---|
| `issues` | `[--repo owner/repo] [--severity critical,high] [--status open] [--type sast]` | Markdown table |
| `repos` | — | bulleted list |
| `summary` | `[--repo owner/repo]` | ASCII dashboard with severity bars |
| `report` | `--repo owner/repo [--output path.md]` | Markdown report on disk |
---
## Troubleshooting
| Error | Cause | Fix |
|-------|-------|-----|
| `credentials not configured` | Missing .env | Create API credentials at Aikido |
| `401 Unauthorized` | Invalid credentials | Regenerate API credentials |
| `403 Forbidden` | Insufficient API scope | Check API permissions |
| `429 Too Many Requests` | Rate limit | Wait and retry |
| `No repos found` | GitHub not connected | Connect GitHub in Aikido settings |
| `No issues found` | Clean scan or filters too narrow | Try without filters |
---
## Backend Details
- **Aikido (Cloud SaaS)** — OAuth 2.0 client-credentials against
`app.aikido.dev/api`. Endpoints, auth flow and response schema:
`references/aikido-api.md`.
- **OSS (local CLI)** — Semgrep (SAST), Trivy (SCA), Gitleaks (secrets), each
with auto-updating rules. Install, exclusions and edge cases:
`references/oss-scanners.md`.