Ctrl K
ComunidadGuías…

SECURITY-SCANNING.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Security Scanning

This document describes the security scanning infrastructure for ShellHub.

**Quick reference**

| Topic | Section |
|---|---|
| Frozen required-check name | [Required check — `security-gate`](#required-check--security-gate) |
| Full list of scan tools | [Scan inventory](#scan-inventory) |
| Suppression files and how to add entries | [Baseline suppression files](#baseline-suppression-files) |
| Inline `#nosec` and `# nosemgrep` conventions | [Inline suppression conventions](#inline-suppression-conventions) |
| `gosec` exclusion review and rationale | [`gosec` exclusion review](#gosec-exclusion-review) |
| amd64-only scan residual risk | [Agent amd64-scan residual risk](#agent-amd64-scan-residual-risk) |
| Fork and Dependabot behavior | [Fork and Dependabot PRs](#fork-and-dependabot-prs) |
| How to flip `security-gate` to blocking | [Rollout order](#rollout-order) |

---

## Required check — `security-gate`

**`security-gate` is the single frozen required-check name** registered in branch protection.
Security runs it as its only job, on every PR: Semgrep always, govulncheck and Trivy only when
the PR changes a `go.mod`, `go.sum`, Dockerfile, `versions.env` or `.trivyignore`. CodeQL
exposes a terminal job of the same name that aggregates its scans, and runs only on the daily
schedule and release tags.

> **Do not rename `security-gate`.**  Renaming it silently removes the branch-protection
> requirement and allows unreviewed code to merge.

---

## Scan inventory

| Workflow file | Tool | What it scans | Runs on |
|---|---|---|---|
| `.github/workflows/security.yml` | **govulncheck** | Known Go CVEs in all modules (`.`, `server`, `agent`, `openapi`, `tests`); SARIF uploaded to GitHub Security tab | PRs changing dependencies or Dockerfiles + tags + daily |
| `.github/workflows/security.yml` | **Trivy (image)** | OS/library CVEs in service images (`server`, `ui`, `agent`) | PRs changing dependencies or Dockerfiles + tags + daily |
| `.github/workflows/security.yml` | **Semgrep** | Static analysis via `p/golang`, `p/dockerfile`, `p/ci`; PR mode uses `--baseline-commit` so only _new_ findings block | PR + tags + daily |
| `.github/workflows/codeql.yml` | **CodeQL** | Semantic Go analysis across all modules; SARIF uploaded to GitHub Security tab | Tags + daily |
| `.github/workflows/build-agent.yml` | **Trivy (image, amd64)** | Agent image CVEs as part of the agent build pipeline | Push to master + tags |

All workflows upload SARIF reports to the GitHub Security tab (skipped on fork PRs because
those lack the necessary `security-events: write` permission).

---

## Baseline suppression files

Two baseline files gate new suppressions behind a team-lead code-owner review
(see `.github/CODEOWNERS`).

### `.trivyignore` — Trivy CVE suppression

```
# CVE-YYYY-NNNNN  # owner: @handle — reason; tracked: ISSUE-123
```

**How to add an entry:**

1. Verify the CVE is genuinely not exploitable in this context (e.g., the affected
   component is not reachable, or a mitigating control already exists).
2. Add a line in the format above with your GitHub handle as `owner` and a brief
   justification.
3. Open a PR — `.github/CODEOWNERS` routes `.trivyignore` changes to
   `@shellhub-io/team-lead` for mandatory review.
4. Schedule a quarterly review to remove the entry once the vulnerability is patched.

### `.semgrepignore` — Semgrep path exclusion

```
# <glob pattern>  # reason
```

Use `.semgrepignore` to exclude paths that produce only false positives (test fixtures,
generated mocks).  **Do not suppress production source paths** — fix the finding instead.

---

## Inline suppression conventions

### `#nosec` (gosec)

Append `//nolint:gosec` or `#nosec GXXX` directly on the line that triggers the finding:

```go
conn, err := tls.Dial("tcp", addr, cfg) //nolint:gosec // G402: minimum TLS version enforced by cfg
```

Always include a short rationale after the directive.

### govulncheck — suppressing a finding

govulncheck findings are surfaced as alerts in the GitHub Security tab (SARIF-based).
To suppress a known, accepted vulnerability:

1. Open the alert in the **Security → Code scanning alerts** tab.
2. Click **Dismiss alert** and choose a reason (e.g. "Used in tests", "Risk accepted",
   "False positive") along with a brief comment identifying the owner and justification.
3. Dismissed alerts are audited and visible to the team in the Security tab.  There is no
   separate suppression file — suppression lives entirely in GitHub code scanning.

> **Note:** The `agent` module scans with the `docker` build tag, via the
> `GOFLAGS` environment variable forwarded to `go/packages`.

### `# nosemgrep: <rule-id> -- <reason>` (Semgrep)

Add a comment on the line **above** the offending code using the exact format
`# nosemgrep: <rule-id> -- <reason>`:

```python
# nosemgrep: go.lang.security.audit.dangerous-exec-command -- input validated by allowlist
exec.Command(cmd, args...)
```

For Go files the comment is an inline `//`:

```go
// nosemgrep: go.lang.security.audit.crypto.use_of_weak_crypto -- MD5 used only for non-security cache key
hash := md5.Sum(data) //nolint:gosec
```

The rule ID must match the Semgrep rule identifier exactly (visible in the Semgrep output
or the SARIF report).

---

## `gosec` exclusion review

Before this work, `.golangci.yaml` blanket-excluded four gosec rules: G104, G301, G302, and G304. The review reduced the exclusion list to G104 only, which is redundant with the project-wide `errcheck` disable. The remaining rules were handled per-site:

| Rule | What it flags | Sites found | Resolution |
|---|---|---|---|
| G301 | Directory created with permissions > 0750 | `gateway/nginx.go` (`MkdirAll 0o755`) | Tightened to `0o750`; annotated with `//nolint:gosec` because gosec still flags the call form |
| G302 | File created with permissions > 0600 | `agent/server/utmp/utmp.go` (`OpenFile 0o644`) | Kept at `0o644` — utmp/wtmp files are conventionally world-readable per POSIX; tighter permissions break system accounting tools. Annotated |
| G304 | File path from variable (potential traversal) | `agent/pkg/keygen/keygen.go` (`Create`, `ReadFile`), `agent/pkg/sysinfo/network.go` (`ReadFile`), `api/store/pg/` tests (`Open`, `ReadFile`) | keygen: path comes from a configured env var, not user input — annotated. sysinfo: path rooted under `/sys/class/net` (read-only kernel VFS) — annotated. agent: added `cleanKeyPath` guard rejecting raw `..` sequences before `generatePrivateKey`/`readPublicKey`. Tests: paths from `runtime.Caller`, not user input — annotated |

The exclusion lives in the repository's single `.golangci.yaml`. `server` and the other
submodules symlink to it, so there is one file to edit rather than one per module.
golangci-lint resolves a single config instead of merging several, so a module that ever
needs its own rules replaces the symlink with a real file and restates everything it keeps.

---

## Agent amd64-scan residual risk

The agent image is a multi-arch build (`linux/amd64`, `linux/arm64/v8`, `linux/arm/v7`,
`linux/arm/v6`, `linux/386`).  Trivy scans only the **amd64** variant during CI
(`.github/workflows/build-agent.yml`).

**Residual risk:** architecture-specific CVEs for `arm` and `386` variants are **not
blocked by CI**.  They are surfaced by the daily scheduled scan
(`.github/workflows/security.yml` scans the `agent` image) and tracked
manually.  If a CVE affects a non-amd64 variant only, add it to `.trivyignore` with an
explicit `owner` and architectural scope note.

---

## Fork and Dependabot PRs

- **Fork PRs** lack the `security-events: write` permission required to upload SARIF reports
  to the GitHub Security tab.  The SARIF upload steps are conditional on
  `!github.event.pull_request.head.repo.fork` and are skipped automatically.  Scan jobs
  themselves still run and `security-gate` still blocks — only the SARIF upload is skipped.

- **Dependabot PRs** are treated as first-party PRs (Dependabot has write access to the
  repo).  A dependency bump changes a `go.mod` or `go.sum`, so govulncheck and Trivy run on
  it alongside Semgrep; CodeQL picks it up in the next daily scan.  Because Dependabot bumps
  one dependency at a time the blast radius of a failing scan is narrow and easy to triage.

---

## Rollout order

Follow this sequence when enabling `security-gate` as a blocking required check on a new
branch or workflow:

1. **Land scans as non-blocking** — merge the workflow files with `security-gate` jobs but
   do not yet mark them as required checks in branch protection.  Let the scans accumulate
   data for at least one week.

2. **Seed the baseline files** — review findings from step 1 and populate `.trivyignore`
   and `.semgrepignore` with justified suppressions for any pre-existing issues that cannot
   be fixed immediately.  Each entry requires an owner and justification comment.  For
   govulncheck findings, dismiss the alert in the GitHub Security tab with a reason — no
   separate file is needed.

3. **Flip to blocking-on-new** — configure Semgrep to run with `--baseline-commit origin/master`
   on PRs (already the default in `security.yml`) so that only _new_ findings introduced by
   a PR cause `security-gate` to fail.  Confirm that existing suppressed findings do not
   re-surface.

4. **Admin marks `security-gate` required + Require review from Code Owners** — in the
   GitHub repository settings → Branches → Branch protection rules for `master`:
   - Under "Require status checks to pass before merging", add **`security-gate`** (the
     exact frozen name). Security reports it on every PR.
   - Enable **"Require review from Code Owners"** so that changes to `.trivyignore`
     and `.semgrepignore` always go through `@shellhub-io/team-lead`.

> **Note:** Steps 1–3 are performed by contributors; step 4 requires a repository admin.