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
72 changes: 54 additions & 18 deletions cli/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
noTrivyFlag bool
noGitleaksFlag bool
wizardFlag bool
ciFlag string
)

var initCmd = &cobra.Command{
Expand Down Expand Up @@ -63,35 +64,40 @@ or run interactively with --wizard.
severity = "high"
}

// Normalize CI provider
ci := strings.ToLower(ciFlag)
switch ci {
case "github", "gitlab", "bitbucket":
// ok
default:
if ci != "" {
fmt.Printf("⚠️ Unknown CI provider '%s', defaulting to 'github'\n", ciFlag)
}
ci = "github"
}

cfg := &generators.InitConfig{
Project: project,
SeverityThreshold: severity,
CIProvider: ci,
Tools: generators.ToolsConfig{
Semgrep: !noSemgrepFlag,
Trivy: !noTrivyFlag,
Gitleaks: !noGitleaksFlag,
},
}

// Ensure .github/workflows exists
wfDir := filepath.Join(dir, ".github", "workflows")
if err := os.MkdirAll(wfDir, 0o755); err != nil {
return fmt.Errorf("failed to create workflows directory: %w", err)
}

fmt.Println("⚙️ Generating workflow + config files...")

if err := generators.GenerateGithubActions(cfg); err != nil {
if err := generateCIWorkflow(cfg, dir); err != nil {
return err
}
if err := generators.GenerateSecurityConfig(cfg); err != nil {
return err
}

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

//
Expand Down Expand Up @@ -161,25 +168,28 @@ func runInitWizard() error {
return nil
}

// Select CI provider
fmt.Println("\n🔧 Select CI provider:")
ciProvider := askChoice(reader, "github | gitlab | bitbucket [default: github]: ",
[]string{"github", "gitlab", "bitbucket"},
"github",
)

// Generate config
cfg := &generators.InitConfig{
Project: project,
SeverityThreshold: severity,
CIProvider: ciProvider,
Tools: generators.ToolsConfig{
Semgrep: enableSemgrep,
Gitleaks: enableGitleaks,
Trivy: enableTrivy,
},
}

wfDir := filepath.Join(dir, ".github", "workflows")
if err := os.MkdirAll(wfDir, 0o755); err != nil {
return fmt.Errorf("failed to create workflows directory: %w", err)
}

fmt.Println("\n⚙️ Generating workflow + config files...")

if err := generators.GenerateGithubActions(cfg); err != nil {
if err := generateCIWorkflow(cfg, dir); err != nil {
return err
}
if err := generators.GenerateSecurityConfig(cfg); err != nil {
Expand All @@ -188,12 +198,38 @@ func runInitWizard() error {

fmt.Println("\n🎉 Setup complete!")
fmt.Println("Generated:")
fmt.Println(" - .github/workflows/security.yml")
fmt.Println(" - security-config.yml")
printGeneratedFiles(ciProvider)

return nil
}

func generateCIWorkflow(cfg *generators.InitConfig, dir string) error {
switch cfg.CIProvider {
case "gitlab":
return generators.GenerateGitLabCI(cfg)
case "bitbucket":
return generators.GenerateBitbucketPipelines(cfg)
default:
wfDir := filepath.Join(dir, ".github", "workflows")
if err := os.MkdirAll(wfDir, 0o755); err != nil {
return fmt.Errorf("failed to create workflows directory: %w", err)
}
return generators.GenerateGithubActions(cfg)
}
}

func printGeneratedFiles(ci string) {
switch ci {
case "gitlab":
fmt.Println(" - .gitlab-ci.yml")
case "bitbucket":
fmt.Println(" - bitbucket-pipelines.yml")
default:
fmt.Println(" - .github/workflows/security.yml")
}
fmt.Println(" - security-config.yml")
}

//
// -----------------------------
// HELPER FUNCTIONS
Expand Down
1 change: 1 addition & 0 deletions cli/generators/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ type InitConfig struct {
Tools ToolsConfig
ExcludePaths []string
FailOn map[string]int
CIProvider string // "github", "gitlab", "bitbucket"
}
44 changes: 44 additions & 0 deletions cli/generators/workflow_bitbucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package generators

import (
"fmt"
"os"
"text/template"

"github.com/edgarpsda/devsecops-kit/cli/templates"
)

func GenerateBitbucketPipelines(cfg *InitConfig) error {
var tmplName string

switch cfg.Project.Language {
case "nodejs":
tmplName = "ci/bitbucket/node_security.yml.tmpl"
case "golang":
tmplName = "ci/bitbucket/go_security.yml.tmpl"
case "python":
tmplName = "ci/bitbucket/python_security.yml.tmpl"
case "java":
tmplName = "ci/bitbucket/java_security.yml.tmpl"
default:
return fmt.Errorf("no Bitbucket Pipelines template for language: %s", cfg.Project.Language)
}

tmplData, err := templates.TemplateFS.ReadFile(tmplName)
if err != nil {
return fmt.Errorf("failed reading embedded template %s: %w", tmplName, err)
}

tmpl, err := template.New("bitbucket-pipelines").Parse(string(tmplData))
if err != nil {
return fmt.Errorf("failed parsing template: %w", err)
}

f, err := os.Create("bitbucket-pipelines.yml")
if err != nil {
return fmt.Errorf("failed to create bitbucket-pipelines.yml: %w", err)
}
defer f.Close()

return tmpl.Execute(f, cfg)
}
44 changes: 44 additions & 0 deletions cli/generators/workflow_gitlab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package generators

import (
"fmt"
"os"
"text/template"

"github.com/edgarpsda/devsecops-kit/cli/templates"
)

func GenerateGitLabCI(cfg *InitConfig) error {
var tmplName string

switch cfg.Project.Language {
case "nodejs":
tmplName = "ci/gitlab/node_security.yml.tmpl"
case "golang":
tmplName = "ci/gitlab/go_security.yml.tmpl"
case "python":
tmplName = "ci/gitlab/python_security.yml.tmpl"
case "java":
tmplName = "ci/gitlab/java_security.yml.tmpl"
default:
return fmt.Errorf("no GitLab CI template for language: %s", cfg.Project.Language)
}

tmplData, err := templates.TemplateFS.ReadFile(tmplName)
if err != nil {
return fmt.Errorf("failed reading embedded template %s: %w", tmplName, err)
}

tmpl, err := template.New("gitlab-ci").Parse(string(tmplData))
if err != nil {
return fmt.Errorf("failed parsing template: %w", err)
}

f, err := os.Create(".gitlab-ci.yml")
if err != nil {
return fmt.Errorf("failed to create .gitlab-ci.yml: %w", err)
}
defer f.Close()

return tmpl.Execute(f, cfg)
}
132 changes: 132 additions & 0 deletions cli/generators/workflow_gitlab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package generators_test

import (
"os"
"strings"
"testing"

"github.com/edgarpsda/devsecops-kit/cli/detectors"
"github.com/edgarpsda/devsecops-kit/cli/generators"
)

func TestGenerateGitLabCI(t *testing.T) {
languages := []string{"nodejs", "golang", "python", "java"}

for _, lang := range languages {
t.Run(lang, func(t *testing.T) {
dir := t.TempDir()
orig, _ := os.Getwd()
defer os.Chdir(orig)
os.Chdir(dir)

cfg := &generators.InitConfig{
Project: &detectors.ProjectInfo{
Language: lang,
Framework: "test",
},
CIProvider: "gitlab",
SeverityThreshold: "high",
Tools: generators.ToolsConfig{
Semgrep: true,
Trivy: true,
Gitleaks: true,
},
}

err := generators.GenerateGitLabCI(cfg)
if err != nil {
t.Fatalf("GenerateGitLabCI(%s) failed: %v", lang, err)
}

data, err := os.ReadFile(".gitlab-ci.yml")
if err != nil {
t.Fatalf("failed to read .gitlab-ci.yml: %v", err)
}

content := string(data)
if !strings.Contains(content, "stages:") {
t.Error("expected 'stages:' in .gitlab-ci.yml")
}
if !strings.Contains(content, "security-scan:") {
t.Error("expected 'security-scan:' job in .gitlab-ci.yml")
}
if !strings.Contains(content, "semgrep") {
t.Error("expected semgrep step in .gitlab-ci.yml")
}
if !strings.Contains(content, "gitleaks") {
t.Error("expected gitleaks step in .gitlab-ci.yml")
}
if !strings.Contains(content, "trivy") {
t.Error("expected trivy step in .gitlab-ci.yml")
}
})
}
}

func TestGenerateBitbucketPipelines(t *testing.T) {
languages := []string{"nodejs", "golang", "python", "java"}

for _, lang := range languages {
t.Run(lang, func(t *testing.T) {
dir := t.TempDir()
orig, _ := os.Getwd()
defer os.Chdir(orig)
os.Chdir(dir)

cfg := &generators.InitConfig{
Project: &detectors.ProjectInfo{
Language: lang,
Framework: "test",
},
CIProvider: "bitbucket",
SeverityThreshold: "high",
Tools: generators.ToolsConfig{
Semgrep: true,
Trivy: true,
Gitleaks: true,
},
}

err := generators.GenerateBitbucketPipelines(cfg)
if err != nil {
t.Fatalf("GenerateBitbucketPipelines(%s) failed: %v", lang, err)
}

data, err := os.ReadFile("bitbucket-pipelines.yml")
if err != nil {
t.Fatalf("failed to read bitbucket-pipelines.yml: %v", err)
}

content := string(data)
if !strings.Contains(content, "pipelines:") {
t.Error("expected 'pipelines:' in bitbucket-pipelines.yml")
}
if !strings.Contains(content, "Security Scan") {
t.Error("expected 'Security Scan' step in bitbucket-pipelines.yml")
}
if !strings.Contains(content, "semgrep") {
t.Error("expected semgrep step in bitbucket-pipelines.yml")
}
})
}
}

func TestUnsupportedCILanguage(t *testing.T) {
dir := t.TempDir()
orig, _ := os.Getwd()
defer os.Chdir(orig)
os.Chdir(dir)

cfg := &generators.InitConfig{
Project: &detectors.ProjectInfo{
Language: "ruby",
Framework: "rails",
},
CIProvider: "gitlab",
}

err := generators.GenerateGitLabCI(cfg)
if err == nil {
t.Error("expected error for unsupported language, got nil")
}
}
Loading
Loading