Skip to content

Commit

Permalink
Add deployment gates and simplify summary
Browse files Browse the repository at this point in the history
  • Loading branch information
Pranay Singh committed Dec 12, 2024
1 parent c82a828 commit d99c6c0
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 46 deletions.
14 changes: 14 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ resource "starchitect_iac_pac" "demo_example" {
iac_path = var.iac_path
# pac_path = var.pac_path
# pac_version = var.pac_version
threshold = var.threshold
log_path = var.log_path
}

variable "iac_path" {
Expand All @@ -29,6 +31,18 @@ variable "pac_version" {
default = "main"
}

variable "threshold" {
description = "Minimum required security score (0-100)"
type = string
default = "50"
}

variable "log_path" {
description = "Path to store log files"
type = string
default = "../logs" # Logs will be stored in ./logs directory
}

output "scan_result" {
value = starchitect_iac_pac.demo_example.scan_result
}
Expand Down
236 changes: 205 additions & 31 deletions resources/iac_pac.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework/resource"
resschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
Expand All @@ -25,8 +28,33 @@ type IACPACResourceModel struct {
IACPath types.String `tfsdk:"iac_path"`
PACPath types.String `tfsdk:"pac_path"`
PACVersion types.String `tfsdk:"pac_version"`
LogPath types.String `tfsdk:"log_path"`
ScanResult types.String `tfsdk:"scan_result"`
Score types.String `tfsdk:"score"`
Threshold types.String `tfsdk:"threshold"`
}

type RegulaRuleResult struct {
Controls []string `json:"controls"`
Families []string `json:"families"`
Filepath string `json:"filepath"`
InputType string `json:"input_type"`
Provider string `json:"provider"`
ResourceID string `json:"resource_id"`
ResourceType string `json:"resource_type"`
ResourceTags map[string]string `json:"resource_tags"`
RuleDescription string `json:"rule_description"`
RuleID string `json:"rule_id"`
RuleMessage string `json:"rule_message"`
RuleName string `json:"rule_name"`
RuleRawResult bool `json:"rule_raw_result"`
RuleResult string `json:"rule_result"`
RuleSeverity string `json:"rule_severity"`
RuleSummary string `json:"rule_summary"`
}

type RegulaOutput struct {
RuleResults []RegulaRuleResult `json:"rule_results"`
}

func NewIACPACResource() resource.Resource {
Expand All @@ -48,11 +76,54 @@ func (r *IACPACResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
iacPath := plan.IACPath.ValueString()
pacPath := plan.PACPath.ValueString()
pacVersion := plan.PACVersion.ValueString()
threshold := plan.Threshold.ValueString()
logPath := plan.LogPath.ValueString()

scanResult, score := GetScanResult(iacPath, pacPath, pacVersion)
scanResult, score := GetScanResult(iacPath, pacPath, pacVersion, logPath)
plan.ScanResult = types.StringValue(scanResult)
plan.Score = types.StringValue(score)

// Check threshold if specified
if threshold != "" {
thresholdValue, err := strconv.ParseFloat(threshold, 64)
if err != nil {
resp.Diagnostics.AddError(
"Invalid threshold value",
fmt.Sprintf("Could not parse threshold value: %v", err),
)
return
}

// Extract score value
scoreStr := strings.TrimSpace(score)
parts := strings.Split(scoreStr, "Score: ")
if len(parts) != 2 {
resp.Diagnostics.AddError(
"Invalid score format",
fmt.Sprintf("Could not parse score from: %s", score),
)
return
}

scoreStr = strings.TrimSuffix(parts[1], " percent")
scoreValue, err := strconv.ParseFloat(scoreStr, 64)
if err != nil {
resp.Diagnostics.AddError(
"Invalid score value",
fmt.Sprintf("Could not parse score value: %v", err),
)
return
}

if scoreValue < thresholdValue {
resp.Diagnostics.AddError(
"Security Score Below Threshold",
fmt.Sprintf("Security score (%.2f%%) is below the required threshold (%.2f%%)", scoreValue, thresholdValue),
)
return
}
}

diags = resp.Plan.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
Expand All @@ -77,6 +148,14 @@ func (r *IACPACResource) Schema(_ context.Context, _ resource.SchemaRequest, res
Description: "default PAC version",
Optional: true,
},
"log_path": resschema.StringAttribute{
Description: "Path to store log files",
Optional: true,
},
"threshold": resschema.StringAttribute{
Description: "Minimum required security score (0-100)",
Optional: true,
},
"scan_result": resschema.StringAttribute{
Description: "Generated scan result",
Computed: true,
Expand Down Expand Up @@ -106,8 +185,9 @@ func (r *IACPACResource) Create(ctx context.Context, req resource.CreateRequest,
iacPath := plan.IACPath.ValueString()
pacPath := plan.PACPath.ValueString()
pacVersion := plan.PACVersion.ValueString()
logPath := plan.LogPath.ValueString()

scanResult, score := GetScanResult(iacPath, pacPath, pacVersion)
scanResult, score := GetScanResult(iacPath, pacPath, pacVersion, logPath)
plan.ScanResult = types.StringValue(scanResult)
plan.Score = types.StringValue(score)

Expand All @@ -126,8 +206,9 @@ func (r *IACPACResource) Read(ctx context.Context, req resource.ReadRequest, res
iacPath := state.IACPath.ValueString()
pacPath := state.PACPath.ValueString()
pacVersion := state.PACVersion.ValueString()
logPath := state.LogPath.ValueString()

scanResult, score := GetScanResult(iacPath, pacPath, pacVersion)
scanResult, score := GetScanResult(iacPath, pacPath, pacVersion, logPath)
state.ScanResult = types.StringValue(scanResult)
state.Score = types.StringValue(score)

Expand All @@ -146,8 +227,9 @@ func (r *IACPACResource) Update(ctx context.Context, req resource.UpdateRequest,
iacPath := plan.IACPath.ValueString()
pacPath := plan.PACPath.ValueString()
pacVersion := plan.PACVersion.ValueString()
logPath := plan.LogPath.ValueString()

scanResult, score := GetScanResult(iacPath, pacPath, pacVersion)
scanResult, score := GetScanResult(iacPath, pacPath, pacVersion, logPath)
plan.ScanResult = types.StringValue(scanResult)
plan.Score = types.StringValue(score)

Expand All @@ -164,47 +246,119 @@ func (r *IACPACResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}

type RegulaOutput struct {
Summary struct {
RuleResults struct {
Fail int `json:"FAIL"`
Pass int `json:"PASS"`
Waived int `json:"WAIVED"`
} `json:"rule_results"`
} `json:"summary"`
func formatRegulaOutput(regulaOutput RegulaOutput) string {
var formatted strings.Builder

// Add timestamp
formatted.WriteString(fmt.Sprintf("Scan Time: %s\n", time.Now().Format(time.RFC3339)))
formatted.WriteString("====================\n\n")

// Calculate summary
var passCount, failCount int
for _, rule := range regulaOutput.RuleResults {
if rule.RuleResult == "PASS" {
passCount++
} else if rule.RuleResult == "FAIL" {
failCount++
}
}

// Add summary
formatted.WriteString("Summary:\n")
formatted.WriteString(fmt.Sprintf("PASSED: %d\n", passCount))
formatted.WriteString(fmt.Sprintf("FAILED: %d\n", failCount))
formatted.WriteString("\nDetailed Results:\n")
formatted.WriteString("----------------\n")

for _, rule := range regulaOutput.RuleResults {
formatted.WriteString(fmt.Sprintf("\nRule ID: %s\n", rule.RuleID))
formatted.WriteString(fmt.Sprintf("Name: %s\n", rule.RuleName))
formatted.WriteString(fmt.Sprintf("Result: %s\n", rule.RuleResult))
formatted.WriteString(fmt.Sprintf("Severity: %s\n", rule.RuleSeverity))
formatted.WriteString(fmt.Sprintf("Summary: %s\n", rule.RuleSummary))
formatted.WriteString(fmt.Sprintf("Description: %s\n", rule.RuleDescription))

if rule.ResourceType != "" {
formatted.WriteString(fmt.Sprintf("Resource Type: %s\n", rule.ResourceType))
}
if rule.ResourceID != "" {
formatted.WriteString(fmt.Sprintf("Resource ID: %s\n", rule.ResourceID))
}
if rule.RuleMessage != "" {
formatted.WriteString(fmt.Sprintf("Message: %s\n", rule.RuleMessage))
}

if len(rule.Controls) > 0 {
formatted.WriteString("Controls:\n")
for _, control := range rule.Controls {
formatted.WriteString(fmt.Sprintf(" - %s\n", control))
}
}

if len(rule.Families) > 0 {
formatted.WriteString("Families:\n")
for _, family := range rule.Families {
formatted.WriteString(fmt.Sprintf(" - %s\n", family))
}
}

formatted.WriteString("---\n")
}

return formatted.String()
}

func calculateScore(filePath string) string {
// Open the file
file, err := os.Open(filePath)
if err != nil {
return fmt.Sprintf("failed to open file: %v", err)
func writeToLogFiles(rawOutput string, formattedOutput string, logPath string) error {
timestamp := time.Now().Format("20060102_150405")

// Create log directory if it doesn't exist
if logPath != "" {
if err := os.MkdirAll(logPath, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %v", err)
}
}
defer file.Close()

// Decode the JSON
var regulaOutput RegulaOutput
decoder := json.NewDecoder(file)
if err := decoder.Decode(&regulaOutput); err != nil {
return fmt.Sprintf("failed to decode JSON: %v", err)
// Write raw output to JSON file
rawFileName := fmt.Sprintf("%s_starchitect_raw.json", timestamp)
if logPath != "" {
rawFileName = filepath.Join(logPath, rawFileName)
}
if err := os.WriteFile(rawFileName, []byte(rawOutput), 0644); err != nil {
return fmt.Errorf("failed to write raw output: %v", err)
}

// Write formatted summary to log file
summaryFileName := fmt.Sprintf("%s_starchitect_summary.log", timestamp)
if logPath != "" {
summaryFileName = filepath.Join(logPath, summaryFileName)
}
if err := os.WriteFile(summaryFileName, []byte(formattedOutput), 0644); err != nil {
return fmt.Errorf("failed to write summary: %v", err)
}

// Get counts
passCount := regulaOutput.Summary.RuleResults.Pass
failCount := regulaOutput.Summary.RuleResults.Fail
return nil
}

func calculateScore(regulaOutput RegulaOutput) string {
var passCount, failCount int
for _, rule := range regulaOutput.RuleResults {
if rule.RuleResult == "PASS" {
passCount++
} else if rule.RuleResult == "FAIL" {
failCount++
}
}

// Calculate the score
total := passCount + failCount
if total == 0 {
return "no PASS or FAIL results found"
}

score := (float64(passCount) / float64(total)) * 100
return fmt.Sprintf("PASSED: %v FAILED: %v Score: %v percent", passCount, failCount, score)
return fmt.Sprintf("PASSED: %d FAILED: %d Score: %.2f percent", passCount, failCount, score)
}

func GetScanResult(iacPath, pacPath, pacVersion string) (string, string) {

func GetScanResult(iacPath, pacPath, pacVersion, logPath string) (string, string) {
if pacPath == "" {
// Step 1: Create a temporary directory
tempCloneDir, err := os.MkdirTemp("", "pac-clone-*")
Expand Down Expand Up @@ -256,10 +410,30 @@ func GetScanResult(iacPath, pacPath, pacVersion string) (string, string) {
err = nil
}

// Read the raw output
content, err := os.ReadFile(outputFile)
if err != nil {
return fmt.Sprintf("Error reading output file: %s %v\n", outputFile, err), ""
}

return string(content), calculateScore(outputFile)
rawOutput := string(content)

// Parse the JSON content
var regulaOutput RegulaOutput
if err := json.Unmarshal(content, &regulaOutput); err != nil {
return fmt.Sprintf("Error parsing JSON output: %v\n", err), ""
}

// Calculate score
score := calculateScore(regulaOutput)

// Format the summary output
formattedOutput := formatRegulaOutput(regulaOutput)

// Write both outputs to separate files
if err := writeToLogFiles(rawOutput, formattedOutput, logPath); err != nil {
log.Printf("Warning: Failed to write to log files: %v", err)
}

return formattedOutput, score
}
Loading

0 comments on commit d99c6c0

Please sign in to comment.