diff --git a/Makefile b/Makefile index 029cdc163..95365036e 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,6 @@ setup: force-upgrade setup-go setup-binaries setup-schemastore go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@v1.3.0 setup-go: go build -o $(PACKAGE_PATH)/bin/ $(REPO_PATH)/golang/cmd/... - go build -o $(PACKAGE_PATH)/bin/cuevalidate.so -buildmode=c-shared $(REPO_PATH)/golang/internal/cue_validator/cue_validator.go setup-binaries: $(PACKAGE_PATH)/bin/slsa-verifier $(PACKAGE_PATH)/resources/mvnw $(PACKAGE_PATH)/resources/gradlew souffle gnu-sed $(PACKAGE_PATH)/bin/slsa-verifier: git clone --depth 1 https://github.com/slsa-framework/slsa-verifier.git -b v2.6.0 diff --git a/golang/README.md b/golang/README.md index 37f0f0d4b..4cefbe323 100644 --- a/golang/README.md +++ b/golang/README.md @@ -1,10 +1,10 @@ # Go module documentation ## Quick start Prerequisites -- Go (tested on `go1.17.8 linux/amd64`). Installation instructions [here](https://go.dev/doc/install). +- Go (tested on `go 1.23.0 linux/amd64`). Installation instructions [here](https://go.dev/doc/install). - Prepare the required libraries by running this command from the root dir of this repository: -``` +```bash go mod download ``` This command will download all packages as defined in [go.mod](../../../go.mod) and [go.sum](../../../go.sum). @@ -12,17 +12,17 @@ This command will download all packages as defined in [go.mod](../../../go.mod) ### Project layout This go module follows the Golang project layout as specified in [golang-standards/project-layout](https://github.com/golang-standards/project-layout). -``` +```bash macaron ├── golang -│ ├── cmd -│ │ └── bashparser -│ ├── internal -│ │ ├── bashparser -│ │ ├── cue_validator -│ │ └── filewriter -│ ├── pkg -│ └── README.md +│   ├── cmd +│   │   ├── bashparser +│   │   └── cuevalidator +│   ├── internal +│   │   ├── bashparser +│   │   ├── cuevalidator +│   │   └── filewriter +│   └── README.md ├── go.mod ├── go.sum └── @@ -36,32 +36,39 @@ macaron ### Run the application code directly using Go To run an application (in the `cmd` dir), from the root dir of this repository: -``` +```bash go run ./golang/cmd//.go [ARGS] ``` -For example, to run the [actionparser](./cmd/actionparser/README.md) application: -``` -go run ./golang/cmd/actionparser/actionparser.go -file ./golang/internal/actionparser/resources/valid.yaml -``` ### Run the Go tests To run all the tests, from the root dir of this repository: +```bash +make test ``` + +To just run the Go tests: +```bash go test ./golang/... ``` To run the tests and record the code coverage, from the root dir of this repository: -``` +```bash go test -cover ./golang/... ``` ### Build the executable To build an executable of an application in this module: + +```bash +make setup-go ``` + +Alternatively you can run: +```bash go build ./golang/cmd//.go ``` This will generate an executable `app_name` in the current directory. We can also change the path of the output executable by using: -``` +```bash go build -o ./golang/cmd//.go ``` diff --git a/golang/cmd/cuevalidator/README.md b/golang/cmd/cuevalidator/README.md new file mode 100644 index 000000000..4e68d7fe7 --- /dev/null +++ b/golang/cmd/cuevalidator/README.md @@ -0,0 +1,45 @@ +# CUE Validator + +This Go module validates CUE provenance against a policy and extracts analysis targets using [CUE](https://cuelang.org/). + +### Run the CUE Validator directly + +To run the validator, from the root directory of this repository: + +```bash +go run ./golang/cmd/cuevalidator/cuevalidator.go -h +``` + + +#### Commands: + +- `-target-policy `: The CUE policy path from which to extract the target. +- `-validate-policy `: The CUE policy path to validate the provenance against. +- `-validate-provenance `: The provenance payload path to validate. + +### Examples: + +1. **Extract Target from Policy** + To extract the target from a CUE policy, use the following command: + +```bash +go run ./golang/cmd/cuevalidator/cuevalidator.go -target-policy +``` + +Output: + +```bash +pkg:maven/io.micronaut/micronaut-core +``` + +2. **Validate Provenance Against Policy** +To validate provenance against a policy, use the following command: + +```bash +go run ./golang/cmd/cuevalidator/cuevalidator.go -validate-policy -validate-provenance +``` + +### Error Handling: + +- If required arguments are missing or invalid, the program will print an error message to `stderr` and exit with a non-zero status code. +- If the validation fails, an error message will be printed, and the program will exit with an appropriate error code. diff --git a/golang/cmd/cuevalidator/cuevalidator.go b/golang/cmd/cuevalidator/cuevalidator.go new file mode 100644 index 000000000..d9cff56e4 --- /dev/null +++ b/golang/cmd/cuevalidator/cuevalidator.go @@ -0,0 +1,110 @@ +/* Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. */ +/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */ + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/oracle/macaron/golang/internal/cuevalidator" +) + +// Utility function to handle file reading and errors. +func readFile(path string) ([]byte, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file '%s': %w", path, err) + } + return content, nil +} + +// Handle validation errors. +func handleError(message string, code int) { + fmt.Fprintln(os.Stderr, message) + os.Exit(code) +} + +// Main entry point for the CUE Validator tool. +// This function processes command-line flags to execute one of the following commands: +// - Extract a target from a CUE policy (using -target-policy flag). +// - Validate provenance against a CUE policy (using -validate-policy and -validate-provenance flags). +// +// Params: +// +// -target-policy : the CUE policy to extract the target from. +// -validate-policy : the CUE policy to validate the provenance against. +// -validate-provenance : the provenance data to validate. +// +// Return code: +// +// 0 - If the target is successfully extracted or the provenance validation finishes with no errors. +// 1 - If there is a missing required argument or invalid command usage. +// 2 - If an error occurs during validation (e.g., invalid provenance or policy). +// +// Usage: +// +// 1. To extract the target from a policy: +// go run cuevalidator.go -target-policy +// Output: The extracted target will be printed to stdout. +// +// 2. To validate provenance against a policy: +// go run cuevalidator.go -validate-policy -validate-provenance +// Output: A success or failure message will be printed based on the validation result. +func main() { + // Define flags for the target command. + targetPolicy := flag.String("target-policy", "", "Path to CUE policy to extract the target from.") + + // Define flags for the validate command + validatePolicy := flag.String("validate-policy", "", "Path to CUE policy to validate against.") + validateProvenance := flag.String("validate-provenance", "", "Path to provenance data to validate.") + + // Parse flags + flag.Parse() + + // Handle 'target-policy' command. + if *targetPolicy != "" { + policyContent, err := readFile(*targetPolicy) + if err != nil { + handleError(err.Error(), 2) + } + + result := cuevalidator.Target(string(policyContent)) + if result == "" { + handleError("Error: Unable to extract target from policy.", 2) + } + + fmt.Print(result) + return + } + + // Handle 'validate' command. + if *validatePolicy != "" && *validateProvenance != "" { + policyContent, err := readFile(*validatePolicy) + if err != nil { + handleError(err.Error(), 2) + } + + provenanceContent, err := readFile(*validateProvenance) + if err != nil { + handleError(err.Error(), 2) + } + + result := cuevalidator.Validate(string(policyContent), string(provenanceContent)) + switch result { + case 1: + fmt.Print("True") + os.Exit(0) + case 0: + fmt.Print("False") + os.Exit(0) + default: + handleError("Error: Validation encountered an issue.", 2) + } + return + } + + // If no valid command was given, print usage message + handleError("Error: Missing required arguments for target or validate command.", 1) +} diff --git a/golang/internal/cue_validator/cgo_helper.go b/golang/internal/cue_validator/cgo_helper.go deleted file mode 100644 index c90984188..000000000 --- a/golang/internal/cue_validator/cgo_helper.go +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. */ -/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */ - -// This module provides CGO helper functions for testing. -package main - -import ( - "C" - "os" - "path" - "runtime" - "testing" -) - -// Get the path to the resources directory. -func GetResourcesPath(t *testing.T) string { - _, filename, _, ok := runtime.Caller(1) - if !ok { - t.Errorf("Unable to locate resources.") - } - return path.Join(path.Dir(filename), "resources") -} - -// Load resource file. -func LoadResource(t *testing.T, name string) *C.char { - path := path.Join(GetResourcesPath(t), name) - content, err := os.ReadFile(path) - if err != nil { - t.Errorf("Unable to load the policy content from %s.", path) - } - return C.CString(string(content)) -} - -func GetGoString(value *C.char) string { - return C.GoString(value) -} diff --git a/golang/internal/cue_validator/cue_validator.go b/golang/internal/cue_validator/cue_validator.go deleted file mode 100644 index fe5b14306..000000000 --- a/golang/internal/cue_validator/cue_validator.go +++ /dev/null @@ -1,80 +0,0 @@ -/* Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. */ -/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */ - -// CUE Validator runs CUE and validates a provenance against a policy. -// See: https://cuelang.org/docs/about/ - -package main - -import ( - "C" - "strings" - - "cuelang.org/go/cue" - "cuelang.org/go/cue/cuecontext" - "cuelang.org/go/encoding/json" -) - -// target returns the analysis target repo for the provided policy content. -// Returns target string value if successful and nil if error has occurred. -// -//export target -func target(policy *C.char) *C.char { - ctx := cuecontext.New() - _policy := C.GoString(policy) - value := ctx.CompileString(_policy) - policy_err := value.Err() - if policy_err != nil { - return nil - } - - target_value := value.LookupPath(cue.ParsePath("target")) - target_err := target_value.Err() - if target_err != nil { - return nil - } - target_path, str_err := target_value.String() - if str_err != nil { - return nil - } - - // We need to be careful about memory leaks on the Python side. - // The documentation at https://pkg.go.dev/cmd/cgo says: - // The C string is allocated in the C heap using malloc. - // It is the caller's responsibility to arrange for it to be - // freed. - return C.CString(strings.TrimSpace(target_path)) -} - -// validate validates the provenance against a CUE policy. -// Returns 1 if policy conforms with the provenance, 0 if -// provenance is invalid, and -1 if CUE returns a validation error. -// -//export validate -func validate(policy *C.char, provenance *C.char) int32 { - _policy := C.GoString(policy) - _provenance := C.GoString(provenance) - - ctx := cuecontext.New() - value := ctx.CompileString(_policy) - - resolved_value := ctx.CompileString(_provenance, cue.Scope(value)) - res_err := resolved_value.Err() - if res_err != nil { - // Unable to process the provenance. - return -1 - } - - validate_err := json.Validate([]byte(_provenance), value) - if validate_err != nil { - // Validation failed. - return 0 - } - - // The provenance conforms with the policy. - return 1 -} - -func main() { - -} diff --git a/golang/internal/cuevalidator/cuevalidator.go b/golang/internal/cuevalidator/cuevalidator.go new file mode 100644 index 000000000..75a1ba8a4 --- /dev/null +++ b/golang/internal/cuevalidator/cuevalidator.go @@ -0,0 +1,61 @@ +/* Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. */ +/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */ + +// CUE Validator runs CUE and validates a provenance against a policy. +// See: https://cuelang.org/docs/about/ + +package cuevalidator + +import ( + "strings" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/encoding/json" +) + +// Target extracts the target from a given CUE policy string. +// It returns the extracted target if successful, or an empty string if an error occurs. +func Target(policy string) string { + ctx := cuecontext.New() + value := ctx.CompileString(policy) + policyErr := value.Err() + if policyErr != nil { + return "" + } + + targetValue := value.LookupPath(cue.ParsePath("target")) + targetErr := targetValue.Err() + if targetErr != nil { + return "" + } + targetPath, strErr := targetValue.String() + if strErr != nil { + return "" + } + + return strings.TrimSpace(targetPath) +} + +// Validate validates the provenance against the given CUE policy. +// It returns 1 if the provenance conforms to the policy, 0 if it does not, and -1 if there is an unexpected error. +func Validate(policy string, provenance string) int32 { + ctx := cuecontext.New() + value := ctx.CompileString(policy) + + resolvedValue := ctx.CompileString(provenance, cue.Scope(value)) + resErr := resolvedValue.Err() + if resErr != nil { + // Unable to process the provenance. + return -1 + } + + validateErr := json.Validate([]byte(provenance), value) + if validateErr != nil { + // Validation failed. + return 0 + } + + // The provenance conforms with the policy. + return 1 +} diff --git a/golang/internal/cue_validator/cue_validator_test.go b/golang/internal/cuevalidator/cuevalidator_test.go similarity index 69% rename from golang/internal/cue_validator/cue_validator_test.go rename to golang/internal/cuevalidator/cuevalidator_test.go index c546e9fb0..a2f51ff42 100644 --- a/golang/internal/cue_validator/cue_validator_test.go +++ b/golang/internal/cuevalidator/cuevalidator_test.go @@ -1,12 +1,34 @@ -/* Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. */ /* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */ -package main +package cuevalidator import ( + "os" + "path" + "runtime" "testing" ) +// Get the path to the resources directory. +func GetResourcesPath(t *testing.T) string { + _, filename, _, ok := runtime.Caller(1) + if !ok { + t.Errorf("Unable to locate resources.") + } + return path.Join(path.Dir(filename), "resources") +} + +func LoadResource(t *testing.T, name string) string { + path := path.Join(GetResourcesPath(t), name) + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("Failed to read file: %s", err) + } + return string(data) +} + +// Test_Target tests the Target function for extracting the target from a CUE policy. func Test_Target(t *testing.T) { tests := []struct { name string @@ -16,7 +38,7 @@ func Test_Target(t *testing.T) { { name: "get target from invalid policy", path: "invalid_policy.cue", - expected: GetGoString(nil), + expected: "", }, { name: "get target from valid policy", @@ -28,16 +50,17 @@ func Test_Target(t *testing.T) { test := test // Re-initialize the test. t.Run(test.name, func(t *testing.T) { policy := LoadResource(t, test.path) - value := target(policy) + value := Target(policy) // GoLang doesn’t provide any built-in support for assert. - if GetGoString(value) != test.expected { - t.Errorf("Expected %s but got %s.", test.expected, GetGoString(value)) + if value != test.expected { + t.Errorf("Expected %s but got %s.", test.expected, value) } }) } } +// Test_ValidatePolicy tests the Validate function for validating the provenance against a CUE policy. func Test_ValidatePolicy(t *testing.T) { tests := []struct { name string @@ -82,7 +105,7 @@ func Test_ValidatePolicy(t *testing.T) { t.Run(test.name, func(t *testing.T) { policy := LoadResource(t, test.policy_path) provenance := LoadResource(t, test.provenance_path) - result := validate(policy, provenance) + result := Validate(policy, provenance) if result != test.expected { t.Errorf("Expected %d but got %d.", test.expected, result) } diff --git a/golang/internal/cue_validator/resources/invalid_policy.cue b/golang/internal/cuevalidator/resources/invalid_policy.cue similarity index 100% rename from golang/internal/cue_validator/resources/invalid_policy.cue rename to golang/internal/cuevalidator/resources/invalid_policy.cue diff --git a/golang/internal/cue_validator/resources/invalid_provenance.json b/golang/internal/cuevalidator/resources/invalid_provenance.json similarity index 100% rename from golang/internal/cue_validator/resources/invalid_provenance.json rename to golang/internal/cuevalidator/resources/invalid_provenance.json diff --git a/golang/internal/cue_validator/resources/valid_policy.cue b/golang/internal/cuevalidator/resources/valid_policy.cue similarity index 100% rename from golang/internal/cue_validator/resources/valid_policy.cue rename to golang/internal/cuevalidator/resources/valid_policy.cue diff --git a/golang/internal/cue_validator/resources/valid_provenance.json b/golang/internal/cuevalidator/resources/valid_provenance.json similarity index 100% rename from golang/internal/cue_validator/resources/valid_provenance.json rename to golang/internal/cuevalidator/resources/valid_provenance.json diff --git a/golang/internal/cue_validator/resources/valid_provenance2.json b/golang/internal/cuevalidator/resources/valid_provenance2.json similarity index 100% rename from golang/internal/cue_validator/resources/valid_provenance2.json rename to golang/internal/cuevalidator/resources/valid_provenance2.json diff --git a/src/macaron/config/global_config.py b/src/macaron/config/global_config.py index 8befb4045..0ef2c2849 100644 --- a/src/macaron/config/global_config.py +++ b/src/macaron/config/global_config.py @@ -94,12 +94,12 @@ def load_expectation_files(self, exp_path: str) -> None: exp_files = [] if os.path.isdir(exp_path): for policy_path in os.listdir(exp_path): - policy_file_path = os.path.join(exp_path, policy_path) + policy_file_path = os.path.abspath(os.path.join(exp_path, policy_path)) if os.path.isfile(policy_file_path): exp_files.append(policy_file_path) logger.info("Added provenance expectation file %s", os.path.relpath(policy_file_path, os.getcwd())) elif os.path.isfile(exp_path): - exp_files.append(exp_path) + exp_files.append(os.path.abspath(exp_path)) logger.info("Added provenance expectation file %s", os.path.relpath(exp_path, os.getcwd())) self.expectation_paths = exp_files diff --git a/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py b/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py index c457d316f..2f8caf3de 100644 --- a/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py +++ b/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py @@ -66,9 +66,9 @@ def make_expectation(cls, expectation_path: str) -> Self | None: with open(expectation_path, encoding="utf-8") as expectation_file: expectation.text = expectation_file.read() expectation.sha = str(hashlib.sha256(expectation.text.encode("utf-8")).hexdigest()) - expectation.target = cue_validator.get_target(expectation.text) + expectation.target = cue_validator.get_target(expectation_path) expectation._validator = ( # pylint: disable=protected-access - lambda provenance: cue_validator.validate_expectation(expectation.text, provenance) + lambda provenance_path: cue_validator.validate_expectation(expectation_path, provenance_path) ) except (OSError, CUERuntimeError, CUEExpectationError) as error: logger.error("CUE expectation error: %s", error) diff --git a/src/macaron/slsa_analyzer/provenance/expectations/cue/cue_validator.py b/src/macaron/slsa_analyzer/provenance/expectations/cue/cue_validator.py index 70e203af8..fc7e92c1b 100644 --- a/src/macaron/slsa_analyzer/provenance/expectations/cue/cue_validator.py +++ b/src/macaron/slsa_analyzer/provenance/expectations/cue/cue_validator.py @@ -1,28 +1,23 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """The cue module invokes the CUE schema validator.""" -import ctypes -import json import os -from collections.abc import Callable +import subprocess # nosec B404 from macaron import MACARON_PATH +from macaron.config.defaults import defaults from macaron.errors import CUEExpectationError, CUERuntimeError -from macaron.json_tools import JsonType -# Load the CUE shared library. -cue = ctypes.CDLL(os.path.join(MACARON_PATH, "bin", "cuevalidate.so")) - -def get_target(expectation: str | None) -> str: +def get_target(expectation_path: str | None) -> str: """Get the analysis target of the expectation. Parameters ---------- - expectation: str | None - The cue expectation content. + expectation_path: str | None + The cue expectation path. Returns ------- @@ -34,42 +29,45 @@ def get_target(expectation: str | None) -> str: CUERuntimeError, CUEExpectationError If expectation is invalid or unable to get the target by invoking the shared library. """ - if not expectation: - raise CUEExpectationError("CUE expectation is empty.") - - cue.target.restype = ctypes.c_void_p - - def _errcheck( - result: ctypes.c_void_p, func: Callable, args: tuple # pylint: disable=unused-argument - ) -> ctypes.c_void_p: - if not result: - raise CUERuntimeError("Unable to find target field in CUE expectation.") - return result - - cue.target.errcheck = _errcheck # type: ignore - expectation_buffer = ctypes.create_string_buffer(bytes(expectation, encoding="utf-8")) - target_ptr = cue.target(expectation_buffer) - res_bytes = ctypes.string_at(target_ptr) - - # Even though Python & Go have a garbage collector that will free up unused memory, - # the documentation says it is the caller's responsibility to free up the C string - # allocated memory. See https://pkg.go.dev/cmd/cgo - free = cue.free - free.argtypes = [ctypes.c_void_p] - free(target_ptr) - - return res_bytes.decode("utf-8") - - -def validate_expectation(expectation: str | None, prov: JsonType) -> bool: + if not expectation_path: + raise CUEExpectationError("CUE expectation path is not provided.") + + cmd = [ + os.path.join(MACARON_PATH, "bin", "cuevalidator"), + "-target-policy", + expectation_path, + ] + + try: + result = subprocess.run( # nosec B603 + cmd, + capture_output=True, + check=True, + cwd=MACARON_PATH, + timeout=defaults.getint("cue_validator", "timeout", fallback=30), + ) + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ) as error: + raise CUERuntimeError("Unable to process CUE expectation.") from error + + if result.returncode == 0: + return result.stdout.decode("utf-8") + + raise CUEExpectationError("Unable to find target field in CUE expectation.") + + +def validate_expectation(expectation_path: str, prov_stmt_path: str) -> bool: """Validate a json document against a cue expectation. Parameters ---------- - expectation: str | None - The cue expectation content. - prov: JsonType - The provenance payload. + expectation_path: str + The cue expectation path. + prov_stmt_path: str + The provenance statement path. Returns ------- @@ -78,20 +76,36 @@ def validate_expectation(expectation: str | None, prov: JsonType) -> bool: Raises ------ - CUERuntimeError, CUEExpectationError + CUERuntimeError If expectation is invalid or unable to validate the expectation by invoking the shared library. """ - if not expectation: - raise CUEExpectationError("CUE policies is empty.") - - expectation_buffer = ctypes.create_string_buffer(bytes(expectation, encoding="utf-8")) - prov_buffer = ctypes.create_string_buffer(bytes(json.dumps(prov), encoding="utf-8")) - - def _errcheck(result: int, func: Callable, args: tuple) -> int: # pylint: disable=unused-argument - if result == -1: - raise CUERuntimeError("Unable to validate the CUE expectation") - return result - - cue.target.errcheck = _errcheck # type: ignore - result = bool(cue.validate(expectation_buffer, prov_buffer)) - return result + cmd = [ + os.path.join(MACARON_PATH, "bin", "cuevalidator"), + "-validate-policy", + expectation_path, + "-validate-provenance", + prov_stmt_path, + ] + + try: + result = subprocess.run( # nosec B603 + cmd, + capture_output=True, + check=True, + cwd=MACARON_PATH, + timeout=defaults.getint("cue_validator", "timeout", fallback=30), + ) + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ) as error: + raise CUERuntimeError("Unable to process CUE expectation or provenance.") from error + + if result.returncode == 0: + if result.stdout.decode("utf-8") == "True": + return True + if result.stdout.decode("utf-8") == "False": + return False + + raise CUERuntimeError("Something unexpected happened while validating the provenance against CUE expectation.") diff --git a/src/macaron/slsa_analyzer/provenance/expectations/expectation.py b/src/macaron/slsa_analyzer/provenance/expectations/expectation.py index 093ba6625..69dc56df9 100644 --- a/src/macaron/slsa_analyzer/provenance/expectations/expectation.py +++ b/src/macaron/slsa_analyzer/provenance/expectations/expectation.py @@ -1,8 +1,10 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module provides a base class for provenance expectation verifiers.""" +import json +import tempfile from abc import abstractmethod from collections.abc import Callable from typing import Any, Self @@ -89,6 +91,12 @@ def validate(self, prov: InTotoPayload) -> bool: If there are errors happened during the validation process. """ if not self._validator: - raise ExpectationRuntimeError(f"Cannot find the validator for expectation {self.path}") + raise ExpectationRuntimeError(f"Unable to find the validator for expectation {self.path}") - return self._validator(prov.statement) # pylint: disable=not-callable + with tempfile.NamedTemporaryFile(suffix=".json", mode="w+", delete=True) as prov_stmt_file: + prov_stmt_file.write(json.dumps(prov.statement)) + # Rewind the file pointer before reading.. + prov_stmt_file.seek(0) + return self._validator(prov_stmt_file.name) # pylint: disable=not-callable + + raise ExpectationRuntimeError("Unable to validate the expectation.") diff --git a/tests/slsa_analyzer/provenance/expectations/cue/test_cue_validator.py b/tests/slsa_analyzer/provenance/expectations/cue/test_cue_validator.py index 71aa0b793..207b05fd2 100644 --- a/tests/slsa_analyzer/provenance/expectations/cue/test_cue_validator.py +++ b/tests/slsa_analyzer/provenance/expectations/cue/test_cue_validator.py @@ -1,14 +1,14 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module tests the CUE expectation validator.""" -import json import os from pathlib import Path import pytest +from macaron.errors import CUERuntimeError from macaron.slsa_analyzer.provenance.expectations.cue import CUEExpectation from macaron.slsa_analyzer.provenance.expectations.cue.cue_validator import get_target, validate_expectation @@ -37,16 +37,23 @@ def test_make_expectation(expectation_path: str) -> None: ("expectation_path", "expected"), [ (os.path.join(EXPECT_RESOURCE_PATH, "valid_expectations", "urllib3_PASS.cue"), PACKAGE_URLLIB3), - (os.path.join(EXPECT_RESOURCE_PATH, "valid_expectations", "urllib3_FAIL.cue"), ""), ], ) def test_get_target(expectation_path: str, expected: str) -> None: """Test getting target from valid CUE expectations.""" - expectation = CUEExpectation.make_expectation(expectation_path=expectation_path) - if expectation: - assert get_target(expectation.text) == expected - else: - raise ValueError("Expected a valid expectation.") + assert get_target(expectation_path) == expected + + +@pytest.mark.parametrize( + "expectation_path", + [ + os.path.join(EXPECT_RESOURCE_PATH, "valid_expectations", "urllib3_FAIL.cue"), + ], +) +def test_no_target(expectation_path: str) -> None: + """Test getting target from valid CUE expectations that misses a target.""" + with pytest.raises(CUERuntimeError): + get_target(expectation_path) @pytest.mark.parametrize( @@ -76,10 +83,4 @@ def test_get_target(expectation_path: str, expected: str) -> None: ) def test_validate_expectation(expectation_path: str, prov_path: str, expected: bool) -> None: """Test validating CUE expectations against provenances.""" - expectation = CUEExpectation.make_expectation(expectation_path=expectation_path) - if expectation: - with open(prov_path, encoding="utf-8") as prov_file: - provenance = json.load(prov_file) - assert validate_expectation(expectation.text, provenance) == expected - else: - raise ValueError("Expected a valid expectation.") + assert validate_expectation(expectation_path, prov_path) == expected