diff --git a/.github/workflows/security-vuln-weekly.yml b/.github/workflows/security-grype.yml similarity index 61% rename from .github/workflows/security-vuln-weekly.yml rename to .github/workflows/security-grype.yml index f6ca387..bc8175c 100644 --- a/.github/workflows/security-vuln-weekly.yml +++ b/.github/workflows/security-grype.yml @@ -1,12 +1,27 @@ -name: "🛡️ Security: Vuln Scan" +name: "🛡️ Security: Grype & Govulncheck" run-name: >- ${{ - github.event_name == 'schedule' && '🛡️ Security: Vuln Scan — Weekly run' || - format('🛡️ Security: Vuln Scan — Manual by {0}', github.actor) + github.event_name == 'schedule' && '🛡️ Security: Grype & Govulncheck — Weekly run' || + github.event_name == 'pull_request' && format('🛡️ Security: Grype & Govulncheck — PR #{0}', github.event.pull_request.number) || + format('🛡️ Security: Grype & Govulncheck — Manual by {0}', github.actor) }} on: workflow_dispatch: + pull_request: + branches: [main, 'dev/*'] + # Dependency / source / build changes are what move the security + # surface — scope PR runs to those so doc-only PRs stay fast. + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + - 'package.json' + - 'package-lock.json' + - '**/package.json' + - 'Dockerfile*' + - '.grype.yaml' + - '.github/workflows/security-grype.yml' schedule: - cron: '15 7 * * 1' # Weekly on Monday at 07:15 UTC @@ -17,7 +32,7 @@ env: GOVULNCHECK_VERSION: v1.2.0 concurrency: - group: security-weekly-${{ github.workflow }} + group: security-grype-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -39,6 +54,10 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: + # go.mod declares only the language version (`go 1.26.0`) with no + # `toolchain` directive, so go-version-file would pin govulncheck to + # 1.26.0 and report stdlib advisories already fixed in 1.26.1+. Track + # the latest 1.26.x like the build and the gating ci.yml scan do. go-version: "1.26" - name: Install govulncheck @@ -52,13 +71,17 @@ jobs: run: | { echo "### Govulncheck" - echo "- Scanned Go dependencies for known vulnerabilities" + echo "- Reachability-based scan of Go dependencies (call-graph aware)" echo "- Tool version: ${GOVULNCHECK_VERSION}" echo "- Database: Go Vulnerability Database (vuln.go.dev)" } >> "$GITHUB_STEP_SUMMARY" grype-image: name: "🐳 Security: Grype Container Scan" + # Image build is the heavy step; keep it on scheduled/manual runs. + # PRs get fast, accurate dependency coverage from grype-deps + + # govulncheck without paying for a Docker build on every push. + if: github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -104,18 +127,19 @@ jobs: uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: ${{ steps.grype.outputs.sarif }} + category: grype-image - name: Summarize if: always() run: | { echo "### Grype Container Scan" - echo "- Scanned container image for HIGH and CRITICAL CVEs" + echo "- Scanned the built image (binary build-info → real shipped deps) for HIGH/CRITICAL CVEs" echo "- Base image: Wolfi (Chainguard) packages on FROM scratch" } >> "$GITHUB_STEP_SUMMARY" - grype-source: - name: "📦 Security: Grype Source Scan" + grype-deps: + name: "📦 Security: Grype Dependency Scan (Go + npm)" runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -132,9 +156,14 @@ jobs: with: persist-credentials: false - - name: Run Grype on source directory + - name: Run Grype across the repository (Go modules + npm lockfiles) + # Scanning the repo root catalogs go.mod/go.sum and package-lock.json, + # so a single pass covers both ecosystems. Grype matches the + # lockfile-resolved versions (not the full module requirement graph), + # so it does not emit the phantom-dependency findings a manifest-graph + # SCA reports for modules the binary never links in. uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 - id: grype-src + id: grype-deps with: path: . severity-cutoff: high @@ -142,11 +171,21 @@ jobs: output-format: sarif config: .grype.yaml - - name: Upload Grype source SARIF + - name: Upload Grype dependency SARIF if: always() && github.event.repository.visibility == 'public' uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: - sarif_file: ${{ steps.grype-src.outputs.sarif }} + sarif_file: ${{ steps.grype-deps.outputs.sarif }} + category: grype-deps + + - name: Summarize + if: always() + run: | + { + echo "### Grype Dependency Scan (Go + npm)" + echo "- Scanned Go modules and npm lockfiles across the repo for HIGH/CRITICAL CVEs" + echo "- Matches lockfile-resolved versions (no module-graph false positives)" + } >> "$GITHUB_STEP_SUMMARY" gosec: name: "🔐 Security: Gosec SAST" @@ -169,13 +208,19 @@ jobs: - name: Run Gosec uses: securego/gosec@9e6a9843d7a4a6e3e9a8539b02612c8a4aa3f889 # v2.27.1 with: - args: -fmt sarif -out gosec-results.sarif ./... + # Report-only: gosec is heuristic (e.g. G115 int->uint conversions in + # the benchmark helpers) and feeds findings to the Security tab via the + # SARIF upload below. Build gating is handled by CodeQL (ruleset-enforced), + # Grype, and govulncheck, so -no-fail keeps gosec from failing the run + # on advisory-level findings while still surfacing them for triage. + args: -no-fail -fmt sarif -out gosec-results.sarif ./... - name: Upload Gosec SARIF if: always() && github.event.repository.visibility == 'public' uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: gosec-results.sarif + category: gosec # NOTE: testssl job omitted — Portwing does not operate a TLS listener of its own # (TLS is handled by a reverse proxy in front). Re-add if Portwing gains direct diff --git a/CHANGELOG.md b/CHANGELOG.md index 1359050..a91d1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Edge exec input ordering**: `exec_input` that arrived immediately after `exec_start` could be dropped, because the session was only registered after the Docker `CreateExec`/`StartExec` round-trip completed. The session is now registered synchronously up front and early input is buffered in arrival order by a single per-session writer goroutine, then replayed once the exec connection is live — keystrokes typed before the shell comes up are no longer lost or reordered. - **Edge outbound backpressure**: every sender (exec output, request/stream responses, metrics, pings) previously wrote the WebSocket directly under one mutex with no write deadline, so a single slow or wedged controller could head-of-line-block every session, stall the read pump, and hang the agent indefinitely. Outbound frames now funnel through a single `sendPump` goroutine fronting a bounded queue with a per-frame write deadline; a controller that can't keep up is evicted and reconnected rather than dropping frames (which would hang a request or corrupt a stream). +### Changed + +- **Standardized dependency/CVE scanning on Grype + govulncheck; Snyk stays off Portwing.** Snyk's GitHub SCM integration scans the full Go *module requirement graph* (`go mod graph`) instead of the compiled build graph, so it flags advisories in modules that transitive deps merely *require* but the binary never links in (nothing in `go list -deps ./...`, nothing reachable per govulncheck, clean under Grype). That's a methodology gap, not staleness, so it's being decommissioned org-wide. Portwing never wired Snyk into the repo (no `.snyk` policy, no workflow step, no README badge), so there was nothing to strip out on the repo side. govulncheck (Go call-graph reachability) and Grype (the built image's binary build-info, plus `go.mod`/`go.sum` and the npm lockfiles) already cover dependencies accurately. The existing weekly scan is consolidated into `security-grype.yml`, which now also runs on pull requests (path-filtered to source/deps/Dockerfile/the workflow itself), keeps the weekly cron and manual dispatch, guards the heavy container build off PRs (govulncheck plus the dependency scan give fast PR coverage), gives each scanner a distinct code-scanning `category` so the Grype image and dependency SARIF no longer clobber each other in the Security tab, and runs gosec in report-only mode (`-no-fail`) so its heuristic findings still feed the Security tab without gating the build (CodeQL, Grype, and govulncheck handle the gating). + ## [0.3.0] - 2026-06-15 ### Added