github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_taskStage_policyEvaluation.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package cloud
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/go-tfe"
    13  )
    14  
    15  type policyEvaluationSummary struct {
    16  	unreachable bool
    17  	pending     int
    18  	failed      int
    19  	passed      int
    20  }
    21  
    22  type Symbol rune
    23  
    24  const (
    25  	Tick          Symbol = '\u2713'
    26  	Cross         Symbol = '\u00d7'
    27  	Warning       Symbol = '\u24be'
    28  	Arrow         Symbol = '\u2192'
    29  	DownwardArrow Symbol = '\u21b3'
    30  )
    31  
    32  type policyEvaluationSummarizer struct {
    33  	finished bool
    34  	cloud    *Cloud
    35  	counter  int
    36  }
    37  
    38  func newPolicyEvaluationSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummarizer {
    39  	if len(ts.PolicyEvaluations) == 0 {
    40  		return nil
    41  	}
    42  	return &policyEvaluationSummarizer{
    43  		finished: false,
    44  		cloud:    b,
    45  	}
    46  }
    47  
    48  func (pes *policyEvaluationSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) {
    49  	if pes.counter == 0 {
    50  		output.Output("[bold]OPA Policy Evaluation\n")
    51  		pes.counter++
    52  	}
    53  
    54  	if pes.finished {
    55  		return false, nil, nil
    56  	}
    57  
    58  	counts := summarizePolicyEvaluationResults(ts.PolicyEvaluations)
    59  
    60  	if counts.pending != 0 {
    61  		pendingMessage := "Evaluating ... "
    62  		return true, &pendingMessage, nil
    63  	}
    64  
    65  	if counts.unreachable {
    66  		output.Output("Skipping policy evaluation.")
    67  		output.End()
    68  		return false, nil, nil
    69  	}
    70  
    71  	// Print out the summary
    72  	if err := pes.taskStageWithPolicyEvaluation(context, output, ts.PolicyEvaluations); err != nil {
    73  		return false, nil, err
    74  	}
    75  	// Mark as finished
    76  	pes.finished = true
    77  
    78  	return false, nil, nil
    79  }
    80  
    81  func summarizePolicyEvaluationResults(policyEvaluations []*tfe.PolicyEvaluation) *policyEvaluationSummary {
    82  	var pendingCount, errCount, passedCount int
    83  	for _, policyEvaluation := range policyEvaluations {
    84  		switch policyEvaluation.Status {
    85  		case "unreachable":
    86  			return &policyEvaluationSummary{
    87  				unreachable: true,
    88  			}
    89  		case "running", "pending", "queued":
    90  			pendingCount++
    91  		case "passed":
    92  			passedCount++
    93  		default:
    94  			// Everything else is a failure
    95  			errCount++
    96  		}
    97  	}
    98  
    99  	return &policyEvaluationSummary{
   100  		unreachable: false,
   101  		pending:     pendingCount,
   102  		failed:      errCount,
   103  		passed:      passedCount,
   104  	}
   105  }
   106  
   107  func (pes *policyEvaluationSummarizer) taskStageWithPolicyEvaluation(context *IntegrationContext, output IntegrationOutputWriter, policyEvaluation []*tfe.PolicyEvaluation) error {
   108  	var result, message string
   109  	// Currently only one policy evaluation supported : OPA
   110  	for _, polEvaluation := range policyEvaluation {
   111  		if polEvaluation.Status == tfe.PolicyEvaluationPassed {
   112  			message = "[dim] This result means that all OPA policies passed and the protected behavior is allowed"
   113  			result = fmt.Sprintf("[green]%s", strings.ToUpper(string(tfe.PolicyEvaluationPassed)))
   114  			if polEvaluation.ResultCount.AdvisoryFailed > 0 {
   115  				result += " (with advisory)"
   116  			}
   117  		} else {
   118  			message = "[dim] This result means that one or more OPA policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules"
   119  			result = fmt.Sprintf("[red]%s", strings.ToUpper(string(tfe.PolicyEvaluationFailed)))
   120  		}
   121  
   122  		output.Output(fmt.Sprintf("[bold]%c%c Overall Result: %s", Arrow, Arrow, result))
   123  
   124  		output.Output(message)
   125  
   126  		total := getPolicyCount(polEvaluation.ResultCount)
   127  
   128  		output.Output(fmt.Sprintf("%d policies evaluated\n", total))
   129  
   130  		policyOutcomes, err := pes.cloud.client.PolicySetOutcomes.List(context.StopContext, polEvaluation.ID, nil)
   131  		if err != nil {
   132  			return err
   133  		}
   134  
   135  		for i, out := range policyOutcomes.Items {
   136  			output.Output(fmt.Sprintf("%c Policy set %d: [bold]%s (%d)", Arrow, i+1, out.PolicySetName, len(out.Outcomes)))
   137  			for _, outcome := range out.Outcomes {
   138  				output.Output(fmt.Sprintf("  %c Policy name: [bold]%s", DownwardArrow, outcome.PolicyName))
   139  				switch outcome.Status {
   140  				case "passed":
   141  					output.Output(fmt.Sprintf("     | [green][bold]%c Passed", Tick))
   142  				case "failed":
   143  					if outcome.EnforcementLevel == tfe.EnforcementAdvisory {
   144  						output.Output(fmt.Sprintf("     | [blue][bold]%c Advisory", Warning))
   145  					} else {
   146  						output.Output(fmt.Sprintf("     | [red][bold]%c Failed", Cross))
   147  					}
   148  				}
   149  				if outcome.Description != "" {
   150  					output.Output(fmt.Sprintf("     | [dim]%s", outcome.Description))
   151  				} else {
   152  					output.Output("     | [dim]No description available")
   153  				}
   154  			}
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  func getPolicyCount(resultCount *tfe.PolicyResultCount) int {
   161  	return resultCount.AdvisoryFailed + resultCount.MandatoryFailed + resultCount.Errored + resultCount.Passed
   162  }