security · diff
git:20260303.e873d21 to git:20260818.efc7b35
38 added, 16 removed. Audit A to A.
---
name: security
description: >-
Security scan: dependency audits, SAST analysis, and secret detection.
Detects project type, runs available security tools, classifies findings
by severity, and creates a structured GitHub issue.
- allowed-tools: [Bash, Read, Grep, Glob, Task]
+ allowed-tools: [Bash, Read, Grep, Glob, Task, Write]
---
# DLC: Security Scan
Run security checks against the current project and create a GitHub issue with findings.
Before running, **read [../dlc/references/ISSUE-TEMPLATE.md](../dlc/references/ISSUE-TEMPLATE.md) now** for the issue format, and **read [../dlc/references/REPORT-FORMAT.md](../dlc/references/REPORT-FORMAT.md) now** for the findings data structure.
## Step 1: Detect Project Type
Scan the repository root for project indicators:
| Indicator | Project Type | Primary Tool |
|-----------|-------------|--------------|
| `package.json` / `package-lock.json` / `bun.lockb` | Node.js | `npm audit` / `bun audit` |
| `requirements.txt` / `pyproject.toml` / `Pipfile` | Python | `pip-audit` |
| `Cargo.toml` | Rust | `cargo audit` |
| `go.mod` | Go | `govulncheck` |
| `pom.xml` / `build.gradle` | Java/Kotlin | `dependency-check` |
| `Gemfile` | Ruby | `bundler-audit` |
If multiple indicators exist, treat as a monorepo and scan each.
## Step 2: Run Security Tools
For each detected project type, run tools in this priority order. Use the first available tool; skip unavailable ones.
### Dependency Audit
Select the tool based on availability (`command -v`), not exit codes — audit tools exit non-zero when vulnerabilities are found, which is a valid result to capture.
```bash
# Node.js — select by availability
if command -v npm >/dev/null 2>&1; then
npm audit --json 2>/dev/null
elif command -v bun >/dev/null 2>&1; then
bun audit 2>/dev/null
fi
# Python
command -v pip-audit >/dev/null 2>&1 && pip-audit --format=json 2>/dev/null
# Rust
command -v cargo-audit >/dev/null 2>&1 && cargo audit --json 2>/dev/null
# Go
command -v govulncheck >/dev/null 2>&1 && govulncheck ./... 2>/dev/null
```
### Dependency Staleness
Check for packages significantly behind the latest stable release — these accumulate security patches without formal CVEs.
Select tools based on availability (`command -v`), not exit codes — staleness tools exit non-zero when outdated packages are found, which is a valid result to capture.
```bash
# Node.js — prefer npm, fall back to Bun
if command -v npm >/dev/null 2>&1; then
npm outdated --json 2>/dev/null
elif command -v bun >/dev/null 2>&1; then
bun outdated 2>/dev/null
fi
# Python
command -v pip >/dev/null 2>&1 && pip list --outdated --format=json 2>/dev/null
# Rust
command -v cargo-outdated >/dev/null 2>&1 && cargo outdated --json 2>/dev/null
# Go
command -v go >/dev/null 2>&1 && go list -u -m -json all 2>/dev/null
```
### Static Analysis (SAST)
Try in order — use the first available:
1. **Semgrep** (preferred): `semgrep scan --config=auto --json .`
2. **Trivy** (filesystem mode): `trivy fs --format json --scanners vuln,secret .`
3. **Claude static analysis** (fallback): Manually review files matching security-sensitive patterns
### Secret Detection
```bash
# Try gitleaks first
gitleaks detect --source . --no-git --report-format json 2>/dev/null
- # Fallback: grep for common patterns (POSIX-compatible)
- grep -rnE "AKIA|sk-|ghp_|password[[:space:]]*=|secret[[:space:]]*=" \
+ # Fallback: grep for common patterns. Not POSIX grep — -r, -o, and the
+ # --include long option are all GNU/BSD extensions this command relies on.
+ # -o prints only the matched token itself — never the surrounding line — so
+ # the actual secret value never reaches output. This scan's findings end up
+ # in the issue's Raw Output section, which gets posted verbatim to a public
+ # GitHub issue.
+ #
+ # A prior version of this fix tried to strip the line content in a separate
+ # sed pass keyed on "first two colon-delimited fields = path:line". That
+ # breaks on any filename containing a colon (valid on Linux/macOS): grep's
+ # own "path:line:" prefix becomes ambiguous, the substitution silently fails
+ # to match, and the raw secret-containing line passes through unredacted —
+ # verified empirically. -o has no such ambiguity: none of these five patterns
+ # captures a value, only a short fixed prefix/keyword (AKIA, sk-, ghp_,
+ # "password =", "secret ="), so there's nothing sensitive left to leak either
+ # way the line is parsed downstream.
+ #
+ # Do NOT widen any pattern to capture the secret body itself (e.g.
+ # AKIA[A-Z0-9]{16} instead of bare AKIA) — -o's safety depends entirely on
+ # every pattern staying prefix/keyword-only. A body-matching pattern would
+ # print the actual secret value straight into the public issue.
+ #
+ # --include flags come before the pattern and the pattern comes before `.` —
+ # verified empirically that with option permutation disabled (POSIXLY_CORRECT=1,
+ # and some non-GNU grep builds by default), --include after the pattern gets
+ # consumed as a positional filename argument instead of a flag, producing
+ # "No such file or directory" for every --include and a nonzero exit code.
+ # *.env* (not *.env) so .env.local / .env.production match too — verified
+ # a leading * in --include isn't shell-glob-style hidden-file-exclusive,
+ # it already matches dotfiles like .env.local without a separate .env*
+ # pattern.
+ grep -rnoE \
--include="*.ts" --include="*.js" --include="*.py" --include="*.go" \
- --include="*.rs" --include="*.java" --include="*.rb" --include="*.env" .
+ --include="*.rs" --include="*.java" --include="*.rb" --include="*.env*" \
+ "AKIA|sk-|ghp_|password[[:space:]]*=|secret[[:space:]]*=" .
```
If **no specialized security tools are available**, use the Explore agent to discover security-sensitive areas across the codebase. Use repomix-explorer (if available) for large codebases to get a structural overview. Then use targeted Grep and Read for detailed analysis:
- Review files matching `**/auth/**`, `**/login/**`, `**/api/**`, `**/*.env*`
- Check for hardcoded credentials, SQL injection, XSS, insecure crypto
- Check dependency manifests for known-vulnerable version ranges
## Step 3: Classify Findings
Map tool output to the findings format from REPORT-FORMAT.md.
**Severity mapping** (reinforced here for defense-in-depth):
| Tool Output | Maps To |
|-------------|---------|
| `critical` / CVSS >= 9.0 | **Critical** |
| `high` / CVSS 7.0-8.9 | **High** |
| `moderate` / `medium` / CVSS 4.0-6.9 | **Medium** |
| `low` / CVSS 0.1-3.9 | **Low** |
| `info` / advisory only | **Info** |
| Dependency > 2 major versions behind latest | **Medium** — type: `dependency-staleness` |
| Dependency > 1 major version behind | **Low** — type: `dependency-staleness` |
| Dependency last updated > 2 years ago (unmaintained, when metadata available) | **Medium** — type: `dependency-staleness` |
| > 10 dependencies with pending updates | **Low** — type: `dependency-staleness` (aggregate) |
Deduplicate findings that appear in multiple tools. Prefer the source with more detail.
## Step 4: Create GitHub Issue
**Read [../dlc/references/ISSUE-TEMPLATE.md](../dlc/references/ISSUE-TEMPLATE.md) now** and format the issue body exactly as specified.
**Critical format rules** (reinforced here):
- Title: `[DLC] Security: {summary of top finding}`
- Label: `dlc-security`
- Body must contain: Scan Metadata table, Findings Summary table (severity x count), Findings Detail grouped by severity, Recommended Actions, Raw Output in collapsed details
+ Acquire `BRANCH` for the Scan Metadata table above — ISSUE-TEMPLATE.md's lifecycle acquires `REPO` itself. Then follow ISSUE-TEMPLATE.md's **Issue Creation Command** lifecycle exactly — substitute `{skill-name}` = `security`, `{Type}` = `Security`, `{type}` = `security`, `{additional-required-sections}` = `'## Raw Output'`:
+
```bash
- REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
BRANCH=$(git branch --show-current)
- TIMESTAMP=$(date +%s)
- BODY_FILE="/tmp/dlc-issue-${TIMESTAMP}.md"
- # ... write formatted body to $BODY_FILE ...
-
- gh issue create \
- --repo "$REPO" \
- --title "[DLC] Security: {summary}" \
- --body-file "$BODY_FILE" \
- --label "dlc-security"
+ echo "BRANCH=$BRANCH" # ISSUE-TEMPLATE.md's Write step is a separate tool call and can't see this shell's variables
```
-
- If issue creation fails, save draft to `/tmp/dlc-draft-${TIMESTAMP}.md` and print the path with a manual command.
## Step 5: Report
Print a summary to the user:
```text
Security scan complete.
- Project type: {type}
- Tools used: {list}
- Findings: {critical} critical, {high} high, {medium} medium, {low} low
- Issue: #{number} ({url})
```
If no findings, skip issue creation and report: "No security issues found."