name: 'AgentShield Security Scanner'
description: 'Scan supported AI agent extension frameworks for security vulnerabilities'
author: 'AgentShield Contributors'
branding:
icon: 'shield'
color: 'blue'
inputs:
path:
description: 'Path to scan (relative to repo root)'
required: false
default: '.'
fail-on:
description: 'Minimum severity to fail the check (info, low, medium, high, critical)'
required: false
default: 'high'
format:
description: 'Output format (console, json, sarif)'
required: false
default: 'sarif'
config:
description: 'Path to .agentshield.toml config file'
required: false
default: ''
baseline:
description: 'Path to an AgentShield baseline file for known findings'
required: false
default: ''
upload-sarif:
description: 'Upload SARIF results to GitHub Code Scanning'
required: false
default: 'true'
ignore-tests:
description: 'Skip test files (test/, tests/, __tests__/, *.test.ts, *.spec.ts, etc.)'
required: false
default: 'false'
strict:
description: 'Fail when no supported adapter is found; false makes only that discovery error non-blocking'
required: false
default: 'true'
version:
description: 'AgentShield version to use (default: latest). Accepts a version with or without a "v" prefix, e.g. v0.8.0 or 0.8.0.'
required: false
default: 'latest'
binary-path:
description: 'Path to an existing AgentShield binary; skips release download when set'
required: false
default: ''
outputs:
finding-count:
description: 'Number of findings detected'
value: ${{ steps.scan.outputs.finding-count }}
sarif-file:
description: 'Path to the SARIF output file (if format=sarif)'
value: ${{ steps.scan.outputs.sarif-file }}
exit-code:
description: 'Exit code from the scan (0=pass, 1=fail, 2=error)'
value: ${{ steps.scan.outputs.exit-code }}
runs:
using: 'composite'
steps:
- name: Determine version
if: inputs.binary-path == ''
id: version
shell: bash
run: |
if [ "${{ inputs.version }}" = "latest" ]; then
VERSION=$(
curl -fsSL https://api.github.com/repos/aiconnai/agentshield/releases/latest 2>/dev/null \
| sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' \
| head -n 1 || true
)
if [ -z "$VERSION" ]; then
echo "::error::Could not determine latest version. Specify version explicitly."
exit 1
fi
else
VERSION="${{ inputs.version }}"
# Normalize: release archives are named with a 'v' prefix (e.g. v0.8.0).
# Prepend it if the user omitted it.
case "$VERSION" in
v*) ;;
*) VERSION="v$VERSION" ;;
esac
if [ "$VERSION" = "v" ] || [ -z "$VERSION" ]; then
echo "::error::Version input is empty or invalid after normalization."
exit 1
fi
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Determine platform
if: inputs.binary-path == ''
id: platform
shell: bash
run: |
case "${{ runner.os }}-${{ runner.arch }}" in
Linux-X64) TARGET="x86_64-unknown-linux-gnu" ;;
Linux-ARM64) TARGET="aarch64-unknown-linux-gnu" ;;
macOS-X64) TARGET="x86_64-apple-darwin" ;;
macOS-ARM64) TARGET="aarch64-apple-darwin" ;;
Windows-X64) TARGET="x86_64-pc-windows-msvc" ;;
*) echo "::error::Unsupported platform: ${{ runner.os }}-${{ runner.arch }}"; exit 1 ;;
esac
echo "target=$TARGET" >> $GITHUB_OUTPUT
- name: Download AgentShield
if: inputs.binary-path == ''
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
TARGET="${{ steps.platform.outputs.target }}"
BASE_URL="https://github.com/aiconnai/agentshield/releases/download/${VERSION}"
if [[ "$TARGET" == *"windows"* ]]; then
ARCHIVE="agentshield-${VERSION}-${TARGET}.zip"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o agentshield.zip
unzip -o agentshield.zip -d ${{ runner.temp }}/agentshield
else
ARCHIVE="agentshield-${VERSION}-${TARGET}.tar.gz"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o agentshield.tar.gz
mkdir -p ${{ runner.temp }}/agentshield
tar xzf agentshield.tar.gz -C ${{ runner.temp }}/agentshield
fi
chmod +x ${{ runner.temp }}/agentshield/agentshield* 2>/dev/null || true
echo "${{ runner.temp }}/agentshield" >> $GITHUB_PATH
- name: Use provided AgentShield
if: inputs.binary-path != ''
shell: bash
env:
AGENTSHIELD_BINARY_PATH: ${{ inputs.binary-path }}
run: |
if [ ! -x "$AGENTSHIELD_BINARY_PATH" ]; then
echo "::error::Provided AgentShield binary is not executable: $AGENTSHIELD_BINARY_PATH"
exit 2
fi
AGENTSHIELD_DEST="${{ runner.temp }}/agentshield/agentshield"
mkdir -p "$(dirname "$AGENTSHIELD_DEST")"
if [ "$AGENTSHIELD_BINARY_PATH" != "$AGENTSHIELD_DEST" ]; then
cp "$AGENTSHIELD_BINARY_PATH" "$AGENTSHIELD_DEST"
fi
chmod +x "$AGENTSHIELD_DEST"
echo "$(dirname "$AGENTSHIELD_DEST")" >> $GITHUB_PATH
- name: Run scan
id: scan
shell: bash
run: |
SCAN_LOG="${{ runner.temp }}/agentshield-scan.log"
ARGS="scan ${{ inputs.path }}"
ARGS="$ARGS --fail-on ${{ inputs.fail-on }}"
SARIF_FILE=""
if [ "${{ inputs.format }}" = "sarif" ]; then
SARIF_FILE="${{ runner.temp }}/agentshield-results.sarif"
ARGS="$ARGS --format sarif --output $SARIF_FILE"
else
ARGS="$ARGS --format ${{ inputs.format }}"
fi
if [ -n "${{ inputs.config }}" ]; then
ARGS="$ARGS --config ${{ inputs.config }}"
fi
if [ -n "${{ inputs.baseline }}" ]; then
ARGS="$ARGS --baseline ${{ inputs.baseline }}"
fi
if [ "${{ inputs.ignore-tests }}" = "true" ]; then
ARGS="$ARGS --ignore-tests"
fi
set +e
agentshield $ARGS 2>&1 | tee "$SCAN_LOG"
EXIT_CODE=${PIPESTATUS[0]}
set -e
if grep -Fq "No suitable adapter found for directory:" "$SCAN_LOG"; then
echo "no_adapter=true" >> $GITHUB_OUTPUT
else
echo "no_adapter=false" >> $GITHUB_OUTPUT
fi
echo "exit-code=$EXIT_CODE" >> $GITHUB_OUTPUT
echo "sarif-file=$SARIF_FILE" >> $GITHUB_OUTPUT
# Count findings from SARIF if available
if [ -n "$SARIF_FILE" ] && [ -f "$SARIF_FILE" ]; then
COUNT=$(python3 -c "import json; d=json.load(open('$SARIF_FILE')); print(len(d.get('runs',[{}])[0].get('results',[])))" 2>/dev/null || echo "0")
echo "finding-count=$COUNT" >> $GITHUB_OUTPUT
if [ "$EXIT_CODE" = "1" ]; then
echo "::group::AgentShield findings"
python3 - "$SARIF_FILE" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
sarif = json.load(handle)
for run in sarif.get("runs", []):
for result in run.get("results", []):
location = (
result.get("locations", [{}])[0]
.get("physicalLocation", {})
)
artifact = location.get("artifactLocation", {})
region = location.get("region", {})
fields = {
"rule": result.get("ruleId", "unknown"),
"level": result.get("level", "unknown"),
"file": artifact.get("uri", "unknown"),
"line": region.get("startLine", "unknown"),
"message": result.get("message", {}).get("text", ""),
}
rendered = " ".join(
f"{key}={json.dumps(value, ensure_ascii=True)}"
for key, value in fields.items()
)
print(f"AgentShield finding: {rendered}")
PY
echo "::endgroup::"
fi
else
echo "finding-count=0" >> $GITHUB_OUTPUT
fi
# Don't fail here — let the upload step run first
echo "AGENTSHIELD_EXIT=$EXIT_CODE" >> $GITHUB_ENV
- name: Upload SARIF to GitHub Code Scanning
if: inputs.upload-sarif == 'true' && inputs.format == 'sarif' && steps.scan.outputs.sarif-file != ''
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.scan.outputs.sarif-file }}
category: agentshield
continue-on-error: true
- name: Check result
shell: bash
run: |
if [ "$AGENTSHIELD_EXIT" = "1" ]; then
echo "::error::AgentShield found findings above the '${{ inputs.fail-on }}' threshold"
exit 1
elif [ "$AGENTSHIELD_EXIT" = "2" ]; then
if [ "${{ steps.scan.outputs.no_adapter }}" = "true" ] && [ "${{ inputs.strict }}" = "false" ]; then
echo "::warning::AgentShield found no supported adapter for the scanned path; non-blocking due strict=false"
exit 0
elif [ "${{ steps.scan.outputs.no_adapter }}" = "true" ]; then
echo "::error::AgentShield encountered a scan error: No suitable adapter found"
exit 2
else
echo "::error::AgentShield encountered a scan error"
exit 2
fi
fi