Skip to content

Commit 48f95cf

Browse files
ci and hooks
1 parent 961baa1 commit 48f95cf

9 files changed

Lines changed: 2412 additions & 1234 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
/goenv
1717
goenv-*
1818
*.exe
19+
coverage.out
20+
*.out
1921

2022
// No docs at root
2123
/*.md

cmd/compliance/sbom_ci.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package compliance
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"github.com/go-nv/goenv/internal/sbom"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var (
14+
ciCheckSBOMPath string
15+
ciCheckMaxAge string
16+
ciCheckJSON bool
17+
18+
ciScanScanner string
19+
ciScanSeverity string
20+
ciScanOutputJSON string
21+
ciScanOutputSARIF string
22+
ciScanFailOn string
23+
)
24+
25+
var sbomCICmd = &cobra.Command{
26+
Use: "ci",
27+
Short: "CI/CD pipeline integration commands",
28+
Long: `Commands designed for CI/CD pipeline integration.
29+
30+
These commands are optimized for continuous integration environments,
31+
providing machine-readable output and proper exit codes for pipeline control.
32+
33+
Supported CI/CD platforms:
34+
- GitHub Actions
35+
- GitLab CI
36+
- CircleCI
37+
- Jenkins
38+
- Azure Pipelines
39+
40+
Examples:
41+
# Check if SBOM exists and is up-to-date
42+
goenv sbom ci check
43+
44+
# Check with custom SBOM path
45+
goenv sbom ci check --sbom sbom.cyclonedx.json
46+
47+
# Run vulnerability scan
48+
goenv sbom ci scan --scanner grype
49+
50+
# Scan with severity threshold
51+
goenv sbom ci scan --scanner trivy --fail-on high`,
52+
}
53+
54+
var sbomCICheckCmd = &cobra.Command{
55+
Use: "check",
56+
Short: "Check SBOM existence and staleness",
57+
Long: `Check if SBOM exists and is up-to-date in CI/CD pipeline.
58+
59+
This command:
60+
- Verifies SBOM file exists
61+
- Checks if SBOM is stale (older than go.mod/go.sum)
62+
- Optionally checks maximum age
63+
- Outputs CI platform-specific annotations
64+
- Returns exit code 1 if checks fail
65+
66+
The command automatically detects the CI/CD platform and formats
67+
output accordingly (GitHub Actions annotations, GitLab CI format, etc.)`,
68+
RunE: runCICheck,
69+
}
70+
71+
var sbomCIScanCmd = &cobra.Command{
72+
Use: "scan",
73+
Short: "Run vulnerability scan in CI/CD",
74+
Long: `Run vulnerability scanner and format output for CI/CD.
75+
76+
This command:
77+
- Runs vulnerability scanner (Grype, Trivy, Snyk, etc.)
78+
- Formats output for CI platform (annotations, reports)
79+
- Exports results in JSON or SARIF format
80+
- Returns exit code based on severity threshold
81+
- Integrates with GitHub Security tab (SARIF upload)
82+
83+
The scanner must be installed and available in PATH.`,
84+
RunE: runCIScan,
85+
}
86+
87+
func init() {
88+
// Add CI subcommands
89+
sbomCICmd.AddCommand(sbomCICheckCmd)
90+
sbomCICmd.AddCommand(sbomCIScanCmd)
91+
92+
// Check flags
93+
sbomCICheckCmd.Flags().StringVar(&ciCheckSBOMPath, "sbom", "", "Path to SBOM file (auto-detected if not specified)")
94+
sbomCICheckCmd.Flags().StringVar(&ciCheckMaxAge, "max-age", "", "Maximum age for SBOM (e.g., '24h', '7d')")
95+
sbomCICheckCmd.Flags().BoolVar(&ciCheckJSON, "json", false, "Output results as JSON")
96+
97+
// Scan flags
98+
sbomCIScanCmd.Flags().StringVar(&ciCheckSBOMPath, "sbom", "", "Path to SBOM file (auto-detected if not specified)")
99+
sbomCIScanCmd.Flags().StringVar(&ciScanScanner, "scanner", "grype", "Scanner to use (grype, trivy, snyk, veracode)")
100+
sbomCIScanCmd.Flags().StringVar(&ciScanSeverity, "fail-on", "high", "Fail on severity level (critical, high, medium, low)")
101+
sbomCIScanCmd.Flags().StringVar(&ciScanOutputJSON, "output-json", "", "Write JSON results to file")
102+
sbomCIScanCmd.Flags().StringVar(&ciScanOutputSARIF, "output-sarif", "", "Write SARIF results to file (for GitHub Code Scanning)")
103+
104+
// Add to parent
105+
sbomCmd.AddCommand(sbomCICmd)
106+
}
107+
108+
func runCICheck(cmd *cobra.Command, args []string) error {
109+
// Create CI checker
110+
checker := sbom.NewCIChecker("")
111+
112+
// Parse max age if specified
113+
var maxAge time.Duration
114+
if ciCheckMaxAge != "" {
115+
var err error
116+
maxAge, err = time.ParseDuration(ciCheckMaxAge)
117+
if err != nil {
118+
return fmt.Errorf("invalid max-age format: %w (use format like '24h', '7d')", err)
119+
}
120+
}
121+
122+
// Run check
123+
result, err := checker.CheckSBOM(ciCheckSBOMPath, maxAge)
124+
if err != nil {
125+
return fmt.Errorf("SBOM check failed: %w", err)
126+
}
127+
128+
// Output results
129+
if ciCheckJSON {
130+
// JSON output
131+
data, err := json.MarshalIndent(result, "", " ")
132+
if err != nil {
133+
return fmt.Errorf("failed to marshal JSON: %w", err)
134+
}
135+
fmt.Println(string(data))
136+
} else {
137+
// CI platform-specific output
138+
output := checker.FormatCIOutput(result)
139+
fmt.Print(output)
140+
141+
// Also print summary for human readers
142+
if !result.Passed {
143+
fmt.Println()
144+
if !result.SBOMExists {
145+
fmt.Println("SBOM file not found. Please generate one with:")
146+
fmt.Println(" goenv sbom generate")
147+
} else if result.IsStale {
148+
fmt.Printf("SBOM is stale: %s\n", result.StaleReason)
149+
fmt.Println("Please regenerate with:")
150+
fmt.Println(" goenv sbom generate")
151+
}
152+
}
153+
}
154+
155+
// Exit with error code if check failed
156+
if !result.Passed {
157+
os.Exit(1)
158+
}
159+
160+
return nil
161+
}
162+
163+
func runCIScan(cmd *cobra.Command, args []string) error {
164+
// Create CI checker
165+
checker := sbom.NewCIChecker("")
166+
167+
// Detect CI platform
168+
platform := sbom.DetectCIPlatform()
169+
fmt.Printf("Detected CI platform: %s\n", platform)
170+
171+
// Find or check SBOM
172+
sbomPath := ciCheckSBOMPath
173+
if sbomPath == "" {
174+
// Auto-detect
175+
candidates := []string{
176+
"sbom.json",
177+
"sbom.cyclonedx.json",
178+
"sbom.spdx.json",
179+
}
180+
181+
for _, candidate := range candidates {
182+
if _, err := os.Stat(candidate); err == nil {
183+
sbomPath = candidate
184+
break
185+
}
186+
}
187+
188+
if sbomPath == "" {
189+
return fmt.Errorf("SBOM file not found. Generate with: goenv sbom generate")
190+
}
191+
}
192+
193+
fmt.Printf("Scanning SBOM: %s\n", sbomPath)
194+
fmt.Printf("Scanner: %s\n", ciScanScanner)
195+
fmt.Println()
196+
197+
// Configure scan options
198+
scanOptions := sbom.ScanOptions{
199+
SBOMPath: sbomPath,
200+
FailOn: ciScanSeverity,
201+
Format: "json",
202+
Verbose: false,
203+
}
204+
205+
// Run scan
206+
result, err := checker.RunScanner(sbomPath, ciScanScanner, scanOptions)
207+
if err != nil {
208+
return fmt.Errorf("scan failed: %w", err)
209+
}
210+
211+
// Output formatted results
212+
output := checker.FormatScanOutput(result)
213+
fmt.Print(output)
214+
215+
// Write JSON output if requested
216+
if ciScanOutputJSON != "" {
217+
fmt.Printf("\nWriting JSON results to: %s\n", ciScanOutputJSON)
218+
if err := checker.WriteScanResultToFile(result, ciScanOutputJSON); err != nil {
219+
return fmt.Errorf("failed to write JSON output: %w", err)
220+
}
221+
}
222+
223+
// Write SARIF output if requested (for GitHub Code Scanning)
224+
if ciScanOutputSARIF != "" {
225+
fmt.Printf("Writing SARIF results to: %s\n", ciScanOutputSARIF)
226+
if err := checker.ExportToGitHubSARIF(result, ciScanOutputSARIF); err != nil {
227+
return fmt.Errorf("failed to write SARIF output: %w", err)
228+
}
229+
230+
if platform == sbom.PlatformGitHubActions {
231+
fmt.Println("\nTo upload to GitHub Security tab, add to your workflow:")
232+
fmt.Println(" - uses: github/codeql-action/upload-sarif@v2")
233+
fmt.Println(" with:")
234+
fmt.Printf(" sarif_file: %s\n", ciScanOutputSARIF)
235+
}
236+
}
237+
238+
// Exit with error code if scan failed
239+
if !result.Passed {
240+
fmt.Println("\n❌ Scan failed due to vulnerabilities exceeding threshold")
241+
os.Exit(1)
242+
}
243+
244+
return nil
245+
}

0 commit comments

Comments
 (0)