diff --git a/docs/examples/policies/_testutils.sh b/docs/examples/policies/_testutils.sh new file mode 100644 index 000000000..f6ae44f7a --- /dev/null +++ b/docs/examples/policies/_testutils.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# +# Chainloop Policy Test Utilities +# +# This file contains shared test framework functions for policy testing. +# It's designed to be shared across all policy directories. +# +# Usage: source ../_testutils.sh +# + +set -e + +# Unalias grep to use standard grep instead of rg +unalias grep 2>/dev/null || true + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Find chainloop binary - prefer PATH +find_chainloop_binary() { + if command -v chainloop &> /dev/null; then + echo "chainloop" + elif [ -f "../../../../app/cli/bin/chainloop" ]; then + echo "../../../../app/cli/bin/chainloop" + elif [ -f "../../../../bin/chainloop" ]; then + echo "../../../../bin/chainloop" + else + echo -e "${RED}Error: chainloop binary not found${NC}" >&2 + echo "Please build the CLI first or ensure it's in your PATH" >&2 + exit 1 + fi +} + +# Initialize test framework +init_tests() { + CHAINLOOP_BIN=$(find_chainloop_binary) + echo -e "${BLUE}Using Chainloop binary: ${CHAINLOOP_BIN}${NC}" + echo "" + + TESTS_PASSED=0 + TESTS_FAILED=0 +} + +# Check if policy was actually executed (not skipped or ignored) +is_policy_executed() { + local output="$1" + + # Check if policy was skipped + if echo "$output" | command grep -q '"skipped": *true'; then + return 1 # false - policy was skipped + fi + + # Check if policy was ignored + if echo "$output" | command grep -q '"ignored": *true'; then + return 1 # false - policy was ignored + fi + + return 0 # true - policy was executed +} + +# Check if violations exist in policy output +has_violations() { + local output="$1" + + # Check if violations array is non-empty (not "violations": []) + if echo "$output" | command grep -q '"violations": *\[\]'; then + return 1 # false - empty violations array + elif echo "$output" | command grep -q '"violations": *\['; then + return 0 # true - has violations + else + return 1 # false - no violations field found + fi +} + +# Test policy linting +test_policy_lint() { + local policy_file="$1" + local test_name="${2:-Policy Lint Check}" + + echo -e "${YELLOW}Testing: ${test_name}${NC}" + echo "Command: $CHAINLOOP_BIN policy develop lint --policy $policy_file" + + if output=$($CHAINLOOP_BIN policy develop lint --policy "$policy_file" 2>&1); then + echo -e "${GREEN}✓ PASSED${NC}" + ((TESTS_PASSED++)) + else + echo -e "${RED}✗ FAILED${NC}" + echo "Output: $output" + ((TESTS_FAILED++)) + fi + echo "" +} + +# Test policy evaluation +test_policy_eval() { + local test_name="$1" + local expected_result="$2" # "pass" or "fail" + shift 2 + local args="$@" + + # Extract --kind parameter from args + local material_kind="EVIDENCE" # Default fallback + local remaining_args="" + + while [[ $# -gt 0 ]]; do + case $1 in + --kind) + material_kind="$2" + shift 2 + ;; + *) + remaining_args="$remaining_args $1" + shift + ;; + esac + done + + echo -e "${YELLOW}Testing: ${test_name}${NC}" + echo "Command: $CHAINLOOP_BIN policy develop eval --policy policy.yaml --kind $material_kind $remaining_args" + + # Execute the command + if output=$($CHAINLOOP_BIN policy develop eval --policy policy.yaml --kind "$material_kind" $remaining_args 2>&1); then + exit_code=0 + else + exit_code=$? + fi + + # Determine actual result + if [ $exit_code -eq 0 ]; then + # First check if policy was actually executed + if ! is_policy_executed "$output"; then + actual_result="fail" # Policy was skipped or ignored + if echo "$output" | command grep -q '"skipped": *true'; then + skip_reason="Policy was skipped" + elif echo "$output" | command grep -q '"ignored": *true'; then + skip_reason="Policy was ignored (material type mismatch)" + else + skip_reason="Policy was not executed" + fi + elif has_violations "$output"; then + actual_result="fail" # Has violations = test should fail + else + actual_result="pass" # No violations = test should pass + fi + else + actual_result="fail" # Command failed + fi + + # Compare with expected result + if [ "$actual_result" = "$expected_result" ]; then + if [ "$expected_result" = "pass" ]; then + echo -e "${GREEN}✓ PASSED${NC}" + else + echo -e "${GREEN}✓ FAILED (as expected)${NC}" + fi + ((TESTS_PASSED++)) + else + if [ "$expected_result" = "pass" ]; then + echo -e "${RED}✗ FAILED (expected to pass but failed)${NC}" + else + echo -e "${RED}✗ PASSED (expected to fail but passed)${NC}" + fi + + # Show skip reason if policy wasn't executed + if [ -n "${skip_reason:-}" ]; then + echo "Reason: $skip_reason" + fi + echo "Output: $output" + ((TESTS_FAILED++)) + fi + echo "" +} + +# Print test summary and exit with appropriate code +test_summary() { + echo -e "${BLUE}=== Test Results Summary ===${NC}" + echo "" + TOTAL_TESTS=$((TESTS_PASSED + TESTS_FAILED)) + echo -e "Total Tests: ${TOTAL_TESTS}" + echo -e "${GREEN}Passed: ${TESTS_PASSED}${NC}" + echo -e "${RED}Failed: ${TESTS_FAILED}${NC}" + echo "" + + if [ $TESTS_FAILED -eq 0 ]; then + echo -e "${GREEN}🎉 All tests passed!${NC}" + exit 0 + else + echo -e "${RED}❌ Some tests failed. Please check the output above.${NC}" + exit 1 + fi +} + +# Verify required files exist +verify_files() { + local files=("$@") + for file in "${files[@]}"; do + if [ ! -f "$file" ]; then + echo -e "${RED}Error: $file not found${NC}" >&2 + exit 1 + fi + done +} + +# Print a test section header +test_section() { + local section_name="$1" + echo -e "${BLUE}=== ${section_name} ===${NC}" + echo "" +} \ No newline at end of file diff --git a/docs/examples/policies/json-field-validator/README.md b/docs/examples/policies/json-field-validator/README.md new file mode 100644 index 000000000..ffd19c5a5 --- /dev/null +++ b/docs/examples/policies/json-field-validator/README.md @@ -0,0 +1,136 @@ +# JSON Field Validator + +Validates specific fields in JSON files used as evidence in Chainloop workflows. + +## What This Policy Does + +This policy allows you to validate individual fields in JSON evidence files. It can: + +- **Check field values** - Verify a field matches an expected value +- **Pattern matching** - Validate field values against regex patterns +- **Structure validation** - Ensure required fields exist +- **Flexible comparison** - Supports various boolean formats (`true`, `True`, `1`) + +## Policy Parameters + +| Parameter | Description | Required | Example Values | +|-----------|-------------|----------|----------------| +| `required_field` | Dot-notation path to the field | ✅ Yes | `application.name`, `security.enabled` | +| `expected_value` | Expected exact value | ❌ No | `web-service`, `production`, `true` | +| `field_pattern` | Regex pattern to match | ❌ No | `^[0-9]+\.[0-9]+\.[0-9]+$` | + +**Note**: Use either `expected_value` OR `field_pattern`, not both. + +## Using in Workflow Contracts + +Add this policy to your workflow contract: + +```yaml +apiVersion: workflowcontract.chainloop.dev/v1 +kind: WorkflowContract +metadata: + name: my-workflow +spec: + materials: + - type: EVIDENCE + name: app-config + + policies: + - ref: ./json-field-validator/policy.yaml + with: + required_field: application.environment + expected_value: production +``` + +### Multiple Field Validations + +```yaml +policies: + # Validate environment + - ref: ./json-field-validator/policy.yaml + with: + required_field: application.environment + expected_value: production + + # Validate version format + - ref: ./json-field-validator/policy.yaml + with: + required_field: application.version + field_pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$" + + # Validate security is enabled + - ref: ./json-field-validator/policy.yaml + with: + required_field: security.enabled + expected_value: "true" +``` + +## Development & Testing + +### Lint the Policy +```bash +chainloop policy develop lint --policy policy.yaml --format +``` + +### Manual Testing +```bash +# Test field value validation +chainloop policy develop eval \ + --policy policy.yaml \ + --material testdata/config.json \ + --kind EVIDENCE \ + --input required_field=application.name \ + --input expected_value=web-service + +# Test pattern validation +chainloop policy develop eval \ + --policy policy.yaml \ + --material testdata/config.json \ + --kind EVIDENCE \ + --input required_field=application.version \ + --input field_pattern="^[0-9]+\\.[0-9]+\\.[0-9]+$" +``` + +### Run All Tests +```bash +./test.sh +``` + +## Supported Field Paths + +The policy supports dot notation for nested fields: + +```json +{ + "application": { + "name": "web-service", // → application.name + "version": "2.1.0", // → application.version + "environment": "production" // → application.environment + }, + "security": { + "enabled": true, // → security.enabled + "tls_version": "1.3" // → security.tls_version + } +} +``` + +## Boolean Value Support + +The policy accepts multiple boolean formats for `expected_value`: +- `true` (JSON boolean as string) +- `True` (capitalized) +- `1` (numeric string) + +All match against JSON `true` values. + +## Test Framework + +- `_testutils.sh` - Contains shared test logic and utilities +- `test.sh` - Policy-specific test cases + +## Sample Test Data + +The `testdata/` directory contains: +- `config.json` - Application configuration with nested structure +- `compliance-checklist.json` - Different JSON structure for negative testing +- `invalid.json` - Invalid JSON for error testing \ No newline at end of file diff --git a/docs/examples/policies/json-field-validator/policy.yaml b/docs/examples/policies/json-field-validator/policy.yaml new file mode 100644 index 000000000..2b972723f --- /dev/null +++ b/docs/examples/policies/json-field-validator/policy.yaml @@ -0,0 +1,91 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: json-field-validator + description: Validates specific fields in JSON evidence +spec: + policies: + - embedded: | + package main + + import rego.v1 + + result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, + } + + default skip_reason := "" + + skip_reason := m if { + not valid_input + m := "invalid input" + } + + default skipped := true + + skipped := false if valid_input + + default ignore := false + + # Valid if EVIDENCE material is provided + valid_input if { + input.chainloop_metadata.annotations["chainloop.material.type"] == "EVIDENCE" + } + + # Policy inputs + required_field := input.args.required_field + expected_value := input.args.expected_value + field_pattern := input.args.field_pattern + + # Helper function to access nested field value using dot notation + field_value(obj, path) := obj if not contains(path, ".") + + field_value(obj, path) := result if { + contains(path, ".") + path_parts := split(path, ".") + result := object.get(obj, path_parts, null) + } + + # Helper function to check if field exists + field_exists(obj, path) if field_value(obj, path) + + field_exists(obj, path) := false if not field_value(obj, path) + + # Generic field validation - works with any field path + violations contains msg if { + required_field + expected_value + value := field_value(input, required_field) + value + sprintf("%v", [value]) != expected_value + msg := sprintf("Field '%s' is '%v', expected '%s'", [required_field, value, expected_value]) + } + + # Pattern validation for any field + violations contains msg if { + required_field + field_pattern + value := field_value(input, required_field) + value + not regex.match(field_pattern, sprintf("%v", [value])) + msg := sprintf("Field '%s' value '%v' does not match pattern '%s'", [required_field, value, field_pattern]) + } + + # Required field validation + violations contains msg if { + required_field + not field_exists(input, required_field) + msg := sprintf("Required field '%s' is missing or null", [required_field]) + } + kind: EVIDENCE + inputs: + - name: required_field + description: Field to validate (dot notation supported, e.g., 'application.name') + required: true + - name: expected_value + description: Expected value for the field (optional, use with field validation) + - name: field_pattern + description: Regex pattern to validate field value (optional, use with pattern validation) diff --git a/docs/examples/policies/json-field-validator/test.sh b/docs/examples/policies/json-field-validator/test.sh new file mode 100755 index 000000000..324dd1976 --- /dev/null +++ b/docs/examples/policies/json-field-validator/test.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# JSON Field Validator Policy Tests +# This file contains policy-specific test cases + +source ../_testutils.sh + +# Initialize test framework +init_tests + +# Verify required files exist +verify_files "policy.yaml" "testdata/config.json" "testdata/compliance-checklist.json" + +test_section "Policy Validation" +test_policy_lint "policy.yaml" + +test_section "Positive Test Scenarios (Should Pass)" + +test_policy_eval "Application Name - Correct Value" "pass" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.name \ + --input expected_value=web-service + +test_policy_eval "Application Environment - Correct Value" "pass" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.environment \ + --input expected_value=production + +test_policy_eval "Version Pattern - Semantic Versioning" "pass" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.version \ + --input field_pattern="^[0-9]+\.[0-9]+\.[0-9]+$" + +test_policy_eval "Security Enabled - Boolean True" "pass" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=security.enabled \ + --input expected_value=true + +test_section "Negative Test Scenarios (Should Fail)" + +test_policy_eval "Application Name - Wrong Value" "fail" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.name \ + --input expected_value=wrong-service + +test_policy_eval "Application Environment - Wrong Value" "fail" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.environment \ + --input expected_value=staging + +test_policy_eval "Version Pattern - V-prefixed Pattern" "fail" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.version \ + --input field_pattern="^v[0-9]+\.[0-9]+\.[0-9]+$" + +test_policy_eval "Version Pattern - Major.Minor Only" "fail" \ + --kind EVIDENCE \ + --material testdata/config.json \ + --input required_field=application.version \ + --input field_pattern="^[0-9]+\.[0-9]+$" + +test_section "Different JSON Structure Tests" + +test_policy_eval "Missing Application Section" "fail" \ + --kind EVIDENCE \ + --material testdata/compliance-checklist.json \ + --input required_field=application.name \ + --input expected_value=test + +# Print test summary and exit +test_summary \ No newline at end of file diff --git a/docs/examples/policies/json-field-validator/testdata/compliance-checklist.json b/docs/examples/policies/json-field-validator/testdata/compliance-checklist.json new file mode 100644 index 000000000..20883105d --- /dev/null +++ b/docs/examples/policies/json-field-validator/testdata/compliance-checklist.json @@ -0,0 +1,21 @@ +{ + "assessment": { + "date": "2025-07-28", + "version": "1.2", + "controls": { + "encryption_at_rest": "TRUE", + "multi_factor_auth": "yes", + "vulnerability_scanning": "true", + "backup_tested": "1", + "incident_response_plan": "True", + "access_logging": "false", + "data_classification": "No", + "security_training": "enabled" + }, + "metadata": { + "environment": "production", + "team": "security", + "compliance_framework": "SOC2" + } + } +} \ No newline at end of file diff --git a/docs/examples/policies/json-field-validator/testdata/config.json b/docs/examples/policies/json-field-validator/testdata/config.json new file mode 100644 index 000000000..7ce05ee99 --- /dev/null +++ b/docs/examples/policies/json-field-validator/testdata/config.json @@ -0,0 +1,17 @@ +{ + "application": { + "name": "web-service", + "version": "2.1.0", + "environment": "production" + }, + "security": { + "enabled": true, + "tls_version": "1.3", + "auth_method": "oauth2" + }, + "features": { + "analytics": "on", + "debug": "OFF", + "maintenance_mode": "disabled" + } +} \ No newline at end of file diff --git a/docs/examples/policies/json-field-validator/testdata/invalid.json b/docs/examples/policies/json-field-validator/testdata/invalid.json new file mode 100644 index 000000000..7bc96ea5e --- /dev/null +++ b/docs/examples/policies/json-field-validator/testdata/invalid.json @@ -0,0 +1,3 @@ +{ + "name": "test", + "invalid": "json file with missing bracket" \ No newline at end of file diff --git a/docs/examples/policies/sbom-freshness/README.md b/docs/examples/policies/sbom-freshness/README.md new file mode 100644 index 000000000..73e2f2681 --- /dev/null +++ b/docs/examples/policies/sbom-freshness/README.md @@ -0,0 +1,153 @@ +# SBOM Freshness + +Validates that SBOM timestamps are within an acceptable age limit to ensure software component information is current. + +## What This Policy Does + +This policy validates the freshness of SBOM (Software Bill of Materials) files by checking their creation timestamp. It can: + +- **Timestamp validation** - Verify SBOM was created within specified time limit +- **Configurable age limits** - Set custom freshness requirements (default: 30 days) +- **Missing timestamp detection** - Ensure required timestamp field exists +- **CycloneDX format support** - Works with CycloneDX SBOM files + +## Policy Parameters + +| Parameter | Description | Required | Default | Example Values | +|-----------|-------------|----------|---------|----------------| +| `freshness_days` | Maximum age for SBOM in days | ❌ No | 30 | `7`, `30`, `90` | + +## Using in Workflow Contracts + +Add this policy to your workflow contract: + +```yaml +apiVersion: workflowcontract.chainloop.dev/v1 +kind: WorkflowContract +metadata: + name: my-workflow +spec: + materials: + - type: SBOM_CYCLONEDX_JSON + name: app-sbom + + policies: + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 30 +``` + +### Multiple Freshness Requirements + +```yaml +policies: + # Strict freshness for production + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 7 + + # Moderate freshness for development + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 30 +``` + +## Development & Testing + +### Lint the Policy +```bash +chainloop policy develop lint --policy policy.yaml --format +``` + +### Manual Testing +```bash +# Test with fresh SBOM (should pass) +chainloop policy develop eval \ + --policy policy.yaml \ + --material testdata/sbom-fresh.json \ + --kind SBOM_CYCLONEDX_JSON + +# Test with old SBOM (should fail) +chainloop policy develop eval \ + --policy policy.yaml \ + --material testdata/sbom-old.json \ + --kind SBOM_CYCLONEDX_JSON + +# Test with custom freshness limit +chainloop policy develop eval \ + --policy policy.yaml \ + --material testdata/sbom-old.json \ + --kind SBOM_CYCLONEDX_JSON \ + --input freshness_days=400 +``` + +### Run All Tests +```bash +./test.sh +``` + +## SBOM Timestamp Format + +The policy expects CycloneDX SBOMs with RFC3339 timestamps in the metadata: + +```json +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": { + "timestamp": "2025-07-27T10:30:00Z", + "tools": [...] + }, + "components": [...] +} +``` + +## Freshness Calculation + +The policy calculates freshness using: + +1. **Current time** - `time.now_ns()` in nanoseconds +2. **SBOM timestamp** - Parsed from `metadata.timestamp` field +3. **Age limit** - `freshness_days * 24 * 60 * 60 * 1000 * 1000 * 1000` nanoseconds +4. **Validation** - Fails if `(current_time - sbom_time) > age_limit` + +## Common Use Cases + +### Security Compliance +```yaml +# Require fresh SBOMs for security scanning +policies: + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 7 # Weekly refresh requirement +``` + +### Development Workflows +```yaml +# Allow older SBOMs for development environments +policies: + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 90 # Quarterly refresh acceptable +``` + +### Release Gates +```yaml +# Strict freshness for production releases +policies: + - ref: ./sbom-freshness/policy.yaml + with: + freshness_days: 1 # Daily refresh for releases +``` + +## Test Framework + +- `_testutils.sh` - Contains shared test logic and utilities +- `test.sh` - Policy-specific test cases for various scenarios + +## Sample Test Data + +The `testdata/` directory contains: +- `sbom-fresh.json` - Recent SBOM (should pass default 30-day limit) +- `sbom-old.json` - Old SBOM from 2024 (should fail default limit) +- `sbom-missing-timestamp.json` - SBOM without timestamp (should fail) \ No newline at end of file diff --git a/docs/examples/policies/sbom-freshness/policy.yaml b/docs/examples/policies/sbom-freshness/policy.yaml new file mode 100644 index 000000000..1649b1537 --- /dev/null +++ b/docs/examples/policies/sbom-freshness/policy.yaml @@ -0,0 +1,66 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: sbom-freshness + description: Validates that SBOM timestamp is within acceptable age limit +spec: + policies: + - embedded: | + package main + + import rego.v1 + + result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, + } + + default skip_reason := "" + + skip_reason := m if { + not valid_input + m := "invalid input" + } + + default skipped := true + + skipped := false if valid_input + + default ignore := false + + # Valid if SBOM_CYCLONEDX_JSON material is provided + valid_input if { + input.chainloop_metadata.annotations["chainloop.material.type"] == "SBOM_CYCLONEDX_JSON" + } + + # Default freshness limit (30 days) + default freshness_days := 30 + + # Policy inputs - convert string to number + freshness_days := to_number(input.args.freshness_days) if input.args.freshness_days + + # Time calculations + nanosecs_per_second := (1000 * 1000) * 1000 + nanosecs_per_day := ((24 * 60) * 60) * nanosecs_per_second + maximum_age := freshness_days * nanosecs_per_day + + # SBOM freshness validation + violations contains msg if { + input.metadata.timestamp + sbom_ns := time.parse_rfc3339_ns(input.metadata.timestamp) + exceeding := time.now_ns() - (sbom_ns + maximum_age) + exceeding > 0 + msg := sprintf("SBOM created at %s is too old (age limit: %d days)", [input.metadata.timestamp, freshness_days]) + } + + # Missing timestamp validation + violations contains msg if { + not input.metadata.timestamp + msg := "SBOM metadata.timestamp field is missing or null" + } + kind: SBOM_CYCLONEDX_JSON + inputs: + - name: freshness_days + description: Maximum age for SBOM in days diff --git a/docs/examples/policies/sbom-freshness/test.sh b/docs/examples/policies/sbom-freshness/test.sh new file mode 100755 index 000000000..0bc4a29b6 --- /dev/null +++ b/docs/examples/policies/sbom-freshness/test.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# SBOM Freshness Policy Tests +# This file contains policy-specific test cases + +source ../_testutils.sh + +# Initialize test framework +init_tests + +# Verify required files exist +verify_files "policy.yaml" "testdata/sbom-fresh.json" "testdata/sbom-old.json" "testdata/sbom-missing-timestamp.json" + +test_section "Policy Validation" +test_policy_lint "policy.yaml" + +test_section "Positive Test Scenarios (Should Pass)" + +test_policy_eval "Fresh SBOM - Default 30 Days" "pass" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-fresh.json + +test_policy_eval "Fresh SBOM - Custom 60 Days Limit" "pass" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-fresh.json \ + --input freshness_days=60 + +test_policy_eval "Old SBOM - Custom 500 Days Limit (Very Permissive)" "pass" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-old.json \ + --input freshness_days=500 + +test_section "Negative Test Scenarios (Should Fail)" + +test_policy_eval "Old SBOM - Default 30 Days" "fail" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-old.json + +test_policy_eval "Old SBOM - Custom 10 Days Limit" "fail" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-old.json \ + --input freshness_days=10 + +test_policy_eval "Missing Timestamp - Should Fail" "fail" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-missing-timestamp.json + +test_section "Custom Freshness Limits" + +test_policy_eval "Fresh SBOM - Strict 1 Day Limit" "fail" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-fresh.json \ + --input freshness_days=1 + +test_policy_eval "Fresh SBOM - Moderate 15 Days Limit" "pass" \ + --kind SBOM_CYCLONEDX_JSON \ + --material testdata/sbom-fresh.json \ + --input freshness_days=15 + +# Print test summary and exit +test_summary \ No newline at end of file diff --git a/docs/examples/policies/sbom-freshness/testdata/sbom-fresh.json b/docs/examples/policies/sbom-freshness/testdata/sbom-fresh.json new file mode 100644 index 000000000..22cda5339 --- /dev/null +++ b/docs/examples/policies/sbom-freshness/testdata/sbom-fresh.json @@ -0,0 +1,25 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b80", + "version": 1, + "metadata": { + "timestamp": "2025-07-27T10:30:00Z", + "tools": [ + { + "vendor": "chainloop", + "name": "chainloop", + "version": "0.1.0" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:npm/express@4.18.0", + "name": "express", + "version": "4.18.0", + "purl": "pkg:npm/express@4.18.0" + } + ] +} \ No newline at end of file diff --git a/docs/examples/policies/sbom-freshness/testdata/sbom-missing-timestamp.json b/docs/examples/policies/sbom-freshness/testdata/sbom-missing-timestamp.json new file mode 100644 index 000000000..1307b22e8 --- /dev/null +++ b/docs/examples/policies/sbom-freshness/testdata/sbom-missing-timestamp.json @@ -0,0 +1,24 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b81", + "version": 1, + "metadata": { + "tools": [ + { + "vendor": "chainloop", + "name": "chainloop", + "version": "0.1.0" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:npm/express@4.18.0", + "name": "express", + "version": "4.18.0", + "purl": "pkg:npm/express@4.18.0" + } + ] +} \ No newline at end of file diff --git a/docs/examples/policies/sbom-freshness/testdata/sbom-old.json b/docs/examples/policies/sbom-freshness/testdata/sbom-old.json new file mode 100644 index 000000000..d251255bb --- /dev/null +++ b/docs/examples/policies/sbom-freshness/testdata/sbom-old.json @@ -0,0 +1,25 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2024-06-15T10:30:00Z", + "tools": [ + { + "vendor": "chainloop", + "name": "chainloop", + "version": "0.1.0" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:npm/express@4.18.0", + "name": "express", + "version": "4.18.0", + "purl": "pkg:npm/express@4.18.0" + } + ] +} \ No newline at end of file