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
2 changes: 1 addition & 1 deletion .github/actions/ai-agent-runner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ai-breaking-change-detector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
# SECURITY: pull_request_target β€” checkout base branch (default), NOT
# the PR head. The composite action fetches the diff via GitHub API,
# so checking out HEAD is unnecessary and would let a malicious PR
# modify .github/actions/ code that runs with elevated GITHUB_TOKEN.
fetch-depth: 0

- name: Run breaking change analysis
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ai-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
# SECURITY: pull_request_target β€” checkout base branch (default), NOT
# the PR head. The composite action fetches the diff via GitHub API,
# so checking out HEAD is unnecessary and would let a malicious PR
# modify .github/actions/ code that runs with elevated GITHUB_TOKEN.
fetch-depth: 0

- name: Run AI code review
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ai-contributor-guide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
(github.event.issue.author_association == 'NONE' ||
github.event.issue.author_association == 'FIRST_TIME_CONTRIBUTOR')
continue-on-error: true
# SECURITY: pull_request_target β€” this job does NOT checkout PR head code.
# It only checks out the base branch for the composite action, and context
# is fetched via GitHub API. Permissions are scoped to minimum needed.
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Expand Down Expand Up @@ -74,6 +77,9 @@ jobs:
(github.event.pull_request.author_association == 'NONE' ||
github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR')
continue-on-error: true
# SECURITY: pull_request_target β€” this job does NOT checkout PR head code.
# Permissions scoped to minimum: contents:read for base checkout, pr:write
# for posting the welcome comment.
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ai-docs-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
# SECURITY: pull_request_target β€” checkout base branch (default), NOT
# the PR head. The composite action fetches the diff via GitHub API,
# so checking out HEAD is unnecessary and would let a malicious PR
# modify .github/actions/ code that runs with elevated GITHUB_TOKEN.
fetch-depth: 0

- name: Check documentation freshness
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ai-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
# SECURITY: pull_request_target β€” checkout base branch (default), NOT
# the PR head. The composite action fetches the diff via GitHub API,
# so checking out HEAD is unnecessary and would let a malicious PR
# modify .github/actions/ code that runs with elevated GITHUB_TOKEN.
fetch-depth: 0

- name: Run AI security scan
Expand Down
27 changes: 18 additions & 9 deletions .github/workflows/ai-spec-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ jobs:
exit 0
fi

# Sanitize title for branch name and filename
SAFE_TITLE=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' \
# Sanitize title for branch name and filename β€” use printf to
# prevent interpretation of backslash escapes and special chars
# (CWE-77: ISSUE_TITLE is untrusted user input)
SAFE_TITLE=$(printf '%s' "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | head -c 50)
BRANCH="docs/spec-${ISSUE_NUMBER}-${SAFE_TITLE}"
SPEC_FILE="docs/specs/issue-${ISSUE_NUMBER}-${SAFE_TITLE}.md"
Expand All @@ -88,22 +90,29 @@ jobs:
printf '%s' "$SPEC_CONTENT" > "$SPEC_FILE"

git add "$SPEC_FILE"
git commit -m "docs: add engineering spec for #${ISSUE_NUMBER}

Auto-generated from issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}"
# Use printf for commit message to safely handle untrusted title
printf -v COMMIT_MSG 'docs: add engineering spec for #%s\n\nAuto-generated from issue #%s' \
"$ISSUE_NUMBER" "$ISSUE_NUMBER"
git commit -m "$COMMIT_MSG"

git push origin "$BRANCH"

gh pr create \
--title "πŸ“‹ Spec: ${ISSUE_TITLE}" \
--body "## Auto-Generated Engineering Spec
# Use --body-file to avoid shell interpretation of untrusted title
PR_BODY="## Auto-Generated Engineering Spec

This spec was auto-generated from issue #${ISSUE_NUMBER}.

**Please review and refine before approving.**

---
Closes #${ISSUE_NUMBER} (spec request)" \
Closes #${ISSUE_NUMBER} (spec request)"
printf '%s' "$PR_BODY" > "$RUNNER_TEMP/pr-body.md"

# Safely pass untrusted ISSUE_TITLE via printf to avoid injection
PR_TITLE=$(printf 'πŸ“‹ Spec: %s' "$ISSUE_TITLE")
gh pr create \
--title "$PR_TITLE" \
--body-file "$RUNNER_TEMP/pr-body.md" \
--base main \
--head "$BRANCH" \
--label "documentation,spec" \
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ai-test-generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
# SECURITY: pull_request_target β€” checkout base branch (default), NOT
# the PR head. The composite action fetches the diff via GitHub API,
# so checking out HEAD is unnecessary and would let a malicious PR
# modify .github/actions/ code that runs with elevated GITHUB_TOKEN.
fetch-depth: 0

- name: Identify changed source files
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@ jobs:
working-directory: packages/${{ matrix.package }}
run: |
pip install --no-cache-dir -e ".[dev]" 2>/dev/null || pip install --no-cache-dir -e ".[test]" 2>/dev/null || pip install --no-cache-dir -e .
pip install --no-cache-dir --require-hashes \
pytest==8.4.1 --hash=sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7 \
pytest-asyncio==1.1.0 --hash=sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf \
2>/dev/null || pip install --no-cache-dir pytest==8.4.1 pytest-asyncio==1.1.0 2>/dev/null || true
pip install --no-cache-dir pytest>=8.0 pytest-asyncio>=0.23 2>/dev/null || true
- name: Test ${{ matrix.package }}
working-directory: packages/${{ matrix.package }}
run: pytest tests/ -q --tb=short
Expand All @@ -63,9 +60,7 @@ jobs:
python-version: "3.11"
- name: Install safety
run: |
pip install --no-cache-dir --require-hashes \
safety==3.2.1 --hash=sha256:9f53646717ba052e1bf631bd54fb3da0fafa58e85d578b20a8b9affdcf81889e \
2>/dev/null || pip install --no-cache-dir safety==3.2.1
pip install --no-cache-dir safety==3.2.1
- name: Check dependencies
env:
GIT_TERMINAL_PROMPT: "0"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/copilot-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
copilot-review:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
# SECURITY: pull_request_target β€” no checkout, API-only. Permissions scoped
# to pull-requests:write (minimum needed to request a reviewer).
steps:
- name: Request Copilot Review
env:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ permissions:
jobs:
label:
runs-on: ubuntu-latest
# SECURITY: pull_request_target β€” uses actions/labeler which reads config from
# the base branch (default checkout). No PR head code is executed.
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pr-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ permissions:
jobs:
size-label:
runs-on: ubuntu-latest
# SECURITY: pull_request_target β€” uses pr-size-labeler which only reads PR
# metadata via API. No checkout of PR head code. Permissions minimal.
steps:
- uses: codelytv/pr-size-labeler@4ec67706cd878fbc1c8db0a5dcd28b6bb412e85a # v1.10.3
with:
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ jobs:

- name: Install build tools
run: |
# Require hash verification β€” no fallback to unverified install (CWE-295)
pip install --no-cache-dir --require-hashes \
build==1.2.1 --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4 \
2>/dev/null || pip install --no-cache-dir build==1.2.1
build==1.2.1 --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4

- name: Build ${{ matrix.package }}
working-directory: packages/${{ matrix.package }}
Expand Down Expand Up @@ -165,7 +165,12 @@ jobs:

- name: Install NuGet CLI
run: |
curl -o /usr/local/bin/nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
# Pin to specific version with SHA-256 verification (CWE-494)
NUGET_VERSION="v6.12.2"
NUGET_URL="https://dist.nuget.org/win-x86-commandline/${NUGET_VERSION}/nuget.exe"
NUGET_SHA256="64f467376f2ee364ba389461df4a29a8f8dd9aa38120d29046e70b9c82045d97"
curl -fsSL -o /usr/local/bin/nuget.exe "$NUGET_URL"
echo "${NUGET_SHA256} /usr/local/bin/nuget.exe" | sha256sum -c -
echo 'alias nuget="mono /usr/local/bin/nuget.exe"' >> ~/.bashrc

- name: Build .NET SDK
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ on:
schedule:
- cron: "15 7 * * 1"

permissions: read-all
# Minimum permissions required by OpenSSF Scorecard
permissions:
security-events: write
id-token: write
contents: read
actions: read

jobs:
analysis:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/welcome.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ permissions:
jobs:
welcome:
runs-on: ubuntu-latest
# SECURITY: pull_request_target β€” uses actions/first-interaction which only
# reads contributor history via API. No checkout of PR head code.
steps:
- uses: actions/first-interaction@a1db7729b356323c7988c20ed6f0d33fe31297be # v1.3.0
with:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Demo `--include-attacks` flag for adversarial scenario testing (prompt injection, tool alias bypass, SQL bypass).
- .NET `SagaStep.MaxAttempts` property replacing deprecated `MaxRetries`.
- `ContentHashInterceptor` for SHA-256 tool identity verification at intercept time.
- `ToolRegistry` content hashing β€” computes and verifies handler integrity at registration and execution.
- `PolicyEngine.freeze()` method with `MappingProxyType` immutability and mutation audit log.
- `QuorumConfig` for M-of-N approval requirements in `EscalationHandler`.
- Escalation fatigue detection β€” auto-DENY when agents exceed configurable rate threshold.
- `EscalationRequest.votes` field for per-approver vote tracking.

### Security
- Replaced XOR placeholder encryption with AES-256-GCM in DMZ module.
- Added Security Model & Limitations section to README.
- Added security advisories to SECURITY.md for CostGuard and thread safety fixes.
- Hardened against agent sandbox escape vectors (tool aliasing, runtime policy self-modification, approval fatigue).

## [2.2.0] - 2026-03-17

Expand Down
42 changes: 42 additions & 0 deletions examples/policies/adk-governance.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# ADK Governance Policy β€” Sample Configuration
#
# ⚠️ IMPORTANT: This is a SAMPLE policy for Google ADK agents.
# Review and customize before production use.

version: "1.0"
name: adk-governance
description: >
Sample governance policy for Google ADK agents. Configures tool
restrictions, rate limits, and delegation controls.

disclaimer: >
This is a sample configuration. Customize for your environment.

adk_governance:
# Tools that are always blocked
blocked_tools:
- execute_shell
- run_command
- delete_database
- drop_table

# Maximum tool calls per agent per session
max_tool_calls: 100

# Tools requiring human approval before execution
require_approval_for:
- send_email
- publish_document
- deploy_service
- transfer_funds

# Delegation controls
delegation:
max_depth: 3
require_scope_narrowing: true

# Audit settings
audit:
log_all_tool_calls: true
log_delegations: true
include_tool_args: false # Set true only in dev (may contain PII)
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public static AgentIdentity Create(string name)
/// <exception cref="InvalidOperationException">
/// Thrown when this identity does not have a private key (verification-only).
/// </exception>
/// <remarks>
/// ⚠️ <b>SECURITY WARNING (CWE-327):</b> This method uses HMAC-SHA256 as a compatibility
/// fallback. HMAC-SHA256 is a symmetric scheme β€” both signing and verification require the
/// private key, which is unsuitable for cross-agent trust scenarios. Prefer Ed25519 (available
/// natively in .NET 9+) for production deployments. This fallback exists only for backward
/// compatibility with .NET 8.0 environments and should be considered deprecated.
/// </remarks>
[Obsolete("HMAC-SHA256 signing is a compatibility fallback. Migrate to Ed25519 on .NET 9+ for proper asymmetric signing.")]
public byte[] Sign(byte[] data)
{
ArgumentNullException.ThrowIfNull(data);
Expand All @@ -105,6 +113,10 @@ public byte[] Sign(byte[] data)
"Cannot sign data: this identity does not have a private key.");
}

System.Diagnostics.Trace.TraceWarning(
"[AgentIdentity] Using HMAC-SHA256 fallback for signing. " +
"This is deprecated β€” migrate to Ed25519 on .NET 9+ for proper asymmetric cryptography.");

using var hmac = new HMACSHA256(PrivateKey);
return hmac.ComputeHash(data);
}
Expand All @@ -114,6 +126,8 @@ public byte[] Sign(byte[] data)
/// </summary>
/// <param name="message">The message to sign.</param>
/// <returns>A 32-byte HMAC-SHA256 signature.</returns>
/// <inheritdoc cref="Sign(byte[])" path="/remarks"/>
[Obsolete("HMAC-SHA256 signing is a compatibility fallback. Migrate to Ed25519 on .NET 9+ for proper asymmetric signing.")]
public byte[] Sign(string message)
{
ArgumentNullException.ThrowIfNull(message);
Expand All @@ -131,6 +145,11 @@ public byte[] Sign(string message)
/// verification requires the signing key. For public-key verification,
/// migrate to Ed25519 on .NET 9+.
/// </exception>
/// <remarks>
/// ⚠️ <b>SECURITY WARNING (CWE-327):</b> HMAC-SHA256 verification requires the private key,
/// making it unsuitable for public-key-only verification. Migrate to Ed25519 on .NET 9+.
/// </remarks>
[Obsolete("HMAC-SHA256 verification is a compatibility fallback. Migrate to Ed25519 on .NET 9+ for public-key verification.")]
public bool Verify(byte[] data, byte[] signature)
{
ArgumentNullException.ThrowIfNull(data);
Expand All @@ -143,7 +162,9 @@ public bool Verify(byte[] data, byte[] signature)
"For cross-agent verification with only a public key, migrate to Ed25519 (.NET 9+).");
}

#pragma warning disable CS0618 // Intentional use of deprecated Sign() for HMAC fallback path
var expected = Sign(data);
#pragma warning restore CS0618
return CryptographicOperations.FixedTimeEquals(expected, signature);
}

Expand All @@ -163,6 +184,12 @@ public bool Verify(byte[] data, byte[] signature)
/// Thrown when <paramref name="privateKey"/> is <c>null</c> because HMAC-SHA256
/// cannot verify without the signing key.
/// </exception>
/// <remarks>
/// ⚠️ <b>SECURITY WARNING (CWE-327):</b> This static overload uses HMAC-SHA256, which
/// requires the private key for verification β€” defeating the purpose of public-key
/// cryptography. Migrate to Ed25519 on .NET 9+ where only the public key is needed.
/// </remarks>
[Obsolete("HMAC-SHA256 verification is a compatibility fallback. Migrate to Ed25519 on .NET 9+ for public-key verification.")]
public static bool VerifySignature(byte[] publicKey, byte[] data, byte[] signature, byte[]? privateKey = null)
{
ArgumentNullException.ThrowIfNull(publicKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,18 @@ public sealed class FileTrustStore : IDisposable
public FileTrustStore(string filePath, double defaultScore = 500.0, double decayRate = 10.0, Action<Exception, string>? loadErrorHandler = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
_filePath = filePath;

// CWE-22: Validate path to prevent directory traversal attacks.
// Resolve the full path and reject any path containing ".." segments.
var resolvedPath = Path.GetFullPath(filePath);
if (filePath.Contains("..", StringComparison.Ordinal))
{
throw new ArgumentException(
$"Path traversal detected: trust store path must not contain '..' segments. Resolved: {resolvedPath}",
nameof(filePath));
}

_filePath = resolvedPath;
_defaultScore = Math.Clamp(defaultScore, 0, 1000);
_decayRate = Math.Max(0, decayRate);
_loadErrorHandler = loadErrorHandler;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-marketplace/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ classifiers = [
dependencies = [
"pydantic>=2.0",
"pyyaml>=6.0",
"cryptography>=41.0",
"cryptography>=44.0.0,<47.0",
]

[project.optional-dependencies]
Expand Down
Loading
Loading