From 02a432cfec2e968a8e421a63b945873c5f68b8dc Mon Sep 17 00:00:00 2001 From: Midia Kiasat Date: Sat, 28 Feb 2026 13:36:14 +0100 Subject: [PATCH 1/2] fix: deterministic exit codes + selftest control (keep marketplace pass default) --- action.yml | 8 ++++++-- gate.sh | 45 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/action.yml b/action.yml index ad2be8a..dc7161c 100644 --- a/action.yml +++ b/action.yml @@ -10,6 +10,10 @@ inputs: description: "pass|enforce" required: false default: "pass" + profile: + description: "Path to policy profile file (required for enforce)" + required: false + default: "" runs: using: "composite" @@ -18,6 +22,6 @@ runs: shell: bash env: CICULLIS_MODE: ${{ inputs.mode }} + CICULLIS_PROFILE: ${{ inputs.profile }} run: | - chmod +x "$GITHUB_ACTION_PATH/gate.sh" - "$GITHUB_ACTION_PATH/gate.sh" + bash "$GITHUB_ACTION_PATH/gate.sh" diff --git a/gate.sh b/gate.sh index a9d362e..f7a0056 100755 --- a/gate.sh +++ b/gate.sh @@ -1,24 +1,49 @@ #!/usr/bin/env bash set -euo pipefail -# CICULLIS default: no-op PASS unless explicit policy env is set. -# This keeps Marketplace smoke green while preserving deterministic "fail-closed" option. -# If you want enforcement: set CICULLIS_MODE=enforce and provide your own checks. - mode="${CICULLIS_MODE:-pass}" +profile="${CICULLIS_PROFILE:-}" + +# Exit codes (deterministic): +# 0 = pass +# 1 = enforcement fail (policy says NO) +# 2 = malformed profile / invalid configuration + +die2(){ echo "CICULLIS: $*" >&2; exit 2; } +die1(){ echo "CICULLIS: $*" >&2; exit 1; } + +# If a profile is provided, it must be readable. +if [[ -n "$profile" ]]; then + [[ -f "$profile" ]] || die2 "profile not found: $profile" +fi + +# Minimal "malformed profile" detector: +# - if profile exists but is empty -> malformed +# - if it contains any NUL byte -> malformed +# - if it contains a line starting with "MALFORMED:" -> malformed (test hook) +if [[ -n "$profile" ]]; then + [[ -s "$profile" ]] || die2 "profile empty: $profile" + if LC_ALL=C grep -q $'\x00' "$profile" 2>/dev/null; then die2 "profile contains NUL: $profile"; fi + if grep -qE '^[[:space:]]*MALFORMED:' "$profile" 2>/dev/null; then die2 "profile marked malformed: $profile"; fi +fi case "$mode" in pass) - echo "CICULLIS: pass (default). Set CICULLIS_MODE=enforce to activate enforcement." + echo "CICULLIS: pass" exit 0 ;; enforce) - echo "CICULLIS: enforce requested, but no policy configured in this runner." - echo "Set CICULLIS_POLICY or vendor your policy checks." - exit 1 + # No profile => deterministic config error for enforcement mode + [[ -n "$profile" ]] || die2 "enforce requires CICULLIS_PROFILE" + # Placeholder for real policy evaluation: + # If profile contains "DENY" => fail-closed (1), else pass (0) + if grep -qE '(^|[[:space:]])DENY($|[[:space:]])' "$profile" 2>/dev/null; then + die1 "policy denied by profile: $profile" + fi + echo "CICULLIS: enforce pass (profile=$profile)" + exit 0 ;; *) - echo "CICULLIS: invalid CICULLIS_MODE=$mode (use pass|enforce)" - exit 2 + die2 "invalid CICULLIS_MODE=$mode (use pass|enforce)" ;; esac From ba93ac3b69ae6ec069bebd4f188bb2f56ba3cffb Mon Sep 17 00:00:00 2001 From: Midia Kiasat Date: Sat, 28 Feb 2026 13:45:03 +0100 Subject: [PATCH 2/2] fix: malformed profile => exit 2 (json-validated) while keeping pass default --- gate.sh | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/gate.sh b/gate.sh index f7a0056..1133193 100755 --- a/gate.sh +++ b/gate.sh @@ -4,27 +4,28 @@ set -euo pipefail mode="${CICULLIS_MODE:-pass}" profile="${CICULLIS_PROFILE:-}" -# Exit codes (deterministic): +# Exit codes: # 0 = pass -# 1 = enforcement fail (policy says NO) -# 2 = malformed profile / invalid configuration - +# 1 = enforcement fail +# 2 = malformed profile / invalid config die2(){ echo "CICULLIS: $*" >&2; exit 2; } die1(){ echo "CICULLIS: $*" >&2; exit 1; } -# If a profile is provided, it must be readable. +# Profile validation (always enforced if profile is provided) if [[ -n "$profile" ]]; then [[ -f "$profile" ]] || die2 "profile not found: $profile" -fi - -# Minimal "malformed profile" detector: -# - if profile exists but is empty -> malformed -# - if it contains any NUL byte -> malformed -# - if it contains a line starting with "MALFORMED:" -> malformed (test hook) -if [[ -n "$profile" ]]; then [[ -s "$profile" ]] || die2 "profile empty: $profile" if LC_ALL=C grep -q $'\x00' "$profile" 2>/dev/null; then die2 "profile contains NUL: $profile"; fi - if grep -qE '^[[:space:]]*MALFORMED:' "$profile" 2>/dev/null; then die2 "profile marked malformed: $profile"; fi + # Must be valid JSON (selftests expect deterministic malformed=2) + python3 - "$profile" <<'PY' || exit 2 +import json,sys +p=sys.argv[1] +try: + with open(p,"rb") as f: + json.load(f) +except Exception: + raise SystemExit(2) +PY fi case "$mode" in @@ -33,10 +34,8 @@ case "$mode" in exit 0 ;; enforce) - # No profile => deterministic config error for enforcement mode [[ -n "$profile" ]] || die2 "enforce requires CICULLIS_PROFILE" - # Placeholder for real policy evaluation: - # If profile contains "DENY" => fail-closed (1), else pass (0) + # Placeholder enforcement: profile contains token "DENY" => exit 1 if grep -qE '(^|[[:space:]])DENY($|[[:space:]])' "$profile" 2>/dev/null; then die1 "policy denied by profile: $profile" fi