Skip to content

Commit c1e1031

Browse files
authored
Merge pull request #10 from EdgarPsda/v0.6.0/multi-ci-support
Add multi-CI support for GitLab CI and Bitbucket Pipelines
2 parents 80d1f48 + 0525459 commit c1e1031

14 files changed

Lines changed: 1333 additions & 18 deletions

cli/cmd/init.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
noTrivyFlag bool
1919
noGitleaksFlag bool
2020
wizardFlag bool
21+
ciFlag string
2122
)
2223

2324
var initCmd = &cobra.Command{
@@ -63,35 +64,40 @@ or run interactively with --wizard.
6364
severity = "high"
6465
}
6566

67+
// Normalize CI provider
68+
ci := strings.ToLower(ciFlag)
69+
switch ci {
70+
case "github", "gitlab", "bitbucket":
71+
// ok
72+
default:
73+
if ci != "" {
74+
fmt.Printf("⚠️ Unknown CI provider '%s', defaulting to 'github'\n", ciFlag)
75+
}
76+
ci = "github"
77+
}
78+
6679
cfg := &generators.InitConfig{
6780
Project: project,
6881
SeverityThreshold: severity,
82+
CIProvider: ci,
6983
Tools: generators.ToolsConfig{
7084
Semgrep: !noSemgrepFlag,
7185
Trivy: !noTrivyFlag,
7286
Gitleaks: !noGitleaksFlag,
7387
},
7488
}
7589

76-
// Ensure .github/workflows exists
77-
wfDir := filepath.Join(dir, ".github", "workflows")
78-
if err := os.MkdirAll(wfDir, 0o755); err != nil {
79-
return fmt.Errorf("failed to create workflows directory: %w", err)
80-
}
81-
8290
fmt.Println("⚙️ Generating workflow + config files...")
8391

84-
if err := generators.GenerateGithubActions(cfg); err != nil {
92+
if err := generateCIWorkflow(cfg, dir); err != nil {
8593
return err
8694
}
8795
if err := generators.GenerateSecurityConfig(cfg); err != nil {
8896
return err
8997
}
9098

9199
fmt.Println("\n🎉 Done! DevSecOps Kit initialized.")
92-
fmt.Println("Files created:")
93-
fmt.Println(" - .github/workflows/security.yml")
94-
fmt.Println(" - security-config.yml")
100+
printGeneratedFiles(ci)
95101
return nil
96102
},
97103
}
@@ -104,6 +110,7 @@ func init() {
104110
initCmd.Flags().BoolVar(&noTrivyFlag, "no-trivy", false, "Disable Trivy in generated workflow")
105111
initCmd.Flags().BoolVar(&noGitleaksFlag, "no-gitleaks", false, "Disable Gitleaks in generated workflow")
106112
initCmd.Flags().BoolVar(&wizardFlag, "wizard", false, "Run interactive guided setup")
113+
initCmd.Flags().StringVar(&ciFlag, "ci", "github", "CI provider: github, gitlab, bitbucket")
107114
}
108115

109116
//
@@ -161,25 +168,28 @@ func runInitWizard() error {
161168
return nil
162169
}
163170

171+
// Select CI provider
172+
fmt.Println("\n🔧 Select CI provider:")
173+
ciProvider := askChoice(reader, "github | gitlab | bitbucket [default: github]: ",
174+
[]string{"github", "gitlab", "bitbucket"},
175+
"github",
176+
)
177+
164178
// Generate config
165179
cfg := &generators.InitConfig{
166180
Project: project,
167181
SeverityThreshold: severity,
182+
CIProvider: ciProvider,
168183
Tools: generators.ToolsConfig{
169184
Semgrep: enableSemgrep,
170185
Gitleaks: enableGitleaks,
171186
Trivy: enableTrivy,
172187
},
173188
}
174189

175-
wfDir := filepath.Join(dir, ".github", "workflows")
176-
if err := os.MkdirAll(wfDir, 0o755); err != nil {
177-
return fmt.Errorf("failed to create workflows directory: %w", err)
178-
}
179-
180190
fmt.Println("\n⚙️ Generating workflow + config files...")
181191

182-
if err := generators.GenerateGithubActions(cfg); err != nil {
192+
if err := generateCIWorkflow(cfg, dir); err != nil {
183193
return err
184194
}
185195
if err := generators.GenerateSecurityConfig(cfg); err != nil {
@@ -188,12 +198,38 @@ func runInitWizard() error {
188198

189199
fmt.Println("\n🎉 Setup complete!")
190200
fmt.Println("Generated:")
191-
fmt.Println(" - .github/workflows/security.yml")
192-
fmt.Println(" - security-config.yml")
201+
printGeneratedFiles(ciProvider)
193202

194203
return nil
195204
}
196205

206+
func generateCIWorkflow(cfg *generators.InitConfig, dir string) error {
207+
switch cfg.CIProvider {
208+
case "gitlab":
209+
return generators.GenerateGitLabCI(cfg)
210+
case "bitbucket":
211+
return generators.GenerateBitbucketPipelines(cfg)
212+
default:
213+
wfDir := filepath.Join(dir, ".github", "workflows")
214+
if err := os.MkdirAll(wfDir, 0o755); err != nil {
215+
return fmt.Errorf("failed to create workflows directory: %w", err)
216+
}
217+
return generators.GenerateGithubActions(cfg)
218+
}
219+
}
220+
221+
func printGeneratedFiles(ci string) {
222+
switch ci {
223+
case "gitlab":
224+
fmt.Println(" - .gitlab-ci.yml")
225+
case "bitbucket":
226+
fmt.Println(" - bitbucket-pipelines.yml")
227+
default:
228+
fmt.Println(" - .github/workflows/security.yml")
229+
}
230+
fmt.Println(" - security-config.yml")
231+
}
232+
197233
//
198234
// -----------------------------
199235
// HELPER FUNCTIONS

cli/generators/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ type InitConfig struct {
1515
Tools ToolsConfig
1616
ExcludePaths []string
1717
FailOn map[string]int
18+
CIProvider string // "github", "gitlab", "bitbucket"
1819
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package generators
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"text/template"
7+
8+
"github.com/edgarpsda/devsecops-kit/cli/templates"
9+
)
10+
11+
func GenerateBitbucketPipelines(cfg *InitConfig) error {
12+
var tmplName string
13+
14+
switch cfg.Project.Language {
15+
case "nodejs":
16+
tmplName = "ci/bitbucket/node_security.yml.tmpl"
17+
case "golang":
18+
tmplName = "ci/bitbucket/go_security.yml.tmpl"
19+
case "python":
20+
tmplName = "ci/bitbucket/python_security.yml.tmpl"
21+
case "java":
22+
tmplName = "ci/bitbucket/java_security.yml.tmpl"
23+
default:
24+
return fmt.Errorf("no Bitbucket Pipelines template for language: %s", cfg.Project.Language)
25+
}
26+
27+
tmplData, err := templates.TemplateFS.ReadFile(tmplName)
28+
if err != nil {
29+
return fmt.Errorf("failed reading embedded template %s: %w", tmplName, err)
30+
}
31+
32+
tmpl, err := template.New("bitbucket-pipelines").Parse(string(tmplData))
33+
if err != nil {
34+
return fmt.Errorf("failed parsing template: %w", err)
35+
}
36+
37+
f, err := os.Create("bitbucket-pipelines.yml")
38+
if err != nil {
39+
return fmt.Errorf("failed to create bitbucket-pipelines.yml: %w", err)
40+
}
41+
defer f.Close()
42+
43+
return tmpl.Execute(f, cfg)
44+
}

cli/generators/workflow_gitlab.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package generators
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"text/template"
7+
8+
"github.com/edgarpsda/devsecops-kit/cli/templates"
9+
)
10+
11+
func GenerateGitLabCI(cfg *InitConfig) error {
12+
var tmplName string
13+
14+
switch cfg.Project.Language {
15+
case "nodejs":
16+
tmplName = "ci/gitlab/node_security.yml.tmpl"
17+
case "golang":
18+
tmplName = "ci/gitlab/go_security.yml.tmpl"
19+
case "python":
20+
tmplName = "ci/gitlab/python_security.yml.tmpl"
21+
case "java":
22+
tmplName = "ci/gitlab/java_security.yml.tmpl"
23+
default:
24+
return fmt.Errorf("no GitLab CI template for language: %s", cfg.Project.Language)
25+
}
26+
27+
tmplData, err := templates.TemplateFS.ReadFile(tmplName)
28+
if err != nil {
29+
return fmt.Errorf("failed reading embedded template %s: %w", tmplName, err)
30+
}
31+
32+
tmpl, err := template.New("gitlab-ci").Parse(string(tmplData))
33+
if err != nil {
34+
return fmt.Errorf("failed parsing template: %w", err)
35+
}
36+
37+
f, err := os.Create(".gitlab-ci.yml")
38+
if err != nil {
39+
return fmt.Errorf("failed to create .gitlab-ci.yml: %w", err)
40+
}
41+
defer f.Close()
42+
43+
return tmpl.Execute(f, cfg)
44+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package generators_test
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
8+
"github.com/edgarpsda/devsecops-kit/cli/detectors"
9+
"github.com/edgarpsda/devsecops-kit/cli/generators"
10+
)
11+
12+
func TestGenerateGitLabCI(t *testing.T) {
13+
languages := []string{"nodejs", "golang", "python", "java"}
14+
15+
for _, lang := range languages {
16+
t.Run(lang, func(t *testing.T) {
17+
dir := t.TempDir()
18+
orig, _ := os.Getwd()
19+
defer os.Chdir(orig)
20+
os.Chdir(dir)
21+
22+
cfg := &generators.InitConfig{
23+
Project: &detectors.ProjectInfo{
24+
Language: lang,
25+
Framework: "test",
26+
},
27+
CIProvider: "gitlab",
28+
SeverityThreshold: "high",
29+
Tools: generators.ToolsConfig{
30+
Semgrep: true,
31+
Trivy: true,
32+
Gitleaks: true,
33+
},
34+
}
35+
36+
err := generators.GenerateGitLabCI(cfg)
37+
if err != nil {
38+
t.Fatalf("GenerateGitLabCI(%s) failed: %v", lang, err)
39+
}
40+
41+
data, err := os.ReadFile(".gitlab-ci.yml")
42+
if err != nil {
43+
t.Fatalf("failed to read .gitlab-ci.yml: %v", err)
44+
}
45+
46+
content := string(data)
47+
if !strings.Contains(content, "stages:") {
48+
t.Error("expected 'stages:' in .gitlab-ci.yml")
49+
}
50+
if !strings.Contains(content, "security-scan:") {
51+
t.Error("expected 'security-scan:' job in .gitlab-ci.yml")
52+
}
53+
if !strings.Contains(content, "semgrep") {
54+
t.Error("expected semgrep step in .gitlab-ci.yml")
55+
}
56+
if !strings.Contains(content, "gitleaks") {
57+
t.Error("expected gitleaks step in .gitlab-ci.yml")
58+
}
59+
if !strings.Contains(content, "trivy") {
60+
t.Error("expected trivy step in .gitlab-ci.yml")
61+
}
62+
})
63+
}
64+
}
65+
66+
func TestGenerateBitbucketPipelines(t *testing.T) {
67+
languages := []string{"nodejs", "golang", "python", "java"}
68+
69+
for _, lang := range languages {
70+
t.Run(lang, func(t *testing.T) {
71+
dir := t.TempDir()
72+
orig, _ := os.Getwd()
73+
defer os.Chdir(orig)
74+
os.Chdir(dir)
75+
76+
cfg := &generators.InitConfig{
77+
Project: &detectors.ProjectInfo{
78+
Language: lang,
79+
Framework: "test",
80+
},
81+
CIProvider: "bitbucket",
82+
SeverityThreshold: "high",
83+
Tools: generators.ToolsConfig{
84+
Semgrep: true,
85+
Trivy: true,
86+
Gitleaks: true,
87+
},
88+
}
89+
90+
err := generators.GenerateBitbucketPipelines(cfg)
91+
if err != nil {
92+
t.Fatalf("GenerateBitbucketPipelines(%s) failed: %v", lang, err)
93+
}
94+
95+
data, err := os.ReadFile("bitbucket-pipelines.yml")
96+
if err != nil {
97+
t.Fatalf("failed to read bitbucket-pipelines.yml: %v", err)
98+
}
99+
100+
content := string(data)
101+
if !strings.Contains(content, "pipelines:") {
102+
t.Error("expected 'pipelines:' in bitbucket-pipelines.yml")
103+
}
104+
if !strings.Contains(content, "Security Scan") {
105+
t.Error("expected 'Security Scan' step in bitbucket-pipelines.yml")
106+
}
107+
if !strings.Contains(content, "semgrep") {
108+
t.Error("expected semgrep step in bitbucket-pipelines.yml")
109+
}
110+
})
111+
}
112+
}
113+
114+
func TestUnsupportedCILanguage(t *testing.T) {
115+
dir := t.TempDir()
116+
orig, _ := os.Getwd()
117+
defer os.Chdir(orig)
118+
os.Chdir(dir)
119+
120+
cfg := &generators.InitConfig{
121+
Project: &detectors.ProjectInfo{
122+
Language: "ruby",
123+
Framework: "rails",
124+
},
125+
CIProvider: "gitlab",
126+
}
127+
128+
err := generators.GenerateGitLabCI(cfg)
129+
if err == nil {
130+
t.Error("expected error for unsupported language, got nil")
131+
}
132+
}

0 commit comments

Comments
 (0)