---
name: security-scan
description: "Scan for secrets, OWASP/SAST code patterns, and compliance gaps; does NOT scan dependency CVEs (owned by dependency-health)"
type: operational
cadence: weekly
---

# Security Scan Runbook

## Objetivo

Detect leaked secrets, SAST/OWASP code-level vulnerabilities, and compliance gaps across source code, configuration files, CI/CD workflows, and scripts. Findings above the severity threshold are filed as task work items with full remediation context.

This runbook does **not** scan for dependency CVEs or vulnerable package versions. That responsibility belongs to the `dependency-health` runbook, which owns the supply-chain surface. The boundary is strict: if the finding originates from a project dependency rather than authored code or configuration, it is out of scope here.

## Precondiciones

- `gitleaks` installed for secret detection. Install via GitHub releases binary or package manager.
- `semgrep` installed for SAST and OWASP scanning. Install via `pip3 install --user semgrep`.
- `gh` or `az` CLI authenticated for work item creation and deduplication.
- `.ai-engineering/runbooks/gitleaks-extended.toml` present for Step 3 extended config scan.

## Procedimiento

### Step 1 -- Secret detection

Run gitleaks against the full working tree to detect leaked credentials, API keys, tokens, and private keys.

```bash
gitleaks detect --source . --no-git --report-format json --report-path /tmp/gitleaks-report.json
```

Parse the output and store findings as `$SECRET_FINDINGS`. Each entry contains the file path, line number, rule ID, and a redacted match preview. Record the count for the final report.

### Step 2 -- SAST scan

Run semgrep with its auto configuration to detect code-level vulnerabilities across all scan targets.

```bash
semgrep scan --config auto --json --output /tmp/semgrep-report.json src/
```

Parse the JSON output. Each finding includes the rule ID, CWE reference, severity, file path, line range, and a message describing the vulnerability. Store as `$SAST_FINDINGS`.

### Step 3 -- Config file credential scan

Run gitleaks a second pass with extended rules targeting connection strings and inline API keys in config files (`.env*`, `*.yml`, `*.json`). Use a custom `--config` that extends the default ruleset with `hardcoded-connection-string` and `hardcoded-api-endpoint-with-key` rules.

```bash
gitleaks detect --source . --no-git --report-format json --report-path /tmp/gitleaks-config-report.json \
  --config .ai-engineering/runbooks/gitleaks-extended.toml
```

Merge results into `$SECRET_FINDINGS`. Deduplicate by file path and line number against Step 1 results.

### Step 4 -- CI/CD workflow security audit

Scan CI/CD workflow files for security anti-patterns.

```bash
# Check for unpinned third-party actions (should use SHA, not tag)
grep -rn 'uses:' .github/workflows/ | grep -v '@[a-f0-9]\{40\}' | grep -v 'actions/\(checkout\|setup-\|cache\)' > /tmp/unpinned-actions.txt || true

# Check for overly permissive workflow permissions
grep -rn 'permissions:' .github/workflows/ | grep -i 'write-all\|contents: write' > /tmp/broad-permissions.txt || true

# Check for secrets exposed in logs or env
grep -rn 'echo.*\${{.*secrets\.' .github/workflows/ > /tmp/secret-echo.txt || true

# Check for pull_request_target with checkout of PR head (code injection risk)
grep -rn -A5 'pull_request_target' .github/workflows/ | grep 'ref:.*head' > /tmp/pr-target-risk.txt || true
```

Consolidate findings into `$CICD_FINDINGS`. Classify each by sub-category: `unpinned-action`, `broad-permissions`, `secret-exposure`, or `pr-injection-risk`.

### Step 5 -- OWASP pattern detection

Extend the semgrep scan to check explicitly for OWASP Top 10 patterns in application code.

```bash
semgrep scan --config "p/owasp-top-ten" --json --output /tmp/semgrep-owasp-report.json src/ scripts/
```

Map each finding to its OWASP category and CWE (SQL injection/CWE-89, XSS/CWE-79, path traversal/CWE-22, command injection/CWE-78, insecure deserialization/CWE-502, SSRF/CWE-918). Store as `$OWASP_FINDINGS`. Deduplicate against `$SAST_FINDINGS` from Step 2 by file, line, and rule ID.

### Step 6 -- Map findings and deduplicate via handler

For each finding above the severity threshold (`medium`), map to the Finding contract and route through the shared dedup handler.

**Finding mapping:**

```yaml
domain_label: "security-finding"
title: "security-finding: $RULE_ID in $FILE_PATH:$LINE"
file_path: $FILE_PATH
rule_id: $RULE_ID
symbol: null
severity: $SEVERITY (critical | high | medium)
body: |
  ## Security Finding

  | Field      | Value              |
  |------------|--------------------|
  | File       | $FILE_PATH:$LINE   |
  | Type       | $FINDING_TYPE      |
  | Severity   | $SEVERITY          |
  | Rule       | $RULE_ID           |
  | CWE/OWASP  | $CWE_REF          |

  ## Description
  $DESCRIPTION

  ## Remediation
  $REMEDIATION

  ---
  *Auto-generated by security-scan runbook.*
```

Follow `handlers/dedup-check.md` to process all findings through the dedup cascade (max 20 per run). The handler labels new issues with `security-finding` and `sev-$SEVERITY`.

### Step 8 -- Emit run summary to stdout

Print a brief run summary to stdout. Do **not** write a local report file -- findings are tracked exclusively in GitHub Issues / Azure DevOps work items created in Step 7.

```bash
echo "=== Security Scan Run $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
echo ""
echo "--- Findings by Category ---"
echo "  Secrets:               $COUNT_SECRETS"
echo "  SAST (code patterns):  $COUNT_SAST"
echo "  Config credentials:    $COUNT_CONFIG"
echo "  CI/CD anti-patterns:   $COUNT_CICD"
echo "  OWASP patterns:        $COUNT_OWASP"
echo "  Total:                 $COUNT_TOTAL"
echo ""
echo "--- Severity Distribution ---"
echo "  critical:              $COUNT_CRITICAL"
echo "  high:                  $COUNT_HIGH"
echo "  medium:                $COUNT_MEDIUM"
echo "  low:                   $COUNT_LOW (logged, not escalated)"
echo "  info:                  $COUNT_INFO (logged, not escalated)"
echo ""
echo "--- Work Items ---"
echo "  New findings:          $COUNT_NEW"
echo "  Pre-existing:          $COUNT_PREEXISTING"
echo "  Work items created:    $COUNT_CREATED"
echo "  Skipped (duplicate):   $COUNT_SKIPPED"
echo ""
echo "Mutations applied:       $MUTATION_COUNT / 20"
echo "Board:                   GitHub Issues (label: security-finding)"
```

## Scope Boundary

This runbook owns the **authored-code and configuration** security surface. The boundary with `dependency-health` is explicit and non-overlapping:

| Concern | Owner | Examples |
|---------|-------|---------|
| Leaked secrets in code/config | `security-scan` | API keys in `.env`, tokens in YAML |
| SAST code patterns | `security-scan` | SQL injection, XSS, path traversal |
| CI/CD workflow anti-patterns | `security-scan` | Unpinned actions, secret exposure |
| Hardcoded credentials | `security-scan` | Passwords in config files |
| Dependency CVEs | `dependency-health` | Known vulnerabilities in pip/npm packages |
| Outdated packages | `dependency-health` | Packages behind latest security patch |
| License compliance | `dependency-health` | Copyleft or restricted licenses |

If a semgrep rule fires on an import statement but the root cause is a vulnerable library version, defer to `dependency-health`. This runbook only acts on patterns in code the team authored or configuration the team maintains.

## Output

Summary to stdout. Work items created for findings. No local files are written.

## Guardrails

- **Never** modifies source code, configuration files, or CI/CD workflows -- this is a detection-only runbook.
- **Never** auto-remediates findings -- remediation is a human or implementation-agent responsibility.
- **Never** exceeds 20 work-item mutations per run -- the `max_mutations` guardrail halts execution and reports remaining findings in the summary without creating work items.
- **Never** modifies items labeled `p1-critical` or `pinned` -- these are protected labels.
- **Never** modifies items in `closed` or `resolved` state -- these are protected states.
- **Never** creates or mutates feature-level work items. This runbook only opens task-level follow-up items.
- **Never** escalates findings below `medium` severity to work items -- low and info findings are logged in the report for awareness but do not generate issues or tasks.
- **Mutations enabled by default.** All qualifying findings are created as work items automatically.
