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
31 changes: 13 additions & 18 deletions cmd/compare/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ import (
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"

"github.com/conforma/cli/internal/policy"
"github.com/conforma/cli/internal/policy/equivalence"
)

var (
effectiveTime string
imageDigest string
imageRef string
imageURL string
outputFormat string
effectiveTime string
allowPastEffectiveTime bool
imageDigest string
imageRef string
imageURL string
outputFormat string
)

var CompareCmd *cobra.Command
Expand Down Expand Up @@ -74,6 +76,7 @@ Examples:
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] flag-declaration-style

Flag description for --allow-past-effective-time in compare.go omits 'By default, dates more than 5 minutes in the past are rejected' which is present in image.go and input.go.

compareCmd.Flags().StringVar(&effectiveTime, "effective-time", "now", "Effective time for policy evaluation (RFC3339 format, 'now')")
compareCmd.Flags().BoolVar(&allowPastEffectiveTime, "allow-past-effective-time", false, "Allow setting --effective-time to a date in the past")
compareCmd.Flags().StringVar(&imageDigest, "image-digest", "", "Image digest for volatile config matching")
compareCmd.Flags().StringVar(&imageRef, "image-ref", "", "Image reference for volatile config matching")
compareCmd.Flags().StringVar(&imageURL, "image-url", "", "Image URL for volatile config matching")
Expand All @@ -84,20 +87,12 @@ Examples:

func runCompare(cmd *cobra.Command, args []string) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] scope

Compare command refactoring replaces inline time parsing with ParseEffectiveTime. Behavioral change beyond stated fix scope — previously compare accepted any past time silently.

// Parse effective time
var effectiveTimeValue time.Time
switch effectiveTime {
case "now":
effectiveTimeValue = time.Now().UTC()
case "attestation":
// For now, use current time as default for attestation time
effectiveTimeValue, err := policy.ParseEffectiveTime(effectiveTime, allowPastEffectiveTime)
if err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] api-contract

The 'attestation' sentinel is accepted by --effective-time but not documented in the compare command's help text. Pre-existing condition.

}
if effectiveTimeValue.IsZero() {
effectiveTimeValue = time.Now().UTC()
default:
var err error
effectiveTimeValue, err = time.Parse(time.RFC3339, effectiveTime)
if err != nil {
return fmt.Errorf("invalid effective time format: %w", err)
}
}

// Create image info if provided
Expand Down
9 changes: 8 additions & 1 deletion cmd/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
data.policyConfiguration = policyConfiguration

policyOptions := policy.Options{
EffectiveTime: data.effectiveTime,
EffectiveTime: data.effectiveTime,
AllowPastEffectiveTime: data.allowPastEffectiveTime,
Identity: cosign.Identity{
Issuer: data.certificateOIDCIssuer,
IssuerRegExp: data.certificateOIDCIssuerRegExp,
Expand Down Expand Up @@ -547,6 +548,11 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z.
`))

cmd.Flags().BoolVar(&data.allowPastEffectiveTime, "allow-past-effective-time", false, hd.Doc(`
Allow setting --effective-time to a date in the past. By default, dates
more than 5 minutes in the past are rejected.
`))

cmd.Flags().StringSliceVar(&data.extraRuleData, "extra-rule-data", data.extraRuleData, hd.Doc(`
Extra data to be provided to the Rego policy evaluator. Use format 'key=value'. May be used multiple times.
`))
Expand Down Expand Up @@ -633,6 +639,7 @@ type imageData struct {
certificateOIDCIssuer string

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] struct-field-ordering

New allowPastEffectiveTime field breaks alphabetical ordering convention in imageData struct.

certificateOIDCIssuerRegExp string
effectiveTime string
allowPastEffectiveTime bool
extraRuleData []string
filePath string
filterType string
Expand Down
1 change: 1 addition & 0 deletions cmd/validate/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ spec:
"/policy.yaml",
"--effective-time",
"1970-01-01",
"--allow-past-effective-time",
}...)
cmd.SetArgs(args)

Expand Down
35 changes: 20 additions & 15 deletions cmd/validate/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,21 @@ type InputValidationFunc func(context.Context, string, policy.Policy, bool) (*ou

func validateInputCmd(validate InputValidationFunc) *cobra.Command {
data := struct {
effectiveTime string
filePaths []string
forceColor bool
info bool
namespaces []string
noColor bool
output []string
policy policy.Policy
policyConfiguration string
strict bool
workers int
serverMode bool
serverAddress string
serverPort int
effectiveTime string
allowPastEffectiveTime bool
filePaths []string
forceColor bool
info bool
namespaces []string
noColor bool
output []string
policy policy.Policy
policyConfiguration string
strict bool
workers int
serverMode bool
serverAddress string
serverPort int
}{
strict: true,
workers: 5,
Expand Down Expand Up @@ -168,7 +169,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
}
data.policyConfiguration = policyConfiguration

if p, err := policy.NewInputPolicy(cmd.Context(), data.policyConfiguration, data.effectiveTime); err != nil {
if p, err := policy.NewInputPolicy(cmd.Context(), data.policyConfiguration, data.effectiveTime, data.allowPastEffectiveTime); err != nil {
allErrors = errors.Join(allErrors, err)
} else {
data.policy = p
Expand Down Expand Up @@ -349,6 +350,10 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
effective dates in the future. The value can be "now" (default) - for
current time, or a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z.`))

cmd.Flags().BoolVar(&data.allowPastEffectiveTime, "allow-past-effective-time", false, hd.Doc(`
Allow setting --effective-time to a date in the past. By default, dates
more than 5 minutes in the past are rejected.`))

cmd.Flags().BoolVar(&data.info, "info", data.info, hd.Doc(`
Include additional information on the failures. For instance for policy
violations, include the title and the description of the failed policy
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/ec_compare.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ ec compare <policy1> <policy2> [flags]
----
== Options

--allow-past-effective-time:: Allow setting --effective-time to a date in the past (Default: false)
--effective-time:: Effective time for policy evaluation (RFC3339 format, 'now') (Default: now)
-h, --help:: help for compare (Default: false)
--image-digest:: Image digest for volatile config matching
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/ec_validate_image.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ Use a regular expression to match certificate attributes.

== Options

--allow-past-effective-time:: Allow setting --effective-time to a date in the past. By default, dates
more than 5 minutes in the past are rejected.
(Default: false)
--attestation-format:: Attestation output format: dsse (signed envelope), predicate (raw JSON) (Default: dsse)
--attestation-output-dir:: Directory for attestation output files. Defaults to a temp directory under /tmp. Must be under /tmp or the current working directory.
--certificate-identity:: URL of the certificate identity for keyless verification
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/ROOT/pages/ec_validate_input.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Start the server on a custom port:

== Options

--allow-past-effective-time:: Allow setting --effective-time to a date in the past. By default, dates
more than 5 minutes in the past are rejected. (Default: false)
--color:: Enable color when using text output even when the current terminal does not support it (Default: false)
--effective-time:: Run policy checks with the provided time. Useful for testing rules with
effective dates in the future. The value can be "now" (default) - for
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ paths can be provided by using the `:` separator.
*EFFECTIVE_TIME* (`string`):: Run policy checks with the provided time.
+
*Default*: `now`
*ALLOW_PAST_EFFECTIVE_TIME* (`string`):: Allow setting EFFECTIVE_TIME to a date in the past.
+
*Default*: `false`
*EXTRA_RULE_DATA* (`string`):: Merge additional Rego variables into the policy data. Use syntax "key=value,key2=value2..."
*POLICY_BUNDLE_DIGEST* (`string`):: Optional OCI digest to pin the release policy bundle. When provided, the policy configuration is resolved and the reference oci::quay.io/conforma/release-policy:konflux is replaced with oci::quay.io/conforma/release-policy@<digest>. Accepts a full digest (sha256:abc123...) or just the hex hash (abc123...).
+
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/verify-enterprise-contract.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ paths can be provided by using the `:` separator.
*EFFECTIVE_TIME* (`string`):: Run policy checks with the provided time.
+
*Default*: `now`
*ALLOW_PAST_EFFECTIVE_TIME* (`string`):: Allow setting EFFECTIVE_TIME to a date in the past.
+
*Default*: `false`
*EXTRA_RULE_DATA* (`string`):: Merge additional Rego variables into the policy data. Use syntax "key=value,key2=value2..."
*POLICY_BUNDLE_DIGEST* (`string`):: Optional OCI digest to pin the release policy bundle. When provided, the policy configuration is resolved and the reference oci::quay.io/conforma/release-policy:konflux is replaced with oci::quay.io/conforma/release-policy@<digest>. Accepts a full digest (sha256:abc123...) or just the hex hash (abc123...).
+
Expand Down
9 changes: 5 additions & 4 deletions features/task_validate_image.feature
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,11 @@ Feature: Verify Enterprise Contract Tekton Tasks
{"publicKey": ${known_PUBLIC_KEY}}
```
When version 0.1 of the task named "verify-enterprise-contract" is run with parameters:
| IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/effective-time"}]} |
| POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} |
| IGNORE_REKOR | true |
| EFFECTIVE_TIME | 2020-01-01T00:00:00Z |
| IMAGES | {"components": [{"containerImage": "${REGISTRY}/acceptance/effective-time"}]} |
| POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} |
| IGNORE_REKOR | true |
| EFFECTIVE_TIME | 2020-01-01T00:00:00Z |
| ALLOW_PAST_EFFECTIVE_TIME | true |
Then the task should succeed
And the task logs for step "show-config" should contain `"effective-time": "2020-01-01T00:00:00Z"`

Expand Down
2 changes: 1 addition & 1 deletion features/validate_image.feature
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ Feature: evaluate enterprise contract
]
}
"""
When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/ec-policy --rekor-url ${REKOR} --public-key ${known_PUBLIC_KEY} --output=json --effective-time 2014-05-31 --show-successes"
When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/ec-policy --rekor-url ${REKOR} --public-key ${known_PUBLIC_KEY} --output=json --effective-time 2014-05-31 --allow-past-effective-time --show-successes"
Then the exit status should be 0
And the output should match the snapshot

Expand Down
2 changes: 1 addition & 1 deletion internal/input/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ sources:
exclude:
[]
`
p, err := policy.NewInputPolicy(ctx, policyConfiguration, "now")
p, err := policy.NewInputPolicy(ctx, policyConfiguration, "now", false)
assert.NoError(t, err)
return p
}
2 changes: 1 addition & 1 deletion internal/input/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func Test_ValidatePipeline(t *testing.T) {
errFile := afero.WriteFile(appFS, validFile, []byte("data"), 0o777)
assert.NoError(t, errFile)
ctx := utils.WithFS(context.Background(), appFS)
policy, err := policy.NewInputPolicy(ctx, "", "2023-01-01T00:00:00.00Z")
policy, err := policy.NewInputPolicy(ctx, "", "2023-01-01T00:00:00.00Z", true)
assert.NoError(t, err)

for _, tt := range tests {
Expand Down
65 changes: 52 additions & 13 deletions internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,22 @@ func (p *policy) SigstoreOpts() (SigstoreOpts, error) {
}

type Options struct {
EffectiveTime string
Identity cosign.Identity
IgnoreRekor bool
SkipImageSigCheck bool
SkipAttSigCheck bool
PolicyRef string
PublicKey string
RekorURL string
EffectiveTime string
AllowPastEffectiveTime bool
Identity cosign.Identity
IgnoreRekor bool
SkipImageSigCheck bool
SkipAttSigCheck bool
PolicyRef string
PublicKey string
RekorURL string
}

// NewOfflinePolicy construct and return a new instance of Policy that is used
// in offline scenarios, i.e. without cluster or specific services access, and
// no signature verification being performed.
Comment thread
cuipinghuo marked this conversation as resolved.
func NewOfflinePolicy(ctx context.Context, effectiveTime string) (Policy, error) {
if efn, err := parseEffectiveTime(effectiveTime); err == nil {
if efn, err := parseEffectiveTime(effectiveTime, true); err == nil {
return &policy{
effectiveTime: efn,
choosenTime: effectiveTime,
Expand Down Expand Up @@ -219,8 +220,8 @@ func NewInertPolicy(ctx context.Context, policyRef string) (Policy, error) {
// resource in Kubernetes using the format: [namespace/]name
//
// If policyRef is blank, an empty EnterpriseContractPolicySpec is used.
func NewInputPolicy(ctx context.Context, policyRef string, effectiveTime string) (Policy, error) {
if efn, err := parseEffectiveTime(effectiveTime); err == nil {
func NewInputPolicy(ctx context.Context, policyRef string, effectiveTime string, allowPastEffectiveTime bool) (Policy, error) {
if efn, err := parseEffectiveTime(effectiveTime, allowPastEffectiveTime); err == nil {
p := policy{
choosenTime: effectiveTime,
checkOpts: &cosign.CheckOpts{},
Expand Down Expand Up @@ -299,7 +300,7 @@ func NewPolicy(ctx context.Context, opts Options) (Policy, error) {
}
}

if efn, err := parseEffectiveTime(opts.EffectiveTime); err != nil {
if efn, err := parseEffectiveTime(opts.EffectiveTime, opts.AllowPastEffectiveTime); err != nil {
return nil, err
} else {
p.effectiveTime = efn
Expand Down Expand Up @@ -421,7 +422,28 @@ func isNow(choosenTime string) bool {
return strings.EqualFold(choosenTime, Now)
}

func parseEffectiveTime(choosenTime string) (*time.Time, error) {
// pastEffectiveTimeGracePeriod is the tolerance for clock skew when rejecting
// past effective-time values. Times within this window are accepted even
// without --allow-past-effective-time.
const pastEffectiveTimeGracePeriod = 5 * time.Minute

// ParseEffectiveTime parses the effective-time string and returns the resolved
// time. It accepts "now", "attestation", RFC3339, or YYYY-MM-DD formats. When
// allowPast is false, times more than 5 minutes in the past are rejected.
// The "attestation" sentinel returns a zero time.Time — callers should handle
// that case separately.
func ParseEffectiveTime(choosenTime string, allowPast bool) (time.Time, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] naming-convention

Parameter name choosenTime perpetuates existing misspelling in new exported ParseEffectiveTime function. Opportunity to use correct spelling chosenTime.

t, err := parseEffectiveTime(choosenTime, allowPast)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] scope-creep

New exported ParseEffectiveTime() has the same name as existing vsa.ParseEffectiveTime() in internal/validate/vsa/validator.go with different signature and behavior, creating a confusing dual-function situation.

Suggested fix: Document the relationship or file follow-up to unify the two functions.

return time.Time{}, err
}
if t == nil {
return time.Time{}, nil
}
return *t, nil
}

func parseEffectiveTime(choosenTime string, allowPast bool) (*time.Time, error) {
switch {
case isNow(choosenTime):
now := now().UTC()
Expand All @@ -435,6 +457,9 @@ func parseEffectiveTime(choosenTime string) (*time.Time, error) {
if when, err := time.Parse(time.RFC3339, choosenTime); err == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] breaking-cli

Default behavior of --effective-time changed in backward-incompatible way. Past dates now rejected unless --allow-past-effective-time is passed. Downstream CI/CD pipelines or scripts using --effective-time with past dates will break after CLI upgrade.

Suggested fix: Document as breaking change in release notes and consider bumping the minor/major version. At minimum, release notes must call out the breaking behavior.

log.Debugf("Using provided effective time %s", when.Format(time.RFC3339))
whenUTC := when.UTC()
if err := rejectPastTime(whenUTC, allowPast); err != nil {
return nil, err
}
return &whenUTC, nil
}

Expand All @@ -444,6 +469,9 @@ func parseEffectiveTime(choosenTime string) (*time.Time, error) {
if when, err := time.Parse(DateFormat, choosenTime); err == nil {
log.Debugf("Using provided effective time %s", when.Format(time.RFC3339))
whenUTC := when.UTC()
if err := rejectPastTime(whenUTC, allowPast); err != nil {
return nil, err
}
return &whenUTC, nil
}
log.Debugf("Unable to parse provided effective time string `%s` using %s format", choosenTime, DateFormat)
Expand All @@ -453,6 +481,17 @@ func parseEffectiveTime(choosenTime string) (*time.Time, error) {
}
}

func rejectPastTime(when time.Time, allowPast bool) error {
if allowPast {
return nil
}
threshold := now().UTC().Add(-pastEffectiveTimeGracePeriod)
if when.Before(threshold) {
return fmt.Errorf("effective time %s is in the past; use --allow-past-effective-time to override", when.Format(time.RFC3339))
}
return nil
}

// checkOpts returns an instance based on attributes of the Policy.
func checkOpts(ctx context.Context, p *policy) (*cosign.CheckOpts, error) {
var err error
Expand Down
Loading
Loading