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)
}
}
}