Skip to content
Open
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
60 changes: 59 additions & 1 deletion config/endpoint/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,31 @@ const (
// This is only used for aesthetic purposes; it does not influence whether the condition evaluation results in a
// success or a failure
maximumLengthBeforeTruncatingWhenComparedWithPattern = 25

// invalidDurationUnitError is the error message for invalid duration units, see https://pkg.go.dev/time#Duration
invalidDurationUnitError = "invalid duration unit in condition '%s': Go's time.Duration only supports units up to 'h' (hours). Use hours instead (e.g., '336h' instead of '14d')"
Comment on lines +93 to +94
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of consistency, the error should be something like

var (
	ErrInvalidDurationUnit = errors.New("invalid duration...")
)

as opposed to a constant string. Maybe at some point I'll change my mind, but since the entirety of the code uses this approach, I'd rather remain consistent.

)

// Condition is a condition that needs to be met in order for an Endpoint to be considered healthy.
type Condition string

// Validate checks if the Condition is valid
func (c Condition) Validate() error {
condition := string(c)
// Check for invalid duration units in CERTIFICATE_EXPIRATION conditions
if strings.Contains(condition, CertificateExpirationPlaceholder) {
parts := strings.Split(condition, " ")
for i, part := range parts {
if strings.Contains(part, CertificateExpirationPlaceholder) && i+2 < len(parts) {
// Check the value after the operator
value := parts[i+2]
if isDurationWithInvalidUnit(value) {
return fmt.Errorf(invalidDurationUnitError, condition)
}
}
}
}

r := &Result{}
c.evaluate(r, false)
if len(r.Errors) != 0 {
Expand Down Expand Up @@ -150,7 +168,7 @@ func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bo
return false
}
if !success {
//logr.Debugf("[Condition.evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
// logr.Debugf("[Condition.evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
}
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
return success
Expand Down Expand Up @@ -306,13 +324,53 @@ func sanitizeAndResolve(elements []string, result *Result) ([]string, []string)
return parameters, resolvedParameters
}

// isDurationWithInvalidUnit checks if a string looks like a duration but uses units not supported by Go's time.Duration
func isDurationWithInvalidUnit(s string) bool {
// Check for common invalid duration units like days (d), weeks (w), months (mo), years (y)
// Go's time.Duration only supports: ns, us (or µs), ms, s, m, h
s = strings.TrimSpace(s)
if len(s) == 0 {
return false
}

// Check for invalid suffixes (case-insensitive)
lower := strings.ToLower(s)
invalidSuffixes := []string{"d", "w", "y", "mo", "month", "months"}

for _, suffix := range invalidSuffixes {
if strings.HasSuffix(lower, suffix) {
// Verify there's a numeric prefix
prefix := s[:len(s)-len(suffix)]
if _, err := strconv.ParseFloat(strings.TrimSpace(prefix), 64); err == nil {
return true
}
}
}

return false
}

func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []string, resolvedNumericalParameters []int64) {
parameters, resolvedParameters := sanitizeAndResolve(list, result)

// Check if we're in a CERTIFICATE_EXPIRATION condition
hasCertExpiration := false
for _, param := range parameters {
if strings.Contains(param, CertificateExpirationPlaceholder) {
hasCertExpiration = true
break
}
}

for _, element := range resolvedParameters {
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
// If the string is a duration, convert it to milliseconds
resolvedNumericalParameters = append(resolvedNumericalParameters, duration.Milliseconds())
} else if number, err := strconv.ParseInt(element, 0, 64); err != nil {
// Check if this is a duration with an invalid unit (like "14d")
if hasCertExpiration && isDurationWithInvalidUnit(element) {
result.AddError(fmt.Sprintf(invalidDurationUnitError, strings.Join(parameters, " ")))
}
// It's not an int, so we'll check if it's a float
if f, err := strconv.ParseFloat(element, 64); err == nil {
// It's a float, but we'll convert it to an int. We're losing precision here, but it's better than
Expand Down
30 changes: 30 additions & 0 deletions config/endpoint/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func TestCondition_Validate(t *testing.T) {
{condition: "has([BODY].users[0].name) == true", expectedErr: nil},
{condition: "[BODY].name == pat(john*)", expectedErr: nil},
{condition: "[CERTIFICATE_EXPIRATION] > 48h", expectedErr: nil},
{condition: "[CERTIFICATE_EXPIRATION] > 14d", expectedErr: errors.New(fmt.Sprintf(invalidDurationUnitError, "[CERTIFICATE_EXPIRATION] > 14d"))},
{condition: "[CERTIFICATE_EXPIRATION] > 2w", expectedErr: errors.New(fmt.Sprintf(invalidDurationUnitError, "[CERTIFICATE_EXPIRATION] > 2w"))},
{condition: "[DOMAIN_EXPIRATION] > 720h", expectedErr: nil},
{condition: "raw == raw", expectedErr: nil},
{condition: "[STATUS] ? 201", expectedErr: errors.New("invalid condition: [STATUS] ? 201")},
Expand Down Expand Up @@ -490,6 +492,34 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: false,
ExpectedOutput: "[CERTIFICATE_EXPIRATION] (86400000) > 48h (172800000)",
},
{
Name: "certificate-expiration-invalid-duration-days",
Condition: Condition("[CERTIFICATE_EXPIRATION] > 14d"),
Result: &Result{CertificateExpiration: 24 * time.Hour},
ExpectedSuccess: true,
ExpectedOutput: "[CERTIFICATE_EXPIRATION] > 14d",
},
{
Name: "certificate-expiration-invalid-duration-weeks",
Condition: Condition("[CERTIFICATE_EXPIRATION] > 2w"),
Result: &Result{CertificateExpiration: 24 * time.Hour},
ExpectedSuccess: true,
ExpectedOutput: "[CERTIFICATE_EXPIRATION] > 2w",
},
{
Name: "certificate-expiration-invalid-duration-months",
Condition: Condition("[CERTIFICATE_EXPIRATION] > 1mo"),
Result: &Result{CertificateExpiration: 24 * time.Hour},
ExpectedSuccess: true,
ExpectedOutput: "[CERTIFICATE_EXPIRATION] > 1mo",
},
{
Name: "certificate-expiration-invalid-duration-years",
Condition: Condition("[CERTIFICATE_EXPIRATION] > 1y"),
Result: &Result{CertificateExpiration: 24 * time.Hour},
ExpectedSuccess: true,
ExpectedOutput: "[CERTIFICATE_EXPIRATION] > 1y",
},
{
Name: "no-placeholders",
Condition: Condition("1 == 2"),
Expand Down