Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .github/actions/pulumi-guarded/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ inputs:
required: false
default: ""

output-file:
description: >-
Optional workspace-relative path for scanned stdout instead of logging it,
independent of work-dir. Absolute paths, .. components, directories, symlink
targets and parents resolving outside GITHUB_WORKSPACE are rejected. The
parent must exist. An existing file is removed before running. Only
successful output with clean stdout and stderr scans is published atomically
with mode 0600. Clean stderr is logged separately. Use command stack with
args 'output <name>' or preview with '--json'; the same invocation refusals
apply in every mode, including config get and --show-secrets.
required: false
default: ""

runs:
using: composite
steps:
Expand All @@ -38,6 +51,7 @@ runs:
PULUMI_GUARD_COMMAND: ${{ inputs.command }}
PULUMI_GUARD_STACK: ${{ inputs.stack-name }}
PULUMI_GUARD_ARGS: ${{ inputs.args }}
PULUMI_GUARD_OUTPUT_FILE: ${{ inputs.output-file }}
PULUMI_GUARD_SCRIPT: ${{ github.action_path }}/../../scripts/pulumi-guarded.sh
run: |
set -euo pipefail
Expand Down
110 changes: 96 additions & 14 deletions .github/scripts/pulumi-guarded.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# Config reads, ESC plaintext reads and explicit secret output are refused before invocation.
# Exit status: 1 when admission or scanning fails (wins over Pulumi's status); otherwise
# Pulumi's own status. Pattern scanning cannot identify all unarmoured secrets.
# PULUMI_GUARD_OUTPUT_FILE optionally publishes clean stdout as a private file instead
# of logging it; stderr is scanned separately.
set -euo pipefail
umask 077

Expand All @@ -31,13 +33,9 @@ admit_command() {
return 0
}

if ! admit_command "$@"; then
echo "::error title=Pulumi output withheld::Pulumi invocation refused."
exit 1
fi

output="$(mktemp)"
trap 'rm -f "$output"' EXIT
temporary_files=("$output")
trap 'rm -f -- "${temporary_files[@]}"' EXIT
trap 'exit 143' TERM
trap 'exit 130' INT

Expand All @@ -55,26 +53,103 @@ CLASSES=(
# grep exit: 0 match, 1 none, anything else = not checked. Prints the status.
match_status() {
local rc=0
grep -Eq -e "$1" "$output" || rc=$?
grep -Eq -e "$1" "$2" || rc=$?
echo "$rc"
}

invalid_path() {
echo "::error title=Invalid Pulumi output path::Use a workspace-relative file with an existing parent inside GITHUB_WORKSPACE, without .. components, directories or symlink targets." >&2
exit 1
}

prepare_destination() {
local requested="$1" parent root
[[ "$requested" =~ [^[:space:]] ]] || invalid_path
case "/$requested/" in
//*|*/../*) invalid_path ;;
esac
[ -n "${GITHUB_WORKSPACE:-}" ] || invalid_path
requested="$GITHUB_WORKSPACE/$requested"
[ ! -d "$requested" ] && [ ! -L "$requested" ] || invalid_path
root="$(cd -- "$GITHUB_WORKSPACE" 2>/dev/null && pwd -P)" || invalid_path
parent="$(cd -- "$(dirname -- "$requested")" 2>/dev/null && pwd -P)" || invalid_path
case "$parent/" in
"$root/"*) ;;
*) invalid_path ;;
esac
workspace_root="$root"
destination_parent="$parent"
destination="$parent/$(basename -- "$requested")"
rm -f -- "$destination"
}

publication_path_is_safe() {
local parent
parent="$(cd -- "$destination_parent" 2>/dev/null && pwd -P)" || return 1
[ "$parent" = "$destination_parent" ] || return 1
case "$parent/" in
"$workspace_root/"*) ;;
*) return 1 ;;
esac
[ ! -L "$destination" ] && [ ! -d "$destination" ]
}

publication_failed() {
echo "::error title=Pulumi output withheld::Pulumi output file could not be published." >&2
exit 1
}

publish_destination() {
local staged
publication_path_is_safe || publication_failed
staged="$(mktemp "$destination_parent/.pulumi-guarded.XXXXXXXX" 2>/dev/null)" || publication_failed
temporary_files+=("$staged")
cat "$output" > "$staged" || publication_failed
chmod 0600 "$staged" 2>/dev/null || publication_failed
publication_path_is_safe || publication_failed
mv -f -T -- "$staged" "$destination" 2>/dev/null || publication_failed
}

destination=""
if [ -n "${PULUMI_GUARD_OUTPUT_FILE:-}" ]; then
prepare_destination "$PULUMI_GUARD_OUTPUT_FILE"
fi

# Prepare the destination and remove stale output before admission, so refused
# commands cannot leave a previous result behind. Admission itself is unchanged.
if ! admit_command "$@"; then
echo "::error title=Pulumi output withheld::Pulumi invocation refused."
exit 1
fi

command_label="${1:-}"
if ! [[ "$command_label" =~ ^[a-zA-Z][a-zA-Z0-9-]*$ ]]; then
command_label="command"
fi
status=0
pulumi "$@" > "$output" 2>&1 || status=$?
scan_files=("$output")
if [ -n "$destination" ]; then
stderr="$(mktemp)"
temporary_files+=("$stderr")
scan_files+=("$stderr")
pulumi "$@" > "$output" 2> "$stderr" || status=$?
else
pulumi "$@" > "$output" 2>&1 || status=$?
fi

matched=()
unchecked=0
for entry in "${CLASSES[@]}"; do
name="${entry%%|*}"
case "$(match_status "${entry#*|}")" in
0) matched+=("$name") ;;
1) ;;
*) unchecked=1 ;;
esac
found=0
for file in "${scan_files[@]}"; do
case "$(match_status "${entry#*|}" "$file")" in
0) found=1 ;;
1) ;;
*) unchecked=1 ;;
esac
done
if [ "$found" -eq 1 ]; then matched+=("$name"); fi
done

if [ "${#matched[@]}" -gt 0 ] || [ "$unchecked" -ne 0 ]; then
Expand All @@ -86,5 +161,12 @@ if [ "${#matched[@]}" -gt 0 ] || [ "$unchecked" -ne 0 ]; then
exit 1
fi

cat "$output"
if [ -n "$destination" ]; then
cat "$stderr" >&2
if [ "$status" -eq 0 ]; then
publish_destination
fi
else
cat "$output"
fi
exit "$status"
14 changes: 14 additions & 0 deletions .github/scripts/tests/fixtures/pulumi-guarded/pulumi
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
#!/usr/bin/env bash
# Fake pulumi for specs: prints PULUMI_FAKE_OUTPUT, records its arguments, exits PULUMI_FAKE_STATUS.
set -euo pipefail
printf '%s\n' "$*" > "${PULUMI_FAKE_ARGS:-/dev/null}"
case "${PULUMI_FAKE_MUTATION:-}" in
parent-symlink)
mv -f -T -- "$PULUMI_FAKE_PARENT" "$PULUMI_FAKE_PARENT.saved"
ln -s -- "$PULUMI_FAKE_TARGET" "$PULUMI_FAKE_PARENT" ;;
destination-symlink)
ln -s -- "$PULUMI_FAKE_TARGET" "$PULUMI_FAKE_DESTINATION" ;;
destination-directory)
mkdir -- "$PULUMI_FAKE_DESTINATION" ;;
esac
if [ -n "${PULUMI_FAKE_SIGNAL:-}" ]; then
kill -s "$PULUMI_FAKE_SIGNAL" "$PPID"
fi
cat "${PULUMI_FAKE_OUTPUT:-/dev/null}"
cat "${PULUMI_FAKE_STDERR:-/dev/null}" >&2
exit "${PULUMI_FAKE_STATUS:-0}"
Loading
Loading