Skip to content

Commit 7fc4d8e

Browse files
authored
Merge pull request #1363 from LeanerCloud/fix/ci-security-gosec
fix(ci): self-install pinned gosec and resolve findings to green Security Scanning
2 parents f7392f7 + 556b788 commit 7fc4d8e

5 files changed

Lines changed: 72 additions & 17 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,35 @@ jobs:
318318
fi
319319
320320
- name: Run gosec Security Scanner
321-
uses: securego/gosec@4a3bd8af174872c778439083ded7adbf3747e770 # v2.26.1
322-
with:
323-
args: '-fmt sarif -out gosec-results.sarif ./...'
321+
run: |
322+
# Install pinned gosec using the job's existing setup-go (GO_VERSION 1.26.5).
323+
# The securego/gosec Docker action bundles its own Go toolchain (1.26.1) which
324+
# cannot satisfy the "go 1.26.5" module requirement in go.mod, causing it to
325+
# load 0 files and exit 1 with a toolchain mismatch rather than real findings.
326+
go install github.com/securego/gosec/v2/cmd/gosec@v2.26.1
327+
# Multi-module repo: each ./... only walks the current module so scanning root
328+
# alone silently misses pkg/ and providers/*. Mirror the govulncheck per-module
329+
# loop, collect per-module SARIF, then merge for the upload step.
330+
set -e
331+
for mod in . pkg providers/aws providers/azure providers/gcp tests/e2e; do
332+
tag=$(echo "$mod" | tr './' '--' | sed 's/^-/root/')
333+
out="$RUNNER_TEMP/gosec-${tag}.sarif"
334+
echo "==> gosec in $mod"
335+
(cd "$mod" && gosec -fmt sarif -out "$out" ./...)
336+
# Code scanning rejects a SARIF file whose runs share a category
337+
# (github.blog changelog 2025-07-21), so give each module's run a
338+
# unique automationDetails.id before merging.
339+
jq --arg id "gosec-${tag}/" '.runs |= map(.automationDetails = {id: $id})' \
340+
"$out" > "$out.tmp" && mv "$out.tmp" "$out"
341+
done
342+
# Merge per-module SARIF runs into one file for the upload step.
343+
# jq is preinstalled on the GitHub Ubuntu runner image (no new deps).
344+
jq -s '{version: "2.1.0", "$schema": "https://json.schemastore.org/sarif-2.1.0.json", runs: [.[].runs[]]}' \
345+
"$RUNNER_TEMP"/gosec-*.sarif > gosec-results.sarif
346+
echo "Merged $(jq '.runs | length' gosec-results.sarif) SARIF runs"
324347
325348
- name: Upload gosec results to GitHub Security
349+
if: always()
326350
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
327351
with:
328352
sarif_file: gosec-results.sarif
@@ -335,17 +359,48 @@ jobs:
335359
format: 'sarif'
336360
output: 'trivy-results.sarif'
337361
severity: 'CRITICAL,HIGH'
362+
# Pin the Trivy binary independently of the action SHA. v0.36.0 is
363+
# the latest trivy-action release but bundles Trivy v0.70.0, which
364+
# panics in adaptDefaultTags on terraform/environments/aws/main.tf
365+
# (null default_tags vars). Fixed in Trivy >= v0.72.0. The action
366+
# forwards this input to aquasecurity/setup-trivy, so both scan
367+
# steps run the same pinned binary. Keep both steps on this version.
368+
version: 'v0.72.0'
338369

339370
- name: Upload Trivy results to GitHub Security
340371
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
341372
with:
342373
sarif_file: 'trivy-results.sarif'
343374

344-
- name: Run tfsec (Terraform Security)
345-
uses: aquasecurity/tfsec-action@b466648d6e39e7c75324f25d83891162a721f2d6 # v1.0.3
375+
# Terraform IaC misconfiguration scanning. Replaces the deprecated
376+
# aquasecurity/tfsec-action, whose bundled HCL parser rejects Terraform
377+
# 1.5+ `check {}` blocks (e.g. terraform/modules/deployment-checks/main.tf)
378+
# with a hard "scan failed" parse error that soft_fail does not suppress
379+
# (soft_fail only downgrades findings, not scan errors). Trivy is tfsec's
380+
# official successor, parses `check {}` blocks, and (like the fs scan
381+
# above) uses the default exit-code 0 so misconfig findings are reported
382+
# to the Security tab without gating the job -- matching tfsec's prior
383+
# soft_fail: true behaviour while preserving Terraform IaC coverage.
384+
- name: Run Trivy IaC misconfiguration scanner (Terraform)
385+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
386+
with:
387+
scan-type: 'config'
388+
scan-ref: 'terraform/'
389+
format: 'sarif'
390+
output: 'trivy-config-results.sarif'
391+
severity: 'CRITICAL,HIGH'
392+
# Same pinned Trivy binary as the filesystem scan above (>= v0.72.0
393+
# avoids the adaptDefaultTags panic on null default_tags vars).
394+
version: 'v0.72.0'
395+
396+
- name: Upload Trivy IaC results to GitHub Security
397+
if: always()
398+
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
346399
with:
347-
working_directory: terraform/
348-
soft_fail: true
400+
sarif_file: 'trivy-config-results.sarif'
401+
# Distinct category so this IaC analysis does not overwrite the
402+
# filesystem Trivy analysis uploaded above (both report as "Trivy").
403+
category: 'trivy-iac'
349404

350405
# Snyk security scanning
351406
snyk-scan:

‎cmd/configure_azure.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func storeAzureCredentials(ctx context.Context, store SecretsStore, stackName st
130130
}
131131

132132
// Marshal credentials to JSON
133-
credJSON, err := json.Marshal(creds)
133+
credJSON, err := json.Marshal(creds) // #nosec G117 -- intentional: marshaling Azure credential struct (contains ClientSecret field) for secure storage in the credential store
134134
if err != nil {
135135
return fmt.Errorf("failed to marshal credentials: %w", err)
136136
}
@@ -341,7 +341,7 @@ func createAzureServicePrincipal(reader *bufio.Reader, subscriptionID string) er
341341
if choice == "r" || choice == "run" || choice == "" {
342342
fmt.Println()
343343
fmt.Println(strings.Repeat("-", 60))
344-
cmd := exec.Command("az", "ad", "sp", "create-for-rbac",
344+
cmd := exec.Command("az", "ad", "sp", "create-for-rbac", // #nosec G204 -- binary "az" is hardcoded; subscriptionID validated by validateAzureUUID before exec
345345
"--name", "CUDly",
346346
"--role", "Reservations Administrator",
347347
"--scopes", fmt.Sprintf("/subscriptions/%s", subscriptionID))
@@ -401,7 +401,7 @@ func executeExplicitCommand(reader *bufio.Reader, displayCmd string, program str
401401
fmt.Printf("Executing: %s\n", displayCmd)
402402
fmt.Println(strings.Repeat("-", 60))
403403

404-
cmd := exec.Command(program, args...)
404+
cmd := exec.Command(program, args...) // #nosec G204 -- configure CLI tool; program is always "az" (Azure CLI) per all callers; no user input reaches this function
405405
cmd.Stdout = os.Stdout
406406
cmd.Stderr = os.Stderr
407407
cmd.Stdin = os.Stdin

‎cmd/configure_gcp.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func loadAWSConfigForGCP(ctx context.Context) (aws.Config, error) {
210210
func loadAndUpdateGCPCredentials(credsFile string) (GCPCredentials, []byte, error) {
211211
expandedPath := expandHomeDirectory(credsFile)
212212

213-
credsData, err := os.ReadFile(expandedPath)
213+
credsData, err := os.ReadFile(expandedPath) // #nosec G304 -- GCP credentials file path is operator-supplied via CLI argument; operator controls the value
214214
if err != nil {
215215
return GCPCredentials{}, nil, fmt.Errorf("failed to read credentials file: %w", err)
216216
}
@@ -222,7 +222,7 @@ func loadAndUpdateGCPCredentials(credsFile string) (GCPCredentials, []byte, erro
222222

223223
if gcpOpts.ProjectID != "" {
224224
creds.ProjectID = gcpOpts.ProjectID
225-
credsData, err = json.Marshal(creds)
225+
credsData, err = json.Marshal(creds) // #nosec G117 -- intentional: marshaling GCP credential struct (contains PrivateKey field) for secure storage in the credential store
226226
if err != nil {
227227
return GCPCredentials{}, nil, fmt.Errorf("failed to marshal updated credentials: %w", err)
228228
}
@@ -289,7 +289,7 @@ func runGCPSetupCommands(reader *bufio.Reader) (string, error) {
289289
// Set the project - use exec.Command with arguments instead of shell
290290
fmt.Println()
291291
fmt.Println("Setting project...")
292-
cmd := exec.Command("gcloud", "config", "set", "project", projectID)
292+
cmd := exec.Command("gcloud", "config", "set", "project", projectID) // #nosec G204 -- binary "gcloud" is hardcoded; projectID validated by validateGCPProjectID before exec
293293
cmd.Stdout = os.Stdout
294294
cmd.Stderr = os.Stderr
295295
if err := cmd.Run(); err != nil {
@@ -407,7 +407,7 @@ func executeGCPCommand(reader *bufio.Reader, displayCmd string, program string,
407407
fmt.Printf("Executing: %s\n", displayCmd)
408408
fmt.Println(strings.Repeat("-", 60))
409409

410-
cmd := exec.Command(program, args...)
410+
cmd := exec.Command(program, args...) // #nosec G204 -- configure CLI tool; program is always "gcloud" per all callers; no user input reaches this function
411411
cmd.Stdout = os.Stdout
412412
cmd.Stderr = os.Stderr
413413
cmd.Stdin = os.Stdin

‎cmd/helpers.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ func ConfirmPurchase(totalInstances int, totalSavings float64, skipConfirmation
533533
// CheckAuditLogWritable opens the audit log file in append mode to verify it is writable.
534534
// Returns an error if the path cannot be opened for writing.
535535
func CheckAuditLogWritable(path string) error {
536-
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
536+
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) // #nosec G304 -- audit log path is operator-configured; value is not reachable from user input
537537
if err != nil {
538538
return fmt.Errorf("audit log %q not writable: %w", path, err)
539539
}

‎cmd/multi_service_csv.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func determineCSVCoverage(cfg Config) float64 {
2727

2828
// loadRecommendationsFromCSV reads and returns recommendations from a CSV file.
2929
func loadRecommendationsFromCSV(csvPath string) ([]common.Recommendation, error) {
30-
file, err := os.Open(csvPath)
30+
file, err := os.Open(csvPath) // #nosec G304 -- CLI tool: csvPath is an operator-supplied command-line argument
3131
if err != nil {
3232
return nil, fmt.Errorf("failed to open CSV file: %w", err)
3333
}
@@ -195,7 +195,7 @@ func writeMultiServiceCSVReport(results []common.PurchaseResult, filepath string
195195
return nil
196196
}
197197

198-
file, err := os.Create(filepath)
198+
file, err := os.Create(filepath) // #nosec G304 -- CLI tool: filepath is an operator-supplied output path argument
199199
if err != nil {
200200
return fmt.Errorf("failed to create CSV file: %w", err)
201201
}

0 commit comments

Comments
 (0)