Repository navigation
Expand file tree
/
Copy pathaction.yml
More file actions
304 lines (263 loc) · 12.9 KB
/
Copy pathaction.yml
File metadata and controls
304 lines (263 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
name: 'Setup AWF'
description: 'Install the Agentic Workflow Firewall (awf) CLI tool'
author: 'GitHub'
branding:
icon: 'shield'
color: 'blue'
inputs:
version:
description: 'Version to install (e.g., v1.0.0). Defaults to latest release.'
required: false
default: 'latest'
pull-images:
description: 'Pull Docker images for the installed version. Set to "true" to pre-pull squid and agent images.'
required: false
default: 'false'
outputs:
version:
description: 'The version of awf that was installed'
value: ${{ steps.install.outputs.version }}
image-tag:
description: 'The image tag metadata for awf runtime images (base tag plus optional per-image digests)'
value: ${{ steps.install.outputs.image_tag }}
runs:
using: 'composite'
steps:
- name: Validate runner OS and architecture
shell: bash
run: |
if [ "$RUNNER_OS" != "Linux" ]; then
echo "::error::This action only supports Linux runners. Current OS: $RUNNER_OS"
exit 1
fi
# Validate architecture (only x64 is supported)
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "amd64" ]; then
echo "::error::This action only supports x64 architecture. Current architecture: $ARCH"
exit 1
fi
- name: Install awf
id: install
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
REPO="github/gh-aw-firewall"
BINARY_NAME="awf-linux-x64"
INSTALL_DIR="${RUNNER_TEMP}/awf-bin"
# gh itself and this action must already come from a trusted channel.
if ! command -v gh &> /dev/null || \
! gh attestation verify --help 2>/dev/null | grep -q -- '--signer-workflow'; then
echo "::error::A trusted gh CLI with attestation verify support is required. Install it through an independently authenticated channel."
exit 1
fi
verify_provenance() {
if ! gh attestation verify "$1" \
--hostname github.com \
--repo "$REPO" \
--signer-workflow "${REPO}/.github/workflows/release.yml" \
--cert-oidc-issuer https://token.actions.githubusercontent.com \
--deny-self-hosted-runners; then
echo "::error::Release provenance verification failed for $(basename "$1"). Legacy unattested releases are not supported; use an attested release and a trusted, authenticated gh CLI."
exit 1
fi
}
# Build auth headers for GitHub API (Bearer is the recommended format for GITHUB_TOKEN)
AUTH_HEADER=()
if [ -n "${GITHUB_TOKEN:-}" ]; then
AUTH_HEADER=(-H "Authorization: Bearer ${GITHUB_TOKEN}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28")
fi
# Create install directory
mkdir -p "$INSTALL_DIR"
# Determine version
if [ "$INPUT_VERSION" = "latest" ] || [ -z "$INPUT_VERSION" ]; then
echo "Fetching latest release version..."
VERSION=""
# Try gh CLI first (pre-installed on GitHub Actions runners, handles auth natively)
if command -v gh &> /dev/null && [ -n "${GITHUB_TOKEN:-}" ]; then
VERSION=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name' 2>/dev/null || true)
fi
# Fall back to curl with GitHub API
if [ -z "${VERSION:-}" ] || [ "$VERSION" = "null" ]; then
if command -v jq &> /dev/null; then
if ! VERSION=$(curl -fsSL "${AUTH_HEADER[@]}" "https://api.github.com/repos/${REPO}/releases/latest" | jq -r '.tag_name'); then
VERSION=""
fi
else
if ! VERSION=$(curl -fsSL "${AUTH_HEADER[@]}" "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'); then
VERSION=""
fi
fi
fi
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "::error::Failed to fetch latest version from GitHub API"
exit 1
fi
echo "Latest version: $VERSION"
else
VERSION="$INPUT_VERSION"
fi
# Validate even API-supplied versions before using them in URLs or outputs.
if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "::error::Invalid version format: $VERSION. Expected format: v1.0.0 or v1.0.0-beta.1"
exit 1
fi
# Download URLs
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
BINARY_URL="${BASE_URL}/${BINARY_NAME}"
CHECKSUMS_URL="${BASE_URL}/checksums.txt"
CONTAINERS_URL="${BASE_URL}/containers.txt"
# Extract the version tag, then attach authenticated digest pins.
IMAGE_TAG="${VERSION#v}"
IMAGE_TAG_WITH_DIGESTS="$IMAGE_TAG"
# Download binary
echo "Downloading awf ${VERSION}..."
if ! curl -fsSL "$BINARY_URL" -o "$INSTALL_DIR/$BINARY_NAME.download"; then
echo "::error::Failed to download binary from $BINARY_URL"
exit 1
fi
# Download checksums
echo "Downloading checksums..."
if ! curl -fsSL "$CHECKSUMS_URL" -o "$INSTALL_DIR/checksums.txt"; then
echo "::error::Failed to download checksums from $CHECKSUMS_URL"
exit 1
fi
# Bind the checksums to the requested tag before consuming any metadata.
if ! curl -fsSL "${BASE_URL}/release-identity.txt" -o "$INSTALL_DIR/release-identity.txt"; then
echo "::error::Missing release identity for ${VERSION}; legacy unattested releases are not supported"
exit 1
fi
verify_provenance "$INSTALL_DIR/release-identity.txt"
EXPECTED_IDENTITY="$(sha256sum "$INSTALL_DIR/checksums.txt" | awk '{print $1}') refs/tags/${VERSION}"
if [ "$(cat "$INSTALL_DIR/release-identity.txt")" != "$EXPECTED_IDENTITY" ]; then
echo "::error::Release identity does not bind checksums.txt to refs/tags/${VERSION}"
exit 1
fi
verify_provenance "$INSTALL_DIR/checksums.txt"
verify_provenance "$INSTALL_DIR/$BINARY_NAME.download"
# The digest manifest is authenticated, checksum-bound, and required.
echo "Downloading containers manifest..."
if curl -fsSL "$CONTAINERS_URL" -o "$INSTALL_DIR/containers.txt"; then
verify_provenance "$INSTALL_DIR/containers.txt"
MANIFEST_SUM="$(sha256sum "$INSTALL_DIR/containers.txt" | awk '{print $1}')"
EXPECTED_MANIFEST_SUM="$(awk '$2 == "containers.txt" {print $1; exit}' "$INSTALL_DIR/checksums.txt")"
if [ "$MANIFEST_SUM" != "$EXPECTED_MANIFEST_SUM" ]; then
echo "::error::containers.txt does not match the authenticated release checksums"
exit 1
fi
extract_digest() {
local image_name="$1"
grep -E "^ghcr\\.io/${REPO}/${image_name}@sha256:[a-fA-F0-9]{64}$" "$INSTALL_DIR/containers.txt" \
| sed -E 's#.*@(sha256:[a-fA-F0-9]{64})#\1#' \
| tr '[:upper:]' '[:lower:]'
}
DIGEST_ENTRIES=()
SQUID_DIGEST="$(extract_digest squid || true)"
AGENT_DIGEST="$(extract_digest agent || true)"
AGENT_ACT_DIGEST="$(extract_digest agent-act || true)"
API_PROXY_DIGEST="$(extract_digest api-proxy || true)"
ROUTER_DIGEST="$(extract_digest router || true)"
CLI_PROXY_DIGEST="$(extract_digest cli-proxy || true)"
ENCLAVE_SCRIPT_DIGEST="$(extract_digest enclave-script || true)"
ENCLAVE_AGENT_DIGEST="$(extract_digest enclave-agent || true)"
ENCLAVE_MCP_SERVER_DIGEST="$(extract_digest enclave-mcp-server || true)"
for DIGEST in "$SQUID_DIGEST" "$AGENT_DIGEST" "$AGENT_ACT_DIGEST" \
"$API_PROXY_DIGEST" "$ROUTER_DIGEST" "$CLI_PROXY_DIGEST" \
"$ENCLAVE_SCRIPT_DIGEST" "$ENCLAVE_AGENT_DIGEST" "$ENCLAVE_MCP_SERVER_DIGEST"; do
if [[ ! "$DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "::error::containers.txt is missing a required image digest or contains duplicate entries"
exit 1
fi
done
[ -n "${SQUID_DIGEST:-}" ] && DIGEST_ENTRIES+=("squid=${SQUID_DIGEST}")
[ -n "${AGENT_DIGEST:-}" ] && DIGEST_ENTRIES+=("agent=${AGENT_DIGEST}")
[ -n "${AGENT_ACT_DIGEST:-}" ] && DIGEST_ENTRIES+=("agent-act=${AGENT_ACT_DIGEST}")
[ -n "${API_PROXY_DIGEST:-}" ] && DIGEST_ENTRIES+=("api-proxy=${API_PROXY_DIGEST}")
[ -n "${ROUTER_DIGEST:-}" ] && DIGEST_ENTRIES+=("router=${ROUTER_DIGEST}")
[ -n "${CLI_PROXY_DIGEST:-}" ] && DIGEST_ENTRIES+=("cli-proxy=${CLI_PROXY_DIGEST}")
[ -n "${ENCLAVE_SCRIPT_DIGEST:-}" ] && DIGEST_ENTRIES+=("enclave-script=${ENCLAVE_SCRIPT_DIGEST}")
[ -n "${ENCLAVE_AGENT_DIGEST:-}" ] && DIGEST_ENTRIES+=("enclave-agent=${ENCLAVE_AGENT_DIGEST}")
[ -n "${ENCLAVE_MCP_SERVER_DIGEST:-}" ] && DIGEST_ENTRIES+=("enclave-mcp-server=${ENCLAVE_MCP_SERVER_DIGEST}")
if [ "${#DIGEST_ENTRIES[@]}" -gt 0 ]; then
DIGEST_CSV="$(IFS=,; echo "${DIGEST_ENTRIES[*]}")"
IMAGE_TAG_WITH_DIGESTS="${IMAGE_TAG},${DIGEST_CSV}"
echo "Discovered digest pins for ${#DIGEST_ENTRIES[@]} image(s)"
fi
else
echo "::error::Missing containers.txt for ${VERSION}; tag-only fallback is not permitted"
exit 1
fi
# Verify checksum
echo "Verifying SHA256 checksum..."
# Validate checksums.txt format (should have "checksum filename" format)
if ! grep -qE '^[a-fA-F0-9]{64} ' "$INSTALL_DIR/checksums.txt"; then
echo "::error::checksums.txt has unexpected format"
exit 1
fi
EXPECTED_SUM=$(awk -v fname="$BINARY_NAME" '$2 == fname {print $1; exit}' "$INSTALL_DIR/checksums.txt")
if [ -z "$EXPECTED_SUM" ]; then
echo "::error::Could not find checksum for $BINARY_NAME in checksums.txt"
exit 1
fi
# Validate checksum format (64 hex characters)
if ! echo "$EXPECTED_SUM" | grep -qE '^[a-fA-F0-9]{64}$'; then
echo "::error::Invalid checksum format: $EXPECTED_SUM"
exit 1
fi
# Normalize to lowercase for comparison
EXPECTED_SUM=$(echo "$EXPECTED_SUM" | tr '[:upper:]' '[:lower:]')
ACTUAL_SUM=$(sha256sum "$INSTALL_DIR/$BINARY_NAME.download" | awk '{print $1}' | tr '[:upper:]' '[:lower:]')
if [ "$EXPECTED_SUM" != "$ACTUAL_SUM" ]; then
echo "::error::Checksum verification failed!"
echo "Expected: $EXPECTED_SUM"
echo "Got: $ACTUAL_SUM"
exit 1
fi
echo "Checksum verification passed ✓"
# Verify it's a valid ELF executable
if ! file "$INSTALL_DIR/$BINARY_NAME.download" | grep -q "ELF.*executable"; then
echo "::error::Downloaded file is not a valid Linux executable"
exit 1
fi
# Make executable
chmod +x "$INSTALL_DIR/$BINARY_NAME.download"
mv "$INSTALL_DIR/$BINARY_NAME.download" "$INSTALL_DIR/awf"
# Clean up downloaded metadata files
rm -f "$INSTALL_DIR/checksums.txt" "$INSTALL_DIR/containers.txt" "$INSTALL_DIR/release-identity.txt"
# Add to PATH
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "image_tag=$IMAGE_TAG_WITH_DIGESTS" >> "$GITHUB_OUTPUT"
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
echo "Successfully installed awf ${VERSION} to $INSTALL_DIR"
echo "awf is now available in PATH for subsequent steps"
- name: Pull Docker images
if: ${{ inputs.pull-images == 'true' }}
shell: bash
env:
IMAGE_TAG: ${{ steps.install.outputs.image_tag }}
run: |
set -euo pipefail
REGISTRY="ghcr.io/github/gh-aw-firewall"
extract_digest_from_tag() {
local key="$1"
echo "$IMAGE_TAG" | tr ',' '\n' | grep -E "^${key}=sha256:[a-fA-F0-9]{64}$" | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]' | head -n 1
}
SQUID_DIGEST="$(extract_digest_from_tag squid || true)"
AGENT_DIGEST="$(extract_digest_from_tag agent || true)"
if [[ ! "$SQUID_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]] || \
[[ ! "$AGENT_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "::error::Authenticated squid and agent digest pins are required"
exit 1
fi
SQUID_REF="${REGISTRY}/squid@${SQUID_DIGEST}"
AGENT_REF="${REGISTRY}/agent@${AGENT_DIGEST}"
echo "Pulling awf Docker images with tag: ${IMAGE_TAG}"
# Pull squid image
echo "Pulling ${SQUID_REF}..."
docker pull "${SQUID_REF}"
# Pull agent image
echo "Pulling ${AGENT_REF}..."
docker pull "${AGENT_REF}"
echo "Docker images pulled successfully ✓"