Skip to content

DevSecOps Automation Scripts

This section outlines security automation scripts designed to build compliance checks, diff dependency SBOMs, and catch secrets during CI/CD execution.


1. CI/CD Security Gate Parser (pipeline-gate.go)

Purpose

Implements a CI/CD pipeline deployment security gate. Parses vulnerability reports and fails the build (exit code 1) if security thresholds (e.g. any Criticals, > 2 Highs, or hardcoded secrets) are violated.

Code Implementation

// Purpose: Implements a CI/CD pipeline deployment security gate. Parses vulnerability reports and fails the build (exit code 1) if security thresholds (e.g. any Criticals, > 2 Highs, or hardcoded secrets) are violated.
package main

import (
    "encoding/json"
    "fmt"
    "os"
)

// SecurityMetrics represents aggregate scan findings from different scanners.
type SecurityMetrics struct {
    BuildID        string          `json:"build_id"`
    SASTScan       VulnerabilitySet `json:"sast_scan"`
    DependencyScan VulnerabilitySet `json:"dependency_scan"`
    SecretScan     SecretFindings   `json:"secret_scan"`
    LicenseScan    LicenseFindings  `json:"license_scan"`
}

// VulnerabilitySet tracks severity counts.
type VulnerabilitySet struct {
    Critical int `json:"critical"`
    High     int `json:"high"`
    Medium   int `json:"medium"`
    Low      int `json:"low"`
}

// SecretFindings tracks detected secrets count.
type SecretFindings struct {
    LeakedSecrets int `json:"leaked_secrets"`
}

// LicenseFindings tracks copyleft license counts.
type LicenseFindings struct {
    CopyleftLicenses int `json:"copyleft_licenses"`
}

// GatePolicy defines the threshold limits for passing the build.
type GatePolicy struct {
    AllowCritical     bool
    MaxAllowedHighs   int
    BlockSecrets      bool
    BlockCopyleft     bool
}

// EvaluateGate checks metrics against the defined policy.
func EvaluateGate(metrics SecurityMetrics, policy GatePolicy) (bool, []string) {
    var failures []string

    // 1. Critical vulnerabilities check
    if !policy.AllowCritical {
        if metrics.SASTScan.Critical > 0 {
            failures = append(failures, fmt.Sprintf("SAST Scan contains %d CRITICAL vulnerability findings (Threshold: 0)", metrics.SASTScan.Critical))
        }
        if metrics.DependencyScan.Critical > 0 {
            failures = append(failures, fmt.Sprintf("Dependency Scan contains %d CRITICAL vulnerability findings (Threshold: 0)", metrics.DependencyScan.Critical))
        }
    }

    // 2. High vulnerabilities check
    if metrics.SASTScan.High > policy.MaxAllowedHighs {
        failures = append(failures, fmt.Sprintf("SAST Scan contains %d HIGH vulnerability findings (Threshold: %d)", metrics.SASTScan.High, policy.MaxAllowedHighs))
    }
    if metrics.DependencyScan.High > policy.MaxAllowedHighs {
        failures = append(failures, fmt.Sprintf("Dependency Scan contains %d HIGH vulnerability findings (Threshold: %d)", metrics.DependencyScan.High, policy.MaxAllowedHighs))
    }

    // 3. Secrets check
    if policy.BlockSecrets && metrics.SecretScan.LeakedSecrets > 0 {
        failures = append(failures, fmt.Sprintf("Secret Scanner detected %d hardcoded secrets in commit diff (Threshold: 0)", metrics.SecretScan.LeakedSecrets))
    }

    // 4. License check
    if policy.BlockCopyleft && metrics.LicenseScan.CopyleftLicenses > 0 {
        failures = append(failures, fmt.Sprintf("License Scanner detected %d libraries with Copyleft licenses (Threshold: 0)", metrics.LicenseScan.CopyleftLicenses))
    }

    return len(failures) == 0, failures
}

func main() {
    // Sample JSON output from scanners
    sampleReportJSON := `{
        "build_id": "job-run-849921",
        "sast_scan": {
            "critical": 0,
            "high": 3,
            "medium": 5,
            "low": 12
        },
        "dependency_scan": {
            "critical": 1,
            "high": 1,
            "medium": 4,
            "low": 8
        },
        "secret_scan": {
            "leaked_secrets": 0
        },
        "license_scan": {
            "copyleft_licenses": 1
        }
    }`

    fmt.Println("==================================================")
    fmt.Println("🛡️ DevSecOps CI/CD Deployment Security Gate Engine")
    fmt.Println("==================================================")

    var metrics SecurityMetrics
    if err := json.Unmarshal([]byte(sampleReportJSON), &metrics); err != nil {
        fmt.Printf("Error unmarshaling security metrics: %v\n", err)
        os.Exit(1)
    }

    // Default Org Security Policy
    policy := GatePolicy{
        AllowCritical:   false, // Fail if any Criticals exist
        MaxAllowedHighs: 2,     // Fail if > 2 Highs exist
        BlockSecrets:    true,  // Fail if any secrets leaked
        BlockCopyleft:   true,  // Fail on GPL/AGPL software
    }

    fmt.Printf("\nEvaluating Build: %s...\n", metrics.BuildID)
    passed, violations := EvaluateGate(metrics, policy)

    if passed {
        fmt.Println("\n✅ [PASS] All security gates successfully cleared. Continuing deployment pipeline.")
        os.Exit(0)
    } else {
        fmt.Println("\n❌ [FAIL] Build blocked by organizational security policy gates!")
        fmt.Println("Violations:")
        for _, violation := range violations {
            fmt.Printf("   - %s\n", violation)
        }
        // In a real pipeline, we exit 1 to cause the execution job runner to halt/fail
        fmt.Println("\n[INFO] Exiting with status code 1.")
        os.Exit(1)
    }
}

2. CycloneDX SBOM Digger (sbom-diff.go)

Purpose

Parses and diffs two CycloneDX Software Bill of Materials (SBOM) files in JSON format to identify new dependencies, version upgrades/downgrades, and license compliance updates.

Code Implementation

// Purpose: Parses and diffs two CycloneDX Software Bill of Materials (SBOM) files in JSON format to identify new dependencies, version upgrades/downgrades, and license compliance updates.
package main

import (
    "encoding/json"
    "fmt"
)

// CycloneDXSBOM represents a simplified CycloneDX SBOM structure.
type CycloneDXSBOM struct {
    BOMFormat    string      `json:"bomFormat"`
    SpecVersion  string      `json:"specVersion"`
    SerialNumber string      `json:"serialNumber"`
    Version      int         `json:"version"`
    Components   []Component `json:"components"`
}

// Component represents a software dependency in the SBOM.
type Component struct {
    Type       string          `json:"type"`
    Group      string          `json:"group,omitempty"`
    Name       string          `json:"name"`
    Version    string          `json:"version"`
    PURL       string          `json:"purl,omitempty"`
    Licenses   []LicenseChoice `json:"licenses,omitempty"`
}

// LicenseChoice represents license definition wrappers in CycloneDX.
type LicenseChoice struct {
    License LicenseDetails `json:"license"`
}

// LicenseDetails holds the license ID or name.
type LicenseDetails struct {
    ID   string `json:"id,omitempty"` // e.g. "MIT", "Apache-2.0", "GPL-3.0-only"
    Name string `json:"name,omitempty"`
}

// Helper to get unique key for a component.
func componentKey(c Component) string {
    group := c.Group
    if group == "" {
        group = "default"
    }
    return group + ":" + c.Name
}

func main() {
    // Simulated SBOM A (Previous version)
    sbomAJSON := `{
        "bomFormat": "CycloneDX",
        "specVersion": "1.4",
        "version": 1,
        "components": [
            {
                "type": "library",
                "group": "github.com/gin-gonic",
                "name": "gin",
                "version": "v1.8.0",
                "purl": "pkg:golang/github.com/gin-gonic/gin@v1.8.0",
                "licenses": [{"license": {"id": "MIT"}}]
            },
            {
                "type": "library",
                "group": "golang.org/x",
                "name": "crypto",
                "version": "v0.1.0",
                "purl": "pkg:golang/golang.org/x/crypto@v0.1.0",
                "licenses": [{"license": {"id": "BSD-3-Clause"}}]
            }
        ]
    }`

    // Simulated SBOM B (New version introducing upgrade and new dependency)
    sbomBJSON := `{
        "bomFormat": "CycloneDX",
        "specVersion": "1.4",
        "version": 2,
        "components": [
            {
                "type": "library",
                "group": "github.com/gin-gonic",
                "name": "gin",
                "version": "v1.9.0",
                "purl": "pkg:golang/github.com/gin-gonic/gin@v1.9.0",
                "licenses": [{"license": {"id": "MIT"}}]
            },
            {
                "type": "library",
                "group": "golang.org/x",
                "name": "crypto",
                "version": "v0.1.0",
                "purl": "pkg:golang/golang.org/x/crypto@v0.1.0",
                "licenses": [{"license": {"id": "BSD-3-Clause"}}]
            },
            {
                "type": "library",
                "group": "github.com/go-gorm",
                "name": "gorm",
                "version": "v1.24.0",
                "purl": "pkg:golang/github.com/go-gorm/gorm@v1.24.0",
                "licenses": [{"license": {"id": "GPL-3.0-only"}}]
            }
        ]
    }`

    fmt.Println("==================================================")
    fmt.Println("🛡️ DevSecOps SBOM Diff & Dependency Audit Tool")
    fmt.Println("==================================================")

    var sbomA, sbomB CycloneDXSBOM
    if err := json.Unmarshal([]byte(sbomAJSON), &sbomA); err != nil {
        fmt.Printf("Error unmarshaling SBOM A: %v\n", err)
        return
    }
    if err := json.Unmarshal([]byte(sbomBJSON), &sbomB); err != nil {
        fmt.Printf("Error unmarshaling SBOM B: %v\n", err)
        return
    }

    // Map components in A
    mapA := make(map[string]Component)
    for _, comp := range sbomA.Components {
        mapA[componentKey(comp)] = comp
    }

    // Map components in B
    mapB := make(map[string]Component)
    for _, comp := range sbomB.Components {
        mapB[componentKey(comp)] = comp
    }

    fmt.Println("\nAnalyzing shifts between SBOM A (Baseline) -> SBOM B (Target Build)...")
    newDependencies := 0
    versionShifts := 0
    licenseAlerts := 0

    // 1. Scan for additions and version changes (B relative to A)
    for key, compB := range mapB {
        compA, exists := mapA[key]
        if !exists {
            fmt.Printf("[NEW DEPENDENCY] %s (%s) was introduced.\n", key, compB.Version)
            newDependencies++

            // Check license of new dependency
            for _, lic := range compB.Licenses {
                if lic.License.ID == "GPL-3.0-only" || lic.License.ID == "AGPL-3.0" {
                    fmt.Printf("   ⚠️  [LICENSE ALERT] Component '%s' introduces copyleft license: %s\n", key, lic.License.ID)
                    licenseAlerts++
                }
            }
        } else if compA.Version != compB.Version {
            fmt.Printf("[VERSION SHIFT] %s: %s -> %s\n", key, compA.Version, compB.Version)
            versionShifts++
        }
    }

    // 2. Scan for removals (A relative to B)
    removals := 0
    for key := range mapA {
        if _, exists := mapB[key]; !exists {
            fmt.Printf("[REMOVED DEPENDENCY] %s was removed.\n", key)
            removals++
        }
    }

    fmt.Println("\n--- Scan Results ---")
    fmt.Printf("Added Dependencies:      %d\n", newDependencies)
    fmt.Printf("Removed Dependencies:    %d\n", removals)
    fmt.Printf("Version Shifted Libs:    %d\n", versionShifts)
    fmt.Printf("High-Risk Licenses:      %d\n", licenseAlerts)
}

3. High-Entropy Secret Scanner (secret-scanner.go)

Purpose

Scans code files or git diff inputs using regular expressions and Shannon entropy calculations to identify hardcoded API keys, passwords, and private keys.

Code Implementation

// Purpose: Scans code files or git diff inputs using regular expressions and Shannon entropy calculations to identify hardcoded API keys, passwords, and private keys.
package main

import (
    "fmt"
    "math"
    "regexp"
    "strings"
)

// ScannerFinding holds information about a detected secret.
type ScannerFinding struct {
    LineNumber int
    RuleName   string
    Severity   string
    Content    string
    Entropy    float64
}

// ShannonEntropy calculates the thermodynamic entropy of a string (character distribution diversity).
// High entropy strings (e.g. > 4.5 for hex/base64) are strong indicators of random cryptographic keys.
func ShannonEntropy(s string) float64 {
    if len(s) == 0 {
        return 0
    }
    charMap := make(map[rune]float64)
    for _, char := range s {
        charMap[char]++
    }

    var entropy float64
    length := float64(len(s))
    for _, count := range charMap {
        prob := count / length
        entropy -= prob * math.Log2(prob)
    }
    return entropy
}

// SecretRules defines patterns we scan for.
var SecretRules = []struct {
    Name     string
    Pattern  *regexp.Regexp
    Severity string
}{
    {
        Name:     "AWS Access Key ID",
        Pattern:  regexp.MustCompile(`(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|ASCA|ASIA)[A-Z0-9]{16}`),
        Severity: "CRITICAL",
    },
    {
        Name:     "AWS Secret Access Key",
        Pattern:  regexp.MustCompile(`(?i)(?:aws_secret|aws_key|secret_key|aws_secret_access_key).*?['\"][A-Za-z0-9/+=]{40}['\"]`),
        Severity: "CRITICAL",
    },
    {
        Name:     "Generic API Key / Token",
        Pattern:  regexp.MustCompile(`(?i)(?:api_key|apikey|secret|token|password|passwd|auth_token).*?['\"][A-Za-z0-9\-._~+/=]{20,80}['\"]`),
        Severity: "HIGH",
    },
    {
        Name:     "Slack Webhook URL",
        Pattern:  regexp.MustCompile(`https://hooks\.slack\.com/services/T[A-Z0-9]{8}/B[A-Z0-9]{8}/[A-Za-z0-9]{24}`),
        Severity: "CRITICAL",
    },
    {
        Name:     "PEM Private Key Header",
        Pattern:  regexp.MustCompile(`-----BEGIN [A-Z ]*PRIVATE KEY-----`),
        Severity: "CRITICAL",
    },
}

func main() {
    // Sample git diff block with leaked credentials
    gitDiffInput := `
diff --git a/config.json b/config.json
index 8389d63..298d8ef 100644
--- a/config.json
+++ b/config.json
@@ -12,4 +12,6 @@
-  "db_host": "localhost",
+  "db_host": "rds-prod.c3x918s.us-east-1.rds.amazonaws.com",
+  "aws_access_key": "AKIAIOSFODNN7EXAMPLE",
+  "aws_secret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+  "slack_channel": "https://hooks.slack.com/services/" + "T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" + `",`
+  "encryption_key": "dTFhM3M5OGQyM2g0ajVraDJsMzQ1Njc4OWFiY2RlZmc=",
+  "non_secret_val": "simple_string_value_here"
    `

    fmt.Println("==================================================")
    fmt.Println("🛡️ Git Commit Secret Scanner & Entropy Audit Engine")
    fmt.Println("==================================================")

    lines := strings.Split(gitDiffInput, "\n")
    var findings []ScannerFinding

    for idx, line := range lines {
        // Only scan added lines in diff to avoid false positives on context
        if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
            continue
        }

        cleanLine := strings.TrimPrefix(line, "+")

        // 1. Scan for pattern matches
        for _, rule := range SecretRules {
            if matches := rule.Pattern.FindAllString(cleanLine, -1); len(matches) > 0 {
                for _, match := range matches {
                    findings = append(findings, ScannerFinding{
                        LineNumber: idx + 1,
                        RuleName:   rule.Name,
                        Severity:   rule.Severity,
                        Content:    match,
                        Entropy:    ShannonEntropy(match),
                    })
                }
            }
        }

        // 2. Entropy-based scanning on assignments to catch customized key names
        // Look for variables or keys assigned a quoted value of length >= 16
        assignmentPattern := regexp.MustCompile(`(?i)(?:[a-z0-9_-]+)\s*[:=]\s*['\"]([A-Za-z0-9\-._~+/=]{16,})['\"]`)
        if matches := assignmentPattern.FindAllStringSubmatch(cleanLine, -1); len(matches) > 0 {
            for _, match := range matches {
                value := match[1]
                entropy := ShannonEntropy(value)

                // High entropy thresholds:
                // Base64-like keys are usually very random and have entropy > 4.5.
                if entropy > 4.5 {
                    // Check if this finding was already caught by pattern matching
                    alreadyCaught := false
                    for _, f := range findings {
                        if strings.Contains(f.Content, value) {
                            alreadyCaught = true
                            break
                        }
                    }

                    if !alreadyCaught {
                        findings = append(findings, ScannerFinding{
                            LineNumber: idx + 1,
                            RuleName:   "High-Entropy Variable Assignment",
                            Severity:   "HIGH",
                            Content:    value,
                            Entropy:    entropy,
                        })
                    }
                }
            }
        }
    }

    // Print results
    if len(findings) == 0 {
        fmt.Println("No secrets or high-entropy credentials detected. Commit passes security gates.")
    } else {
        fmt.Printf("Alert! Detected %d potential secrets leakage in commit changes:\n\n", len(findings))
        for _, f := range findings {
            fmt.Printf("[%s] Line %d: Found '%s'\n", f.Severity, f.LineNumber, f.RuleName)
            fmt.Printf("   - String:  %s\n", f.Content)
            fmt.Printf("   - Entropy: %.4f (bits/character)\n\n", f.Entropy)
        }
    }
}