Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -132,21 +156,36 @@ 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
fail-build: true
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"
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading