Security scanning & posture¶
Work Wingman is a local-first desktop app: the .NET API listens on loopback only
(http://127.0.0.1:5211), secrets live in a local KeePass vault, and no user data leaves the
machine except through connections the user explicitly configures (BYO credentials). The
scanning stack below defends the codebase itself — its dependencies, its history, and the one
network surface the app has (the loopback API).
What runs, where¶
| Layer | Tool | Where | Gate |
|---|---|---|---|
| SAST (code) | Semgrep — p/csharp p/typescript p/javascript p/security-audit p/secrets |
CI sast job + local Docker |
Hard fail on any finding |
| Secrets (history) | Gitleaks over the full git history, .gitleaks.toml allowlist |
CI secrets job + local Docker |
Hard fail on any leak |
| Dependencies (CVE) | dotnet list package --vulnerable (+ allowlist script), npm audit --audit-level=high |
CI security job |
Hard fail on High/Critical |
| Dependency updates | Dependabot — nuget, /frontend npm, /electron npm, github-actions |
GitHub, weekly | PRs, not a gate |
| DAST (runtime) | OWASP ZAP baseline vs the real API booted on loopback | CI dast job + local Docker |
Hard fail on FAILs; WARNs reported |
| Interactive triage | Semgrep MCP server (.mcp.json) |
Claude Code sessions | Advisory (gating stays in CI) |
| Review gates | Codex + Gemini (+ local council) review every commit, incl. an explicit OWASP ask | Per-commit, pre-push | Convergence required |
Running the scans locally¶
All scanners run through Docker so results match CI exactly (Semgrep and ZAP have no native
Windows CLI). From the repo root, Git Bash needs MSYS_NO_PATHCONV=1 so /repo-style
container paths survive:
# SAST — identical invocation to the CI `sast` job
MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd):/src" -w /src semgrep/semgrep \
semgrep scan --metrics=off --error \
--config p/csharp --config p/typescript --config p/javascript \
--config p/security-audit --config p/secrets \
--exclude node_modules --exclude dist src frontend electron tools
# Secrets — full history, same config as CI
MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest \
git --no-banner -c /repo/.gitleaks.toml /repo
# DAST — boot the API first (dotnet run --project src/WorkWingman.Api --no-launch-profile),
# then baseline-scan it (host.docker.internal reaches the host's loopback from the container)
MSYS_NO_PATHCONV=1 docker run --rm zaproxy/zap-stable \
zap-baseline.py -t http://host.docker.internal:5211 -I
Suppression policy¶
A finding is either fixed or suppressed with a reviewed justification — never silently ignored:
- Semgrep: inline
// nosemgrep: <rule-id> -- <reason>at the flagged line. The reason is part of the suppression; a barenosemgrepshould be rejected in review. - Gitleaks: an entry in
.gitleaks.tomlwith a comment explaining why it is a false positive. Never allowlist a real credential — rotate it instead. - CVEs:
.github/dependency-allowlist.txt(advisory id + reason), consumed by.github/scripts/parse-vulnerable-packages.sh. - ZAP: WARNs are tolerated by the gate (
-I) but should be fixed when cheap — e.g. the API now sendsCache-Control: no-storeon every response because its data is personal and fully dynamic.
Interactive scanning in Claude Code (Semgrep MCP)¶
.mcp.json registers the Semgrep MCP server — the CURRENT form, semgrep mcp, built into the
Semgrep binary (the old uvx semgrep-mcp package and ghcr.io/semgrep/mcp image are
deprecated; the image now only serves a deprecation notice tool). It runs inside the
official semgrep/semgrep container because the Semgrep CLI has no native Windows build. The
repo is mounted read-only at /workspace so the path-based tools (semgrep_scan,
semgrep_scan_with_custom_rule, get_abstract_syntax_tree, …) can see the code. The mount
path in .mcp.json is absolute — clones at a different path must adjust it. The MCP is for
interactive triage — deterministic gating stays in CI.
As of 2026-07 there is no official OWASP-Foundation scanner MCP (the "OWASP MCP Top 10" is a framework about securing MCP servers themselves, not a scanning tool); Semgrep's OWASP-mapped rulesets are the chosen equivalent. Re-check occasionally.
Deliberately not used (and why)¶
- CodeQL — the CodeQL CLI/action license covers public repos and GitHub Advanced Security customers; this repo is private without GHAS, so running it would violate the license. Semgrep is the SAST of record. Revisit if the repo goes public or gets GHAS.
- SARIF upload / code-scanning alerts tab — same GHAS gating on private repos. The hard CI gates make the signal impossible to miss without the dashboard.
- gitleaks-action — the wrapper wants a license key for org accounts; the MIT-licensed CLI in Docker does the same job with no key.
- SonarCloud — free tier is public-repos-only.
App-level guardrails (context for reviewers)¶
These are properties reviews and scans must never regress:
- The automation engines locate but never click Submit; applying is always the user's act.
- The app never types stored passwords into pages ("offer, don't type" — see
docs/technical/ats-accounts.md); account walls pause the run for the user. - Sensitive endpoints require the per-launch loopback token (
RequireLocalToken); tokens seen in docs or history are dead by design (regenerated every launch, loopback-only). - BYO credentials only: the app never creates accounts or mints keys for the user; user keys live in the local KeePass vault.
- Apply-URL extraction validates hosts against the detected ATS family (SSRF guard in
AtsDetector).