Skip to content
Open
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
128 changes: 128 additions & 0 deletions .github/workflows/gosec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Gosec Security Scan

# Port of the Lumera chain repo's `gosec.yml` (LumeraProtocol/lumera#213).
#
# Same tool, same policy: HIGH severity + HIGH confidence, suppressions must
# carry both a rule ID and a justification (`-nosec-require-rules`,
# `-nosec-require-justification`), results published to GitHub code scanning
# as SARIF.
#
# Difference from the chain repo: supernode has four Go modules instead of
# three. gosec's package loader walks a single module at a time, so each is
# scanned from its own working directory. A root `./...` run does NOT cover
# the nested modules.

on:
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- ".gitignore"
push:
branches: [master]

permissions:
contents: read
security-events: write

jobs:
gosec:
name: gosec (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
# Report every module; one module's findings must not hide another's.
fail-fast: false
matrix:
include:
- name: supernode
module: .
- name: sncli
module: cmd/sncli
- name: sn-manager
module: sn-manager
- name: systemtests
module: tests/system

steps:
- name: Checkout code
uses: actions/checkout@v6.0.1

# Installs Go from go.mod, libwebp-dev (required for the root module to
# type-check — without it gosec fails SSA construction rather than
# reporting a clean scan), and GOPRIVATE for the Lumera module.
- name: Setup Go and system deps
uses: ./.github/actions/setup-env

- name: Download Go modules
working-directory: ${{ matrix.module }}
run: go mod download

- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0

# No `continue-on-error` here: this is a blocking gate. `-stdout
# -verbose text` keeps findings readable in the job log even when the
# SARIF upload is skipped (fork PRs).
- name: Run gosec
working-directory: ${{ matrix.module }}
run: |
gosec \
-severity high \
-confidence high \
-nosec-require-rules \
-nosec-require-justification \
-fmt sarif \
-out gosec.sarif \
-stdout \
-verbose text \
./...

# gosec emits artifact URIs relative to the scanned module, so findings
# from nested modules would otherwise be attributed to paths that do not
# exist at the repository root. Prefix them with the module directory.
- name: Normalize SARIF paths
if: ${{ always() && matrix.module != '.' }}
env:
GOSEC_MODULE: ${{ matrix.module }}
GOSEC_SARIF: ${{ matrix.module }}/gosec.sarif
run: |
python3 - <<'PY'
import json
import os
from pathlib import PurePosixPath
from urllib.parse import urlparse

sarif_path = os.environ["GOSEC_SARIF"]
module = os.environ["GOSEC_MODULE"].strip("/")
with open(sarif_path, encoding="utf-8") as sarif_file:
sarif = json.load(sarif_file)

def prefix_artifact_uris(value):
if isinstance(value, dict):
artifact = value.get("artifactLocation")
if isinstance(artifact, dict):
uri = artifact.get("uri")
if isinstance(uri, str) and uri and not urlparse(uri).scheme and not uri.startswith("/"):
if not uri.startswith(module + "/"):
artifact["uri"] = str(PurePosixPath(module) / uri)
for child in value.values():
prefix_artifact_uris(child)
elif isinstance(value, list):
for child in value:
prefix_artifact_uris(child)

prefix_artifact_uris(sarif)
with open(sarif_path, "w", encoding="utf-8") as sarif_file:
json.dump(sarif, sarif_file, separators=(",", ":"))
PY

# `security-events: write` is not granted to fork PRs, and dependabot
# runs with a read-only token, so skip only the upload for those. The
# scan itself still runs and still blocks.
- name: Upload SARIF
if: ${{ always() && (github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')) }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ matrix.module }}/gosec.sarif
category: gosec-${{ matrix.name }}
63 changes: 63 additions & 0 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Go Vulnerability Scan

# Port of the Lumera chain repo's `govulncheck.yml` (LumeraProtocol/lumera#212).
#
# Same tool, same goal: fail a PR when it introduces (or is built on) a
# *reachable* known vulnerability. govulncheck's default mode only reports
# vulnerabilities whose vulnerable symbols are actually called, so unused
# transitive dependencies do not create noise.
#
# Difference from the chain repo: supernode is a multi-module repository
# (root, cmd/sncli, sn-manager, tests/system). The chain repo scans a single
# module; here each module is scanned independently via a matrix so a
# vulnerability in sn-manager cannot hide behind a clean root scan.

on:
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- ".gitignore"
push:
branches: [master]

permissions:
contents: read

jobs:
govulncheck:
name: govulncheck (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
# Surface every module's result; do not let the first failure mask others.
fail-fast: false
matrix:
include:
- name: supernode
module: .
- name: sncli
module: cmd/sncli
- name: sn-manager
module: sn-manager
- name: systemtests
module: tests/system

steps:
- name: Checkout code
uses: actions/checkout@v6.0.1

# Installs Go from go.mod, libwebp-dev (required to build the root
# module), and sets GOPRIVATE for the Lumera module.
- name: Setup Go and system deps
uses: ./.github/actions/setup-env

# Pinned rather than @latest: an unpinned scanner turns an unrelated
# upstream release into a surprise CI break, and makes a red run
# non-reproducible after the fact.
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4

- name: Run govulncheck
working-directory: ${{ matrix.module }}
run: govulncheck ./...
93 changes: 93 additions & 0 deletions .github/workflows/lint-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: lint-pr

# Port of the Lumera chain repo's `lint-pr.yml` (LumeraProtocol/lumera#126).
#
# Runs golangci-lint through reviewdog with `filter_mode: added`, so findings
# are posted as inline review comments on exactly the lines a PR changed and
# the pre-existing baseline stays invisible.
#
# IMPORTANT DIFFERENCE FROM THE CHAIN REPO:
# The chain repo has a separate full-scan gate (`lint.yml`) that is the
# authoritative merge blocker, which lets its reviewdog layer be purely
# additive. Supernode has NO existing lint gate and a large legacy baseline,
# so a full-repo hard gate would be red on day one. This workflow is therefore
# the only lint enforcement, and it is intentionally scoped to the diff:
# new code must be clean, legacy debt is not retroactively blocking.
#
# Version pinning (see below) is not cosmetic — go.mod declares go 1.26.2 and
# golangci-lint must be BUILT with a Go >= that language version or it exits
# with a hard "the Go language version used to build golangci-lint is lower
# than the targeted Go version" error. v2.13.2 is built with go1.27.0.
# reviewdog/action-golangci-lint must be >= v2.8.0 to honour a v2.x pin.

on:
pull_request:
paths-ignore:
- "**.md"
- "docs/**"
- ".gitignore"

permissions:
contents: read
pull-requests: write # reviewdog needs this to post inline review comments (same-repo PRs only)
checks: write # for the check-run summary

jobs:
golangci-lint-diff:
name: golangci-lint (diff-only, inline)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v6.0.1
with:
# reviewdog needs the merge-base to compute the PR diff.
fetch-depth: 0

# Installs Go from go.mod plus libwebp-dev, which the root module
# requires in order to type-check.
- name: Setup Go and system deps
uses: ./.github/actions/setup-env

# Same-repo PRs: post findings as inline review comments. The default
# GITHUB_TOKEN has the `pull-requests: write` scope declared above.
#
# We deliberately do NOT set `level` here: reviewdog's `level` input
# REWRITES every finding's severity rather than defaulting it, so
# `level: warning` combined with `fail_level: error` would silently
# neuter the gate. Leaving it unset preserves native severities so
# govet/staticcheck errors actually trip `fail_level: error`.
- name: golangci-lint via reviewdog (same-repo PR — inline comments)
if: github.event.pull_request.head.repo.full_name == github.repository
uses: reviewdog/action-golangci-lint@v2.10.0
with:
go_version_file: go.mod
golangci_lint_version: v2.13.2
golangci_lint_flags: "--config=.golangci.yml --timeout=15m"
workdir: .
# Only annotate lines actually changed by the PR.
filter_mode: added
reporter: github-pr-review
fail_level: error
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Fork PRs: GITHUB_TOKEN is strictly read-only regardless of the
# workflow-level permissions block, so neither `github-pr-review`
# (needs pull-requests: write) nor `github-pr-check` (needs
# checks: write) can post results. The `local` reporter writes findings
# to the job log instead. `fail_level: error` still fails the job, so
# the check continues to block merge for external contributors.
- name: golangci-lint via reviewdog (fork PR — log-only)
if: github.event.pull_request.head.repo.full_name != github.repository
uses: reviewdog/action-golangci-lint@v2.10.0
with:
go_version_file: go.mod
golangci_lint_version: v2.13.2
golangci_lint_flags: "--config=.golangci.yml --timeout=15m"
workdir: .
filter_mode: added
reporter: local
fail_level: error
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77 changes: 77 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
version: "2"

# Linter policy for the supernode repo.
#
# Mirrors the Lumera chain repo's `.golangci.yml` (same linter set, same
# v2 schema) so both repos enforce a consistent bar. The exclusion list is
# supernode-specific: it covers generated protobuf/mock surfaces and test
# helpers rather than the chain's Cosmos SDK deprecations.
#
# Enforcement is diff-only via reviewdog (`.github/workflows/lint-pr.yml`),
# so the pre-existing baseline is not a merge blocker; only findings on
# lines a PR actually touches are reported.

linters:
default: none
enable:
- errcheck
- staticcheck
- unused
- ineffassign
- govet
- nolintlint

exclusions:
generated: strict
rules:
# Test files: setup helpers routinely ignore errors from fixture
# construction where a failure would surface as a test failure anyway.
- path: _test\.go
linters:
- errcheck

# Generated gRPC/protobuf gateway + mock surfaces.
- path: (gen/|/mocks?/|\.pb\.go|\.pb\.gw\.go)
linters:
- errcheck
- unused
- staticcheck

# SDK deprecated APIs — cannot be removed until upstream drops them.
- linters:
- staticcheck
text: "SA1019"

# Cosmetic quick-fix suggestions (QF1003/QF1007/QF1008/QF1011).
- linters:
- staticcheck
text: "QF10(0[378]|11)"

# S1001 (use copy), S1009 (nil check before len), S1011 (use append),
# S1021 (merge var) — cosmetic, not correctness issues.
- linters:
- staticcheck
text: "S100[19]|S101[1]|S1021"

# ST1005 (error string capitalization), ST1019 (duplicate import),
# ST1023 (omit type from declaration) — style, not correctness.
- linters:
- staticcheck
text: "ST10(05|19|23)"

# SA4031 (nil check on make result), SA9003 (empty branch) —
# intentional patterns in existing code.
- linters:
- staticcheck
text: "SA(4031|9003)"

# SA1029 (built-in type as context key) — existing pattern.
- linters:
- staticcheck
text: "SA1029"

formatters:
enable:
- gofmt
exclusions:
generated: strict
2 changes: 2 additions & 0 deletions cmd/sncli/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func NormalizePath(path string) string {
func processConfigPath(path string) string {
path = NormalizePath(path)
// check if path defines directory
// #nosec G703 -- path is an explicit operator-supplied --config value for
// a local CLI, already normalized/cleaned above; only stat'ed here.
if info, err := os.Stat(path); err == nil && info.IsDir() {
path = filepath.Join(path, defaultConfigFileName)
}
Expand Down
2 changes: 2 additions & 0 deletions sn-manager/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func ensureSupernodeInitialized() error {
userHome = os.Getenv("HOME")
}
supernodeConfigPath := filepath.Join(userHome, ".supernode", "config.yml")
// #nosec G703 -- path is derived from the operator's own home directory
// (os.UserHomeDir/$HOME), not from untrusted input, and is only stat'ed.
if _, err := os.Stat(supernodeConfigPath); os.IsNotExist(err) {
return fmt.Errorf("SuperNode not initialized. Please run 'sn-manager init' first to configure your validator keys and network settings")
}
Expand Down
2 changes: 2 additions & 0 deletions sn-manager/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ func runInit(cmd *cobra.Command, args []string) error {
userHome = os.Getenv("HOME")
}
supernodeConfigPath := filepath.Join(userHome, ".supernode", "config.yml")
// #nosec G703 -- path is derived from the operator's own home directory
// (os.UserHomeDir/$HOME), not from untrusted input, and is only stat'ed.
if _, err := os.Stat(supernodeConfigPath); err == nil {
fmt.Println("✓ SuperNode already initialized, skipping initialization")
} else {
Expand Down
Loading
Loading