mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
Merge remote-tracking branch 'origin/main' into claude/issue-220-20250804-1147
Resolved import-block conflict in orchestrator/scripts/challenge.py by taking the union of imports (all used by the interactive submit-project script and main). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@
|
||||
**/.pdm-build/
|
||||
**/.pdm-python
|
||||
**/pdm.lock
|
||||
fuzzer/dockerfiles/runner_image.Dockerfile
|
||||
fuzzer/Dockerfile
|
||||
orchestrator/Dockerfile
|
||||
patcher/Dockerfile
|
||||
seed-gen/Dockerfile
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# actionlint configuration
|
||||
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
|
||||
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- gha-ubuntu-8
|
||||
+1
-10
@@ -1,5 +1,6 @@
|
||||
export BUTTERCUP_K8S_VALUES_TEMPLATE=./k8s/values-upstream-minikube.template
|
||||
export DEPLOY_CLUSTER=true
|
||||
export DEPLOY_SIGNOZ=false
|
||||
export CLUSTER_TYPE=minikube
|
||||
export TAILSCALE_ENABLED=false
|
||||
export COMPETITION_API_ENABLED=false
|
||||
@@ -13,13 +14,3 @@ export OTEL_PROTOCOL="http"
|
||||
export LANGFUSE_ENABLED=true
|
||||
export DOCKER_USERNAME=""
|
||||
export DOCKER_PAT=""
|
||||
|
||||
# Secrets/Env replaced
|
||||
export OTEL_ENDPOINT=replace-me
|
||||
export BUTTERCUP_NAMESPACE=replace-me
|
||||
export GHCR_AUTH=replace-me
|
||||
export OPENAI_API_KEY=replace-me
|
||||
export ANTHROPIC_API_KEY=replace-me
|
||||
export LANGFUSE_HOST=replace-me
|
||||
export LANGFUSE_PUBLIC_KEY=replace-me
|
||||
export LANGFUSE_SECRET_KEY=replace-me
|
||||
|
||||
@@ -8,7 +8,10 @@ updates:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
# Supply chain security: delay updates to allow community vetting
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
@@ -17,4 +20,7 @@ updates:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: daily
|
||||
interval: weekly
|
||||
# Supply chain security: delay updates to allow community vetting
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- name: Run Claude Code
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: "url.https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/.insteadof"
|
||||
GIT_CONFIG_VALUE_0: "https://github.com/"
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
||||
# model: "claude-opus-4-20250514"
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
# Optional: Trigger when specific user is assigned to an issue
|
||||
# assignee_trigger: "claude-bot"
|
||||
|
||||
# Optional: Allow Claude to run specific commands
|
||||
allowed_tools: "Bash"
|
||||
|
||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||
# custom_instructions: |
|
||||
# Follow our coding standards
|
||||
# Ensure all new code has tests
|
||||
# Use TypeScript for new files
|
||||
|
||||
# Optional: Custom environment variables for Claude
|
||||
# claude_env: |
|
||||
# NODE_ENV: test
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
name: Component Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# Always run full test suite on main branch
|
||||
pull_request:
|
||||
|
||||
schedule:
|
||||
# Run integration tests daily at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
components:
|
||||
description: 'Components to test (comma-separated: common,patcher,program-model,seed-gen)'
|
||||
required: false
|
||||
default: 'common,patcher,program-model,seed-gen'
|
||||
type: string
|
||||
run_integration:
|
||||
description: 'Run integration tests'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Integration tests - run on schedule, manual trigger, or labeled PRs
|
||||
test-integration:
|
||||
permissions:
|
||||
contents: read
|
||||
# Run if:
|
||||
# - Scheduled run
|
||||
# - Manual dispatch with run_integration=true
|
||||
# - Push to main branch
|
||||
# - PR with 'integration-tests' label
|
||||
if: |
|
||||
github.event_name == 'schedule' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_integration == 'true') ||
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'integration-tests'))
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- component: common
|
||||
coverage_module: buttercup.common
|
||||
python: "3.12"
|
||||
- component: patcher
|
||||
coverage_module: buttercup.patcher
|
||||
python: "3.12"
|
||||
- component: program-model
|
||||
coverage_module: buttercup.program_model
|
||||
python: "3.12"
|
||||
- component: seed-gen
|
||||
coverage_module: buttercup.seed_gen
|
||||
python: "3.12"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis@sha256:e647cfe134bf5e8e74e620f66346f93418acfc240b71dd85640325cb7cd01402 # 7.4
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Check if component should be tested
|
||||
id: should_test
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
INPUT_COMPONENTS: ${{ github.event.inputs.components }}
|
||||
MATRIX_COMPONENT: ${{ matrix.component }}
|
||||
run: |
|
||||
if [[ -z "$INPUT_COMPONENTS" ]] || [[ "$INPUT_COMPONENTS" == *"$MATRIX_COMPONENT"* ]]; then
|
||||
echo "test=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "test=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install uv
|
||||
if: steps.should_test.outputs.test != 'false'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Setup uv cache
|
||||
if: steps.should_test.outputs.test != 'false'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
~/.local/share/uv
|
||||
key: ${{ runner.os }}-uv-integration-${{ matrix.component }}-${{ hashFiles(format('{0}/uv.lock', matrix.component)) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-integration-${{ matrix.component }}-
|
||||
${{ runner.os }}-uv-
|
||||
|
||||
- name: Download Wasm runtime
|
||||
if: matrix.component == 'seed-gen' && steps.should_test.outputs.test != 'false'
|
||||
run: wget https://github.com/vmware-labs/webassembly-language-runtimes/releases/download/python%2F3.12.0%2B20231211-040d5a6/python-3.12.0.wasm
|
||||
working-directory: seed-gen
|
||||
|
||||
- name: Install integration test dependencies
|
||||
if: steps.should_test.outputs.test != 'false'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y codequery ripgrep
|
||||
make install-cscope
|
||||
|
||||
- name: Prepare environment
|
||||
if: steps.should_test.outputs.test != 'false'
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update
|
||||
sudo mkdir -p /crs_scratch
|
||||
sudo chmod -R 777 /crs_scratch
|
||||
|
||||
- name: Setup ${{ matrix.component }} component
|
||||
if: steps.should_test.outputs.test != 'false'
|
||||
run: |
|
||||
uv sync --all-extras --frozen
|
||||
uv pip install 'pytest-html>=4.1.1' 'pytest-cov>=6.0.0'
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Run program-model libpng integration test
|
||||
if: matrix.component == 'program-model' && steps.should_test.outputs.test != 'false'
|
||||
run: |
|
||||
uv run --frozen pytest -svv --runintegration tests/c/test_libpng.py \
|
||||
--junit-xml=integration-test-results.xml \
|
||||
--html=integration-test-report.html \
|
||||
--self-contained-html \
|
||||
--cov=${{ matrix.coverage_module }} \
|
||||
--cov-report=xml:integration-coverage.xml \
|
||||
--cov-report=html:integration-htmlcov \
|
||||
--cov-report=term
|
||||
env:
|
||||
PYTHON_WASM_BUILD_PATH: "python-3.12.0.wasm"
|
||||
working-directory: ${{ matrix.component }}
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Run integration tests on ${{ matrix.component }}
|
||||
if: matrix.component != 'program-model' && steps.should_test.outputs.test != 'false'
|
||||
run: |
|
||||
uv run --frozen pytest -svv --runintegration \
|
||||
--junit-xml=integration-test-results.xml \
|
||||
--html=integration-test-report.html \
|
||||
--self-contained-html \
|
||||
--cov=${{ matrix.coverage_module }} \
|
||||
--cov-report=xml:integration-coverage.xml \
|
||||
--cov-report=html:integration-htmlcov \
|
||||
--cov-report=term
|
||||
env:
|
||||
PYTHON_WASM_BUILD_PATH: "python-3.12.0.wasm"
|
||||
working-directory: ${{ matrix.component }}
|
||||
timeout-minutes: 30
|
||||
|
||||
- name: Generate integration test summary
|
||||
if: always() && steps.should_test.outputs.test != 'false'
|
||||
env:
|
||||
COMPONENT: ${{ matrix.component }}
|
||||
run: |
|
||||
echo "### Integration Test Results: ${COMPONENT}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -f "${COMPONENT}/integration-test-results.xml" ]; then
|
||||
python -c "
|
||||
import xml.etree.ElementTree as ET
|
||||
import os
|
||||
tree = ET.parse(os.environ['COMPONENT'] + '/integration-test-results.xml')
|
||||
root = tree.getroot()
|
||||
tests = root.get('tests', '0')
|
||||
failures = root.get('failures', '0')
|
||||
errors = root.get('errors', '0')
|
||||
skipped = root.get('skipped', '0')
|
||||
time = root.get('time', '0')
|
||||
print(f'- **Total Tests**: {tests}')
|
||||
print(f'- **Passed**: {int(tests) - int(failures) - int(errors) - int(skipped)}')
|
||||
print(f'- **Failed**: {failures}')
|
||||
print(f'- **Errors**: {errors}')
|
||||
print(f'- **Skipped**: {skipped}')
|
||||
print(f'- **Duration**: {float(time):.2f}s')
|
||||
" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "No integration test results found" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Upload integration test results
|
||||
if: always() && steps.should_test.outputs.test != 'false'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: integration-test-results-${{ matrix.component }}-py${{ matrix.python }}
|
||||
path: |
|
||||
${{ matrix.component }}/integration-test-results.xml
|
||||
${{ matrix.component }}/integration-test-report.html
|
||||
${{ matrix.component }}/integration-coverage.xml
|
||||
${{ matrix.component }}/integration-htmlcov/
|
||||
retention-days: 30
|
||||
@@ -17,6 +17,7 @@ on:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
types: [labeled, synchronize]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -31,7 +32,7 @@ jobs:
|
||||
- component: orchestrator
|
||||
dockerfile: ./orchestrator/Dockerfile
|
||||
- component: fuzzer
|
||||
dockerfile: ./fuzzer/dockerfiles/runner_image.Dockerfile
|
||||
dockerfile: ./fuzzer/Dockerfile
|
||||
- component: patcher
|
||||
dockerfile: ./patcher/Dockerfile
|
||||
- component: seed-gen
|
||||
@@ -44,16 +45,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
||||
with:
|
||||
dockerfile: ${{ matrix.dockerfile }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -61,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}-${{ matrix.component }}
|
||||
tags: |
|
||||
@@ -72,11 +78,11 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=${{ inputs.suffix }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: ${{ (github.event_name != 'pull_request' || startsWith(github.head_ref, 'ci/') || inputs.push) && 'true' || 'false' }}
|
||||
push: ${{ (github.event_name != 'pull_request' || startsWith(github.head_ref, 'ci/') || inputs.push || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'docker-push'))) && 'true' || 'false' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
@@ -1,214 +0,0 @@
|
||||
name: Docker Compose integration tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "ci/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
trigger_task_data:
|
||||
description: "competition-api webhook/trigger_task data (JSON)"
|
||||
required: true
|
||||
default: '{"challenge_repo_url": "git@github.com:tob-challenges/example-libpng.git", "challenge_repo_base_ref": "0cc367aaeaac3f888f255cee5d394968996f736e", "challenge_repo_head_ref": "fdacd5a1dcff42175117d674b0fda9f8a005ae88", "fuzz_tooling_url": "git@github.com:tob-challenges/oss-fuzz-aixcc.git", "fuzz_tooling_ref": "d5fbd68fca66e6fa4f05899170d24e572b01853d", "fuzz_tooling_project_name": "libpng", "duration": 7200}'
|
||||
build_timeout:
|
||||
description: "Timeout for fuzzer build in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
vuln_timeout:
|
||||
description: "Timeout for vuln submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
patch_timeout:
|
||||
description: "Timeout for patch submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
seed_gen_timeout:
|
||||
description: "Timeout for seed-gen submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
bundle_timeout:
|
||||
description: "Timeout for bundle submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
sarif_timeout:
|
||||
description: "Timeout for SARIF submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
env:
|
||||
FUZZER_BUILD_TIMEOUT: ${{ inputs.build_timeout || 25 }}
|
||||
VULN_TIMEOUT: ${{ inputs.vuln_timeout || 25 }}
|
||||
PATCH_TIMEOUT: ${{ inputs.patch_timeout || 25 }}
|
||||
BUNDLE_TIMEOUT: ${{ inputs.bundle_timeout || 5 }}
|
||||
SARIF_TIMEOUT: ${{ inputs.sarif_timeout || 5 }}
|
||||
SEED_GEN_TIMEOUT: ${{ inputs.seed_gen_timeout || 25 }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
runs-on: gha-ubuntu-32
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Set BUTTERCUP_NAMESPACE for PRs
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||
export BUTTERCUP_NAMESPACE=pr-${pull_number}-${{ github.run_number }}
|
||||
echo "BUTTERCUP_NAMESPACE=${BUTTERCUP_NAMESPACE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set BUTTERCUP_NAMESPACE for branch
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
export BUTTERCUP_NAMESPACE=${GITHUB_REF_NAME/\//-}-${{ github.run_number }}
|
||||
echo "BUTTERCUP_NAMESPACE=${BUTTERCUP_NAMESPACE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure env file for minikube
|
||||
run: |
|
||||
cp ../.github/ci-env.template env
|
||||
|
||||
sed -i "s|BUTTERCUP_NAMESPACE=.*|BUTTERCUP_NAMESPACE=${{ env.BUTTERCUP_NAMESPACE }}|" env
|
||||
sed -i "s|OPENAI_API_KEY=.*|OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}|" env
|
||||
sed -i "s|ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}|" env
|
||||
sed -i "s|LANGFUSE_HOST=.*|LANGFUSE_HOST=${{ secrets.LANGFUSE_HOST }}|" env
|
||||
sed -i "s|LANGFUSE_PUBLIC_KEY=.*|LANGFUSE_PUBLIC_KEY=${{ secrets.LANGFUSE_PUBLIC_KEY }}|" env
|
||||
sed -i "s|LANGFUSE_SECRET_KEY=.*|LANGFUSE_SECRET_KEY=${{ secrets.LANGFUSE_SECRET_KEY }}|" env
|
||||
sed -i "s|GHCR_AUTH=.*|GHCR_AUTH=$(echo "USERNAME:${{ secrets.GH_TOKEN }}" | base64 --wrap=0)|" env
|
||||
sed -i "s|OTEL_ENDPOINT=.*|OTEL_ENDPOINT=${{ secrets.OTEL_ENDPOINT }}|" env
|
||||
|
||||
working-directory: deployment
|
||||
|
||||
- name: Run CRS
|
||||
run: make deploy-local
|
||||
|
||||
- name: Submit custom task to the CRS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
kubectl port-forward -n $BUTTERCUP_NAMESPACE service/buttercup-ui 31323:1323 &
|
||||
sleep 5
|
||||
./orchestrator/scripts/custom_task_crs.sh "$DATA"
|
||||
sleep 5
|
||||
env:
|
||||
DATA: ${{ inputs.trigger_task_data }}
|
||||
|
||||
- name: Submit example-libpng task to the CRS
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |
|
||||
kubectl port-forward -n $BUTTERCUP_NAMESPACE service/buttercup-ui 31323:1323 &
|
||||
sleep 5
|
||||
./orchestrator/scripts/task_crs.sh
|
||||
sleep 5
|
||||
|
||||
- name: Wait for fuzzer build processing
|
||||
timeout-minutes: ${{ fromJSON(env.FUZZER_BUILD_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "buttercup.orchestrator.scheduler.scheduler - INFO - .* Processing build output for type FUZZER"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for vuln to be found
|
||||
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "] POV submission response: pov_id=" ; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Wait for vuln to enter the passed state in competition api
|
||||
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Updated POV status. New status PASSED" ; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for seed-gen to submit at least 1 seed
|
||||
timeout-minutes: ${{ fromJSON(env.SEED_GEN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=seed-gen -c seed-gen --tail=-1 | grep "Copied [1-9][0-9]* files to corpus"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for patch to be recorded
|
||||
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Appending patch for task"; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Wait for patch to enter the passed state in competition api
|
||||
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Patch passed"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for bundle to be submitted
|
||||
timeout-minutes: ${{ fromJSON(env.BUNDLE_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Bundle submission response: bundle_id="; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Send a SARIF broadcast
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
TASK_ID=$(kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Submitting bundle for harness" | grep -o "\[[^]]*\]" | grep -o "[^[]*$" | cut -d: -f3 | tr -d ']')
|
||||
echo "Task ID: $TASK_ID"
|
||||
while true; do
|
||||
# Kill any existing port-forward processes
|
||||
pkill -f "kubectl port-forward.*buttercup-competition-api" || true
|
||||
|
||||
# Start port forwarding in background
|
||||
kubectl port-forward -n $BUTTERCUP_NAMESPACE service/buttercup-ui 31323:1323 &
|
||||
|
||||
# Give port-forward time to establish
|
||||
sleep 5
|
||||
|
||||
# Try to send SARIF report
|
||||
if ./orchestrator/scripts/send_sarif.sh $TASK_ID; then
|
||||
# Success - exit loop
|
||||
break
|
||||
fi
|
||||
|
||||
# Failed - wait a bit before retrying
|
||||
sleep 10
|
||||
done
|
||||
- name: Wait for Bundle to be patched to include the SARIF
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Bundle patch submission response: broadcast_sarif_id="; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Wait for SARIF to be submitted as correct
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Matching SARIF submission response"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
# Disable them for now because they can contain sensitive information
|
||||
# - name: Collect logs
|
||||
# run: |
|
||||
# ./collect-logs.sh
|
||||
# mkdir -p docker_logs
|
||||
# cp -rv crs_pod_logs_* docker_logs
|
||||
# if: always()
|
||||
# working-directory: deployment
|
||||
|
||||
- name: Turn off CRS
|
||||
run: |
|
||||
make undeploy
|
||||
make clean-local
|
||||
if: always()
|
||||
|
||||
# - name: Upload Docker logs
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: docker-logs
|
||||
# path: deployment/docker_logs/
|
||||
# retention-days: 4
|
||||
# if: always()
|
||||
+141
-29
@@ -4,68 +4,180 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# Always run full suite on main branch
|
||||
pull_request:
|
||||
paths:
|
||||
# Component-specific triggers
|
||||
- "common/**"
|
||||
- "orchestrator/**"
|
||||
- "fuzzer/**"
|
||||
- "fuzzer_runner/**"
|
||||
- "program-model/**"
|
||||
- "seed-gen/**"
|
||||
- "patcher/**"
|
||||
# Workflow changes
|
||||
- ".github/workflows/lint.yml"
|
||||
- ".github/workflows/tests.yml"
|
||||
# Global configuration that affects linting
|
||||
- "Makefile"
|
||||
- "protoc.sh"
|
||||
- "**/*.proto"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Detect which components need linting based on changed files
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
common: ${{ steps.filter.outputs.common }}
|
||||
orchestrator: ${{ steps.filter.outputs.orchestrator }}
|
||||
fuzzer: ${{ steps.filter.outputs.fuzzer }}
|
||||
fuzzer_runner: ${{ steps.filter.outputs.fuzzer_runner }}
|
||||
program-model: ${{ steps.filter.outputs.program-model }}
|
||||
seed-gen: ${{ steps.filter.outputs.seed-gen }}
|
||||
patcher: ${{ steps.filter.outputs.patcher }}
|
||||
run-all: ${{ steps.filter.outputs.workflow || steps.filter.outputs.global }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
common:
|
||||
- 'common/**'
|
||||
orchestrator:
|
||||
- 'orchestrator/**'
|
||||
fuzzer:
|
||||
- 'fuzzer/**'
|
||||
fuzzer_runner:
|
||||
- 'fuzzer_runner/**'
|
||||
program-model:
|
||||
- 'program-model/**'
|
||||
seed-gen:
|
||||
- 'seed-gen/**'
|
||||
patcher:
|
||||
- 'patcher/**'
|
||||
workflow:
|
||||
- '.github/workflows/lint.yml'
|
||||
global:
|
||||
- 'Makefile'
|
||||
- 'protoc.sh'
|
||||
- '**/*.proto'
|
||||
|
||||
matrix-generator:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Generate matrix
|
||||
id: set-matrix
|
||||
env:
|
||||
RUN_ALL: ${{ needs.changes.outputs.run-all }}
|
||||
CHANGED_COMMON: ${{ needs.changes.outputs.common }}
|
||||
CHANGED_ORCHESTRATOR: ${{ needs.changes.outputs.orchestrator }}
|
||||
CHANGED_FUZZER: ${{ needs.changes.outputs.fuzzer }}
|
||||
CHANGED_FUZZER_RUNNER: ${{ needs.changes.outputs.fuzzer_runner }}
|
||||
CHANGED_PROGRAM_MODEL: ${{ needs.changes.outputs.program-model }}
|
||||
CHANGED_SEED_GEN: ${{ needs.changes.outputs.seed-gen }}
|
||||
CHANGED_PATCHER: ${{ needs.changes.outputs.patcher }}
|
||||
run: |
|
||||
components=()
|
||||
if [[ "$RUN_ALL" == "true" ]]; then
|
||||
components=("common" "orchestrator" "fuzzer" "fuzzer_runner" "program-model" "seed-gen" "patcher")
|
||||
else
|
||||
[[ "$CHANGED_COMMON" == "true" ]] && components+=("common")
|
||||
[[ "$CHANGED_ORCHESTRATOR" == "true" ]] && components+=("orchestrator")
|
||||
[[ "$CHANGED_FUZZER" == "true" ]] && components+=("fuzzer")
|
||||
[[ "$CHANGED_FUZZER_RUNNER" == "true" ]] && components+=("fuzzer_runner")
|
||||
[[ "$CHANGED_PROGRAM_MODEL" == "true" ]] && components+=("program-model")
|
||||
[[ "$CHANGED_SEED_GEN" == "true" ]] && components+=("seed-gen")
|
||||
[[ "$CHANGED_PATCHER" == "true" ]] && components+=("patcher")
|
||||
fi
|
||||
if [[ ${#components[@]} -eq 0 ]]; then
|
||||
echo 'matrix={"component":[]}' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "matrix={\"component\":[$(printf '"%s",' "${components[@]}" | sed 's/,$//')]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
lint:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [changes, matrix-generator]
|
||||
strategy:
|
||||
matrix:
|
||||
component:
|
||||
[common, orchestrator, fuzzer, program-model, seed-gen, patcher]
|
||||
continue-on-error: [true]
|
||||
include:
|
||||
- component: patcher
|
||||
continue-on-error: false
|
||||
- component: program-model
|
||||
continue-on-error: false
|
||||
- component: common
|
||||
continue-on-error: false
|
||||
- component: orchestrator
|
||||
continue-on-error: false
|
||||
- component: seed-gen
|
||||
continue-on-error: false
|
||||
fail-fast: false # Continue running other components even if one fails
|
||||
matrix: ${{ fromJSON(needs.matrix-generator.outputs.matrix) }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Setup uv cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
~/.local/share/uv
|
||||
key: ${{ runner.os }}-uv-${{ matrix.component }}-${{ hashFiles(format('{0}/uv.lock', matrix.component)) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-${{ matrix.component }}-
|
||||
${{ runner.os }}-uv-
|
||||
|
||||
- name: Setup ${{ matrix.component }} component
|
||||
run: uv sync --all-extras
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Run ruff on ${{ matrix.component }}
|
||||
run: uv run ruff format --check && uv run ruff check
|
||||
run: |
|
||||
uv run ruff format --check --diff
|
||||
uv run ruff check --output-format=github
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Run mypy on ${{ matrix.component }}
|
||||
run: uv run mypy
|
||||
- name: Run ty on ${{ matrix.component }}
|
||||
run: uv run ty check src/
|
||||
working-directory: ${{ matrix.component }}
|
||||
continue-on-error: ${{ matrix.continue-on-error }}
|
||||
|
||||
check-protobuf:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: changes
|
||||
# Run if protobuf files changed or we need to run all
|
||||
if: needs.changes.outputs.run-all == 'true' || contains(github.event.head_commit.modified, '.proto') || contains(github.event.head_commit.modified, 'protoc.sh')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y protobuf-compiler
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Generate protobufs
|
||||
run: |
|
||||
# Generate fresh protobufs
|
||||
./protoc.sh
|
||||
# Generate fresh protobufs (must run from common venv for correct grpcio-tools)
|
||||
cd common && uv run ../protoc.sh
|
||||
|
||||
# Check if any files were modified
|
||||
if ! git diff --quiet common/src/buttercup/common/datastructures; then
|
||||
if ! git diff --quiet src/buttercup/common/datastructures; then
|
||||
echo "Error: Generated protobufs differ from committed files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Static Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
static-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Lint GitHub Actions
|
||||
run: |
|
||||
# Install actionlint
|
||||
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
||||
./actionlint -color
|
||||
|
||||
- name: Security audit GitHub Actions
|
||||
run: |
|
||||
# Run zizmor in an isolated environment using uvx
|
||||
uvx zizmor .github/workflows/
|
||||
|
||||
- name: Check YAML files
|
||||
run: |
|
||||
python3 -c "
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
for f in Path('.').rglob('*.yaml'):
|
||||
if 'deployment/k8s' not in str(f):
|
||||
list(yaml.safe_load_all(f.read_text()))
|
||||
for f in Path('.').rglob('*.yml'):
|
||||
if 'deployment/k8s' not in str(f):
|
||||
list(yaml.safe_load_all(f.read_text()))
|
||||
"
|
||||
|
||||
- name: Check TOML files
|
||||
run: |
|
||||
python3 -c "
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
for f in Path('.').rglob('*.toml'):
|
||||
tomllib.load(f.open('rb'))
|
||||
"
|
||||
|
||||
- name: Check JSON files
|
||||
run: |
|
||||
python3 -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
for f in Path('.').rglob('*.json'):
|
||||
json.load(f.open())
|
||||
"
|
||||
|
||||
- name: Check for merge conflicts
|
||||
run: |
|
||||
# Match exact git merge conflict markers:
|
||||
# - <<<<<<< (followed by space, e.g., "<<<<<<< HEAD")
|
||||
# - ======= (exactly 7 equals at end of line)
|
||||
# - >>>>>>> (followed by space, e.g., ">>>>>>> branch-name")
|
||||
! grep -rE '^(<{7} |>{7} |={7}$)' --include='*.py' --include='*.yaml' --include='*.yml' --include='*.toml' --include='*.json' . || exit 1
|
||||
|
||||
- name: Lint shell scripts
|
||||
run: |
|
||||
shellcheck --version
|
||||
# Exclude external directories (node_data_storage contains cloned repos, external has vendored code)
|
||||
find . -name '*.sh' -type f \
|
||||
! -path './.git/*' \
|
||||
! -path '*/.venv/*' \
|
||||
! -path './node_data_storage/*' \
|
||||
! -path './external/*' \
|
||||
-print0 | xargs -0 shellcheck
|
||||
|
||||
- name: Lint Dockerfiles
|
||||
run: |
|
||||
# Install hadolint
|
||||
curl -sL -o hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64"
|
||||
chmod +x hadolint
|
||||
./hadolint --version
|
||||
# Exclude external directories (node_data_storage contains cloned repos, external has vendored code)
|
||||
find . -name 'Dockerfile*' -type f \
|
||||
! -path './.git/*' \
|
||||
! -path './node_data_storage/*' \
|
||||
! -path './external/*' \
|
||||
-print0 | xargs -0 ./hadolint
|
||||
@@ -0,0 +1,279 @@
|
||||
name: System Integration Tests
|
||||
|
||||
on:
|
||||
# Daily schedule - 3 AM UTC
|
||||
schedule:
|
||||
- cron: "0 3 * * 0"
|
||||
# Pull requests with 'full-integration' label
|
||||
pull_request:
|
||||
types: [labeled, synchronize]
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
trigger_task_data:
|
||||
description: "competition-api webhook/trigger_task data (JSON)"
|
||||
required: true
|
||||
default: '{"challenge_repo_url": "git@github.com:tob-challenges/example-libpng.git", "challenge_repo_base_ref": "0cc367aaeaac3f888f255cee5d394968996f736e", "challenge_repo_head_ref": "fdacd5a1dcff42175117d674b0fda9f8a005ae88", "fuzz_tooling_url": "git@github.com:tob-challenges/oss-fuzz-aixcc.git", "fuzz_tooling_ref": "d5fbd68fca66e6fa4f05899170d24e572b01853d", "fuzz_tooling_project_name": "libpng", "duration": 7200}'
|
||||
build_timeout:
|
||||
description: "Timeout for fuzzer build in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
vuln_timeout:
|
||||
description: "Timeout for vuln submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
patch_timeout:
|
||||
description: "Timeout for patch submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
seed_gen_timeout:
|
||||
description: "Timeout for seed-gen submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
bundle_timeout:
|
||||
description: "Timeout for bundle submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
sarif_timeout:
|
||||
description: "Timeout for SARIF submission in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
merger_timeout:
|
||||
description: "Timeout for merger bot in minutes"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FUZZER_BUILD_TIMEOUT: ${{ inputs.build_timeout || 25 }}
|
||||
VULN_TIMEOUT: ${{ inputs.vuln_timeout || 25 }}
|
||||
PATCH_TIMEOUT: ${{ inputs.patch_timeout || 25 }}
|
||||
BUNDLE_TIMEOUT: ${{ inputs.bundle_timeout || 5 }}
|
||||
SARIF_TIMEOUT: ${{ inputs.sarif_timeout || 5 }}
|
||||
SEED_GEN_TIMEOUT: ${{ inputs.seed_gen_timeout || 25 }}
|
||||
MERGER_TIMEOUT: ${{ inputs.merger_timeout || 10 }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
# Only run on PRs if they have the 'full-integration' label
|
||||
if: |
|
||||
github.event_name == 'schedule' ||
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'full-integration')
|
||||
runs-on: gha-ubuntu-8
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Set BUTTERCUP_NAMESPACE for PRs
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
|
||||
export BUTTERCUP_NAMESPACE="pr-${pull_number}-${{ github.run_number }}"
|
||||
echo "BUTTERCUP_NAMESPACE=${BUTTERCUP_NAMESPACE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set BUTTERCUP_NAMESPACE for branch
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
run: |
|
||||
export BUTTERCUP_NAMESPACE="${GITHUB_REF_NAME/\//-}-${{ github.run_number }}"
|
||||
echo "BUTTERCUP_NAMESPACE=${BUTTERCUP_NAMESPACE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure env file for minikube
|
||||
env:
|
||||
BUTTERCUP_NS: ${{ env.BUTTERCUP_NAMESPACE }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GEMINI_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
LF_HOST: ${{ secrets.LANGFUSE_HOST }}
|
||||
LF_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
|
||||
LF_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
|
||||
OTEL_EP: ${{ secrets.OTEL_ENDPOINT }}
|
||||
OTEL_TK: ${{ secrets.OTEL_TOKEN }}
|
||||
run: |
|
||||
# The env file is sourced by deployment/crs-architecture.sh, so every
|
||||
# value must be shell-quoted. printf %q produces a form bash can
|
||||
# re-parse as the literal string, defending against shell injection
|
||||
# from secret contents (whitespace, $(), backticks, quotes, etc.).
|
||||
write_var() {
|
||||
printf 'export %s=%q\n' "$1" "$2" >> ./env
|
||||
}
|
||||
|
||||
cp ../.github/ci-env.template ./env
|
||||
write_var BUTTERCUP_NAMESPACE "$BUTTERCUP_NS"
|
||||
write_var OPENAI_API_KEY "$OPENAI_KEY"
|
||||
write_var ANTHROPIC_API_KEY "$ANTHROPIC_KEY"
|
||||
write_var GEMINI_API_KEY "$GEMINI_KEY"
|
||||
write_var LANGFUSE_HOST "$LF_HOST"
|
||||
write_var LANGFUSE_PUBLIC_KEY "$LF_PUBLIC_KEY"
|
||||
write_var LANGFUSE_SECRET_KEY "$LF_SECRET_KEY"
|
||||
write_var OTEL_ENDPOINT "$OTEL_EP"
|
||||
write_var OTEL_TOKEN "$OTEL_TK"
|
||||
working-directory: deployment
|
||||
|
||||
- name: Run CRS
|
||||
run: FORCE=true make deploy
|
||||
|
||||
- name: Submit custom task to the CRS
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
kubectl port-forward -n "$BUTTERCUP_NAMESPACE" service/buttercup-ui 31323:1323 &
|
||||
sleep 5
|
||||
./orchestrator/scripts/custom_task_crs.sh "$DATA"
|
||||
sleep 5
|
||||
env:
|
||||
DATA: ${{ inputs.trigger_task_data }}
|
||||
|
||||
- name: Submit example-libpng task to the CRS
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |
|
||||
kubectl port-forward -n "$BUTTERCUP_NAMESPACE" service/buttercup-ui 31323:1323 &
|
||||
sleep 5
|
||||
./orchestrator/scripts/task_crs.sh
|
||||
sleep 5
|
||||
|
||||
- name: Wait for fuzzer build processing
|
||||
timeout-minutes: ${{ fromJSON(env.FUZZER_BUILD_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "buttercup.orchestrator.scheduler.scheduler - INFO - .* Processing build output for type FUZZER"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for vuln to be found
|
||||
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "] POV submission response: pov_id=" ; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Wait for vuln to enter the passed state in competition api
|
||||
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Updated POV status. New status PASSED" ; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for seed-gen to submit at least 1 seed
|
||||
timeout-minutes: ${{ fromJSON(env.SEED_GEN_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=seed-gen -c seed-gen --tail=-1 | grep "Copied [1-9][0-9]* files to corpus"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for merger-bot to submit files to remote corpus and prune local corpus
|
||||
timeout-minutes: ${{ fromJSON(env.MERGER_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=merger-bot -c merger-bot --tail=-1 | grep "Synced [1-9][0-9]* files that add coverage to remote corpus"; do
|
||||
sleep 60
|
||||
done
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=merger-bot -c merger-bot --tail=-1 | grep "Removed [1-9][0-9]* files from local corpus .* that don't add coverage"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for patch to be recorded
|
||||
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Appending patch for task"; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Approve the patch in the buttercup-ui
|
||||
run: |
|
||||
# Wait until a log line with competition_patch_id= is available, then extract PATCH_ID and TASK_ID from that line
|
||||
while true; do
|
||||
PATCH_LINE=$(kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "competition_patch_id=" | head -n 1)
|
||||
if [ -n "$PATCH_LINE" ]; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting for PATCH_ID to be available..."
|
||||
sleep 10
|
||||
done
|
||||
echo "Patch log line: $PATCH_LINE"
|
||||
# Extract PATCH_ID from the log line (after 'competition_patch_id=' and before any space or end of line)
|
||||
PATCH_ID=$(echo "$PATCH_LINE" | sed -n 's/.*competition_patch_id=\([^ ]*\).*/\1/p')
|
||||
# Extract TASK_ID from the log line (between the first '[' and the first ']'), then after the first ':' (if present)
|
||||
TASK_ID=$(echo "$PATCH_LINE" | sed -n 's/.*\[\([^]]*\)\].*/\1/p' | sed 's/^[^:]*://')
|
||||
echo "Patch ID: $PATCH_ID"
|
||||
echo "Task ID: $TASK_ID"
|
||||
kubectl port-forward -n "$BUTTERCUP_NAMESPACE" service/buttercup-ui 31323:1323 &
|
||||
sleep 5
|
||||
curl -X POST "http://localhost:31323/v1/task/${TASK_ID}/patch/${PATCH_ID}/approve"
|
||||
sleep 5
|
||||
- name: Wait for patch to enter the passed state in competition api
|
||||
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Patch passed"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Wait for bundle to be submitted
|
||||
timeout-minutes: ${{ fromJSON(env.BUNDLE_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Bundle submission response: bundle_id="; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
- name: Send a SARIF broadcast
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
TASK_ID=$(kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Submitting bundle for harness" | grep -o "\[[^]]*\]" | grep -o "[^[]*$" | cut -d: -f3 | tr -d ']')
|
||||
echo "Task ID: $TASK_ID"
|
||||
while true; do
|
||||
# Kill any existing port-forward processes
|
||||
pkill -f "kubectl port-forward.*buttercup-competition-api" || true
|
||||
|
||||
# Start port forwarding in background
|
||||
kubectl port-forward -n "$BUTTERCUP_NAMESPACE" service/buttercup-ui 31323:1323 &
|
||||
|
||||
# Give port-forward time to establish
|
||||
sleep 5
|
||||
|
||||
# Try to send SARIF report
|
||||
if ./orchestrator/scripts/send_sarif.sh "$TASK_ID"; then
|
||||
# Success - exit loop
|
||||
break
|
||||
fi
|
||||
|
||||
# Failed - wait a bit before retrying
|
||||
sleep 10
|
||||
done
|
||||
- name: Wait for Bundle to be patched to include the SARIF
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Bundle patch submission response: broadcast_sarif_id="; do
|
||||
sleep 60
|
||||
done
|
||||
- name: Wait for SARIF to be submitted as correct
|
||||
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
|
||||
run: |
|
||||
while ! kubectl logs -n "$BUTTERCUP_NAMESPACE" -l app=scheduler --tail=-1 | grep "Matching SARIF submission response"; do
|
||||
sleep 60
|
||||
done
|
||||
|
||||
# Disable them for now because they can contain sensitive information
|
||||
# - name: Collect logs
|
||||
# run: |
|
||||
# ./collect-logs.sh
|
||||
# mkdir -p docker_logs
|
||||
# cp -rv crs_pod_logs_* docker_logs
|
||||
# if: always()
|
||||
# working-directory: deployment
|
||||
|
||||
- name: Turn off CRS
|
||||
run: |
|
||||
make undeploy
|
||||
make clean-local
|
||||
if: always()
|
||||
|
||||
# - name: Upload Docker logs
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: docker-logs
|
||||
# path: deployment/docker_logs/
|
||||
# retention-days: 4
|
||||
# if: always()
|
||||
+152
-17
@@ -4,27 +4,51 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# Always run full test suite on main branch
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false # Continue running other components even if one fails
|
||||
matrix:
|
||||
component:
|
||||
[common, orchestrator, fuzzer, program-model, seed-gen, patcher]
|
||||
python:
|
||||
- "3.12"
|
||||
# TODO: Add integration tests back in
|
||||
# include:
|
||||
# - component: common
|
||||
# pytest_args: "--runintegration"
|
||||
# - component: program-model
|
||||
# pytest_args: "--runintegration"
|
||||
include:
|
||||
- component: common
|
||||
coverage_module: buttercup.common
|
||||
python: "3.12"
|
||||
- component: orchestrator
|
||||
coverage_module: buttercup.orchestrator
|
||||
python: "3.12"
|
||||
- component: program-model
|
||||
coverage_module: buttercup.program_model
|
||||
python: "3.12"
|
||||
- component: seed-gen
|
||||
coverage_module: buttercup.seed_gen
|
||||
python: "3.12"
|
||||
- component: patcher
|
||||
coverage_module: buttercup.patcher
|
||||
python: "3.12"
|
||||
- component: fuzzer
|
||||
coverage_module: buttercup.fuzzer
|
||||
python: "3.12"
|
||||
- component: fuzzer_runner
|
||||
coverage_module: buttercup.fuzzer_runner
|
||||
python: "3.12"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# Removed if: matrix.should_run since we're not using path filtering right now
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
image: redis@sha256:e647cfe134bf5e8e74e620f66346f93418acfc240b71dd85640325cb7cd01402 # 7.4
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
@@ -34,25 +58,50 @@ jobs:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
|
||||
- name: Setup uv cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
~/.local/share/uv
|
||||
key: ${{ runner.os }}-uv-${{ matrix.component }}-${{ hashFiles(format('{0}/uv.lock', matrix.component)) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-${{ matrix.component }}-
|
||||
${{ runner.os }}-uv-
|
||||
|
||||
- name: Download Wasm runtime
|
||||
run: wget https://github.com/vmware-labs/webassembly-language-runtimes/releases/download/python%2F3.12.0%2B20231211-040d5a6/python-3.12.0.wasm
|
||||
if: ${{ matrix.component }} == 'seed-gen'
|
||||
if: matrix.component == 'seed-gen'
|
||||
working-directory: seed-gen
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install dependencies for program-model, seed-gen, and patcher
|
||||
if: matrix.component == 'program-model' || matrix.component == 'seed-gen' || matrix.component == 'patcher'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y codequery ripgrep git-lfs
|
||||
sudo apt-get install -y codequery ripgrep
|
||||
make install-cscope
|
||||
|
||||
- name: Install minimal dependencies
|
||||
if: matrix.component != 'program-model' && matrix.component != 'seed-gen' && matrix.component != 'patcher' && matrix.component != 'fuzzer' && matrix.component != 'fuzzer_runner'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ripgrep
|
||||
|
||||
# Fuzzer and fuzzer_runner only need ripgrep, no codequery
|
||||
- name: Install fuzzer dependencies
|
||||
if: matrix.component == 'fuzzer' || matrix.component == 'fuzzer_runner'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ripgrep
|
||||
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
@@ -63,10 +112,96 @@ jobs:
|
||||
- name: Setup ${{ matrix.component }} component
|
||||
run: |
|
||||
uv sync --all-extras --frozen
|
||||
# Install test reporting tools into the project venv
|
||||
# This avoids adding them to every component's dependencies
|
||||
uv pip install 'pytest-html>=4.1.1' 'pytest-cov>=6.0.0'
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Run tests on ${{ matrix.component }} component
|
||||
run: uv run --frozen pytest -svv ${{ matrix.pytest_args }}
|
||||
run: |
|
||||
uv run --frozen pytest -svv \
|
||||
--junit-xml=test-results.xml \
|
||||
--html=test-report.html \
|
||||
--self-contained-html \
|
||||
--cov=${{ matrix.coverage_module }} \
|
||||
--cov-report=xml \
|
||||
--cov-report=html \
|
||||
--cov-report=term
|
||||
env:
|
||||
PYTHON_WASM_BUILD_PATH: "python-3.12.0.wasm"
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Audit dependencies for vulnerabilities
|
||||
if: always()
|
||||
run: |
|
||||
# Ignore CVEs with no available fix:
|
||||
# - CVE-2026-4539: pygments ReDoS in AdlLexer (no fix available)
|
||||
# Use --skip-editable to ignore local packages not on PyPI
|
||||
# Use uvx to run pip-audit in an isolated environment
|
||||
uvx pip-audit --strict --desc \
|
||||
--skip-editable \
|
||||
--ignore-vuln CVE-2026-4539
|
||||
working-directory: ${{ matrix.component }}
|
||||
|
||||
- name: Generate test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### Test Results: ${{ matrix.component }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -f "${{ matrix.component }}/test-results.xml" ]; then
|
||||
python -c "
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse('${{ matrix.component }}/test-results.xml')
|
||||
root = tree.getroot()
|
||||
tests = root.get('tests', '0')
|
||||
failures = root.get('failures', '0')
|
||||
errors = root.get('errors', '0')
|
||||
skipped = root.get('skipped', '0')
|
||||
time = root.get('time', '0')
|
||||
print(f'- **Total Tests**: {tests}')
|
||||
print(f'- **Passed**: {int(tests) - int(failures) - int(errors) - int(skipped)}')
|
||||
print(f'- **Failed**: {failures}')
|
||||
print(f'- **Errors**: {errors}')
|
||||
print(f'- **Skipped**: {skipped}')
|
||||
print(f'- **Duration**: {float(time):.2f}s')
|
||||
" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "No test results found" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-${{ matrix.component }}-py${{ matrix.python }}
|
||||
path: |
|
||||
${{ matrix.component }}/test-results.xml
|
||||
${{ matrix.component }}/test-report.html
|
||||
${{ matrix.component }}/coverage.xml
|
||||
${{ matrix.component }}/htmlcov/
|
||||
retention-days: 30
|
||||
|
||||
# Coverage will be uploaded in a separate job after all tests complete
|
||||
|
||||
# Consolidated coverage upload after all tests complete
|
||||
coverage-upload:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [test]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all coverage reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: test-results-*
|
||||
path: coverage-reports
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
directory: coverage-reports
|
||||
files: '*/coverage.xml,**/coverage.xml'
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
|
||||
+16
@@ -14,6 +14,7 @@ errored.tfstate
|
||||
deployment/k8s/values-*.yaml
|
||||
deployment/k8s/base/tailscale-coredns/coredns-custom.yaml
|
||||
deployment/k8s/base/tailscale-operator/operator.yaml
|
||||
deployment/k8s/base/tailscale-connections/proxies.yaml
|
||||
|
||||
## Python
|
||||
__pycache__/
|
||||
@@ -41,6 +42,14 @@ dist/
|
||||
pdm.lock
|
||||
.langchain.db
|
||||
|
||||
# Mypy type checking cache
|
||||
.mypy_cache/
|
||||
**/.mypy_cache/
|
||||
|
||||
# Hypothesis property-based testing cache
|
||||
.hypothesis/
|
||||
**/.hypothesis/
|
||||
|
||||
*.bak
|
||||
*.orig
|
||||
|
||||
@@ -50,3 +59,10 @@ common/common/datastructures/fuzzer_msg*
|
||||
crs_scratch/*
|
||||
tasks_storage/*
|
||||
node_data_storage/*
|
||||
|
||||
# Serena runtime files (keep .serena/project.yml tracked)
|
||||
.serena/memories/
|
||||
.serena/*.log
|
||||
|
||||
# WASM runtimes (downloaded for seed-gen sandbox tests)
|
||||
.wasm/
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
[submodule "external/aixcc-cscope"]
|
||||
path = external/aixcc-cscope
|
||||
url = https://github.com/trailofbits/aixcc-cscope.git
|
||||
[submodule "external/buttercup-cscope"]
|
||||
path = external/buttercup-cscope
|
||||
url = https://github.com/trail-of-forks/buttercup-cscope
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Hadolint configuration for Buttercup
|
||||
# https://github.com/hadolint/hadolint
|
||||
|
||||
ignored:
|
||||
# Pin versions in apt-get install - impractical for multi-arch builds where
|
||||
# versions vary by base image and architecture; base image provides reproducibility
|
||||
- DL3008
|
||||
|
||||
trustedRegistries:
|
||||
- docker.io
|
||||
- ghcr.io
|
||||
- gcr.io
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"serena": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"--from", "git+https://github.com/oraios/serena",
|
||||
"serena", "start-mcp-server",
|
||||
"--context", "claude-code",
|
||||
"--project", ".",
|
||||
"--enable-web-dashboard", "false"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# Pre-commit hooks for Buttercup CRS
|
||||
# See https://pre-commit.com for more information
|
||||
|
||||
repos:
|
||||
# Standard file fixes
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
exclude: 'deployment/k8s/.*\.(yaml|yml)$'
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1000]
|
||||
- id: check-merge-conflict
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
exclude: '(\.proto$|test/data/|tests/data/)'
|
||||
- id: check-toml
|
||||
- id: check-json
|
||||
- id: debug-statements
|
||||
|
||||
# Ruff - Python linting and formatting
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
# Run the formatter first
|
||||
- id: ruff-format
|
||||
types_or: [python, pyi]
|
||||
|
||||
# Then run the linter with auto-fix
|
||||
- id: ruff
|
||||
types_or: [python, pyi]
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
# ShellCheck - Shell script linting (all .sh files)
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: [--severity=warning]
|
||||
|
||||
# shfmt - Shell script formatting (all .sh files)
|
||||
- repo: https://github.com/scop/pre-commit-shfmt
|
||||
rev: v3.12.0-2
|
||||
hooks:
|
||||
- id: shfmt
|
||||
args: [-i, '4', -ci]
|
||||
|
||||
# actionlint - GitHub Actions workflow linting
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.10
|
||||
hooks:
|
||||
- id: actionlint
|
||||
|
||||
# zizmor - GitHub Actions security analysis
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.22.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args: [--min-severity=low, --min-confidence=medium]
|
||||
|
||||
# hadolint - Dockerfile linting
|
||||
- repo: https://github.com/AleksaC/hadolint-py
|
||||
rev: v2.14.0
|
||||
hooks:
|
||||
- id: hadolint
|
||||
args: [--failure-threshold, warning]
|
||||
|
||||
# ty - Astral type checking (local hook)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ty
|
||||
name: ty type checking
|
||||
entry: >-
|
||||
bash -c 'for dir in common orchestrator fuzzer fuzzer_runner patcher program-model seed-gen; do
|
||||
if [[ "$1" == "$dir/"* ]]; then cd "$dir" && uvx ty check src/; exit $?; fi; done' --
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^(common|orchestrator|fuzzer|fuzzer_runner|patcher|program-model|seed-gen)/src/
|
||||
pass_filenames: true
|
||||
|
||||
# Global exclusions for files that should never be checked
|
||||
exclude: |
|
||||
(?x)^(
|
||||
external/.*|
|
||||
.*\.pb\.py|
|
||||
.*_pb2\.py|
|
||||
.*_pb2_grpc\.py|
|
||||
.*/\.venv/.*|
|
||||
.*/\.mypy_cache/.*|
|
||||
.*/\.ruff_cache/.*|
|
||||
.*/\.pytest_cache/.*|
|
||||
__pycache__/.*|
|
||||
.*\.egg-info/.*
|
||||
)$
|
||||
|
||||
# Configuration
|
||||
fail_fast: false
|
||||
ci:
|
||||
autofix_prs: true
|
||||
autoupdate_schedule: weekly
|
||||
@@ -0,0 +1 @@
|
||||
/cache
|
||||
@@ -0,0 +1,66 @@
|
||||
languages:
|
||||
- python
|
||||
|
||||
encoding: utf-8
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
roots:
|
||||
- common
|
||||
- orchestrator
|
||||
- fuzzer
|
||||
- fuzzer_runner
|
||||
- patcher
|
||||
- program-model
|
||||
- seed-gen
|
||||
|
||||
ignore:
|
||||
- ".git"
|
||||
- ".venv"
|
||||
- "__pycache__"
|
||||
- "*.egg-info"
|
||||
- "*.pyc"
|
||||
- "build"
|
||||
- "dist"
|
||||
- ".ruff_cache"
|
||||
- ".mypy_cache"
|
||||
- ".pytest_cache"
|
||||
- ".hypothesis"
|
||||
- "htmlcov"
|
||||
- ".idea"
|
||||
- ".vscode"
|
||||
- ".worktrees"
|
||||
- "crs_scratch"
|
||||
- "tasks_storage"
|
||||
- "external"
|
||||
- "**/node_modules"
|
||||
- "**/uv.lock"
|
||||
- "**/*.pb2.py"
|
||||
- "litellm/**"
|
||||
- "deployment/k8s/**"
|
||||
- "competition-server/**"
|
||||
|
||||
allow_write: true
|
||||
|
||||
excluded_tools:
|
||||
- onboarding
|
||||
- check_onboarding_performed
|
||||
- list_dir
|
||||
- find_file
|
||||
- write_memory
|
||||
- read_memory
|
||||
- list_memories
|
||||
- delete_memory
|
||||
- edit_memory
|
||||
|
||||
initial_prompt: |
|
||||
This project has a comprehensive CLAUDE.md with architecture, development
|
||||
commands, and component-specific patterns. Refer to it for project context
|
||||
rather than using onboarding or memories.
|
||||
|
||||
Tool usage patterns for this multi-component codebase:
|
||||
- Use get_symbols_overview before reading files to understand structure
|
||||
- Use find_symbol to locate definitions across the 7 component packages
|
||||
- Use find_referencing_symbols to trace Redis message handlers and protobuf usage
|
||||
- Prefer replace_symbol_body over line-based edits for precision
|
||||
- Use search_for_pattern for YAML configs and proto files (not indexed by LSP)
|
||||
- Call think_about_collected_information after exploring before making changes
|
||||
@@ -0,0 +1,6 @@
|
||||
# Shellcheck configuration for Buttercup
|
||||
shell=bash
|
||||
|
||||
# Globally disable SC1091 (source file not followed)
|
||||
# These are dynamically sourced or external files
|
||||
disable=SC1091
|
||||
@@ -17,6 +17,9 @@ make lint # Format, lint, and type-check all c
|
||||
|
||||
# Example: lint the patcher component
|
||||
make lint-component COMPONENT=patcher
|
||||
|
||||
# Regenerate protobuf files (MUST run from common venv for correct grpcio-tools)
|
||||
cd common && uv run ../protoc.sh
|
||||
```
|
||||
|
||||
### Testing
|
||||
@@ -126,6 +129,12 @@ cd <component> && uv lock --upgrade
|
||||
- Mock external dependencies (Redis, LLM APIs, file system)
|
||||
- Integration tests use Docker containers
|
||||
- Test data stored in `<component>/tests/data/`
|
||||
- **Redis-dependent tests**: Several components (common, orchestrator, patcher, fuzzer, seed-gen) have tests that require a running Redis instance. Do not skip these tests. Start a temporary Redis container before running tests:
|
||||
```bash
|
||||
docker run -d --name redis-temp -p 6379:6379 redis:latest
|
||||
# Run tests...
|
||||
docker stop redis-temp && docker rm redis-temp
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
@@ -134,6 +143,14 @@ cd <component> && uv lock --upgrade
|
||||
- Line length: 120 characters
|
||||
- Pydantic models for data validation
|
||||
|
||||
### Code Navigation
|
||||
|
||||
- Use `get_symbols_overview` before reading full files to understand structure
|
||||
- Use `find_symbol` to locate classes/functions across components
|
||||
- Use `find_referencing_symbols` to trace message flow between services
|
||||
- Use `search_for_pattern` for non-code files (YAML, proto, configs)
|
||||
- Prefer symbol-based edits (`replace_symbol_body`, `insert_after_symbol`) over line-based edits
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
The system runs as Kubernetes microservices with Helm charts in `/deployment/k8s/`:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
* @ret2libc @hbrodin @michaelbrownuc
|
||||
seed-gen @reytchison
|
||||
fuzzer @reytchison
|
||||
fuzzer_runner @reytchison
|
||||
program-model @evandowning
|
||||
patcher @ret2libc
|
||||
orchestrator @hbrodin
|
||||
deployments @ret2libc @hbrodin
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Contributing to Buttercup CRS
|
||||
|
||||
Thank you for contributing to the Buttercup Cyber Reasoning System!
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone --recurse-submodules https://github.com/trailofbits/buttercup.git
|
||||
cd buttercup
|
||||
make setup-local # Automated setup
|
||||
make deploy # Start environment
|
||||
|
||||
# Setup development tools (optional but recommended)
|
||||
cargo install prek # Fast Rust-based git hooks
|
||||
prek install # Auto-runs checks on commit
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
make help # View all commands
|
||||
make lint # Lint all components
|
||||
make lint-component COMPONENT=orchestrator # Lint specific component
|
||||
make send-libpng-task # Run test task
|
||||
make status # Check deployment status
|
||||
make undeploy # Clean up resources
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Tools:** `ruff` (formatting/linting), `ty` (type checking)
|
||||
- **Git hooks:** `prek` automatically validates code, configs, and line endings on commit
|
||||
- **Manual checks:** `prek run` or `make lint`
|
||||
|
||||
### Test Prerequisites
|
||||
|
||||
**Redis** (required for unit tests):
|
||||
```bash
|
||||
# Start Redis with Docker
|
||||
docker run -d -p 6379:6379 --name buttercup-redis redis:7.4
|
||||
|
||||
# Or on macOS with Homebrew
|
||||
brew install redis && brew services start redis
|
||||
```
|
||||
|
||||
**System dependencies** (varies by component):
|
||||
|
||||
| Component | Dependencies |
|
||||
|-----------|-------------|
|
||||
| common, orchestrator | `ripgrep` |
|
||||
| fuzzer, fuzzer_runner | `ripgrep` |
|
||||
| program-model, patcher, seed-gen | `ripgrep`, `codequery`, `universal-ctags`, custom cscope |
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install ripgrep codequery universal-ctags autoconf automake
|
||||
git submodule update --init external/buttercup-cscope
|
||||
make install-cscope
|
||||
|
||||
# IMPORTANT: universal-ctags must be in PATH before BSD ctags (macOS default).
|
||||
# BSD ctags is incompatible with cscope/codequery. Add to ~/.zshrc or ~/.bashrc:
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install -y ripgrep codequery universal-ctags autoconf automake
|
||||
git submodule update --init external/buttercup-cscope
|
||||
make install-cscope
|
||||
```
|
||||
|
||||
**seed-gen only** - requires WASM Python build path:
|
||||
```bash
|
||||
export PYTHON_WASM_BUILD_PATH="python-3.12.0.wasm"
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
1. **Unit Tests** (5-10 min): Run on all PRs
|
||||
```bash
|
||||
cd <component> && uv run pytest
|
||||
```
|
||||
|
||||
2. **Integration Tests** (15-30 min): Daily or with `integration-tests` label
|
||||
```bash
|
||||
# Requires: codequery, ripgrep, cscope (for program-model, patcher, seed-gen)
|
||||
# Requires: NODE_DATA_DIR env var pointing to writable scratch directory
|
||||
NODE_DATA_DIR=/tmp/buttercup_test uv run pytest --runintegration
|
||||
```
|
||||
|
||||
**macOS limitations:** Some integration tests require Docker builds that only work on
|
||||
x86_64 Linux (CI). Tests that work locally on macOS ARM64:
|
||||
- `common/tests/test_reliable_queue.py` - Redis queue tests
|
||||
- `program-model/tests/test_tree_sitter.py` - Tree-sitter parsing
|
||||
- `patcher/tests/test_utils.py`, `test_context_retriever.py` - Patcher utilities
|
||||
- `seed-gen/test/test_find_harness.py` - Harness finding (some skipped)
|
||||
|
||||
3. **Full System** (90+ min): Weekly or with `full-integration` label
|
||||
```bash
|
||||
make deploy && make send-libpng-task
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `/common/` | Shared utilities, protobufs, Redis queues |
|
||||
| `/orchestrator/` | Task coordination, scheduling, API client |
|
||||
| `/fuzzer/` | Vulnerability discovery bots |
|
||||
| `/fuzzer_runner/` | Fuzzer execution runner |
|
||||
| `/program-model/` | Code analysis (CodeQuery, Tree-sitter) |
|
||||
| `/patcher/` | LLM-powered patch generation |
|
||||
| `/seed-gen/` | Intelligent input generation |
|
||||
|
||||
## Contribution Process
|
||||
|
||||
1. **Branch** from main: `git checkout -b feature/your-feature`
|
||||
2. **Code** following existing patterns and conventions
|
||||
3. **Test** your changes at appropriate level
|
||||
4. **Commit** (prek hooks run automatically if installed)
|
||||
5. **Push** and create PR with clear description
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Each component uses `uv`:
|
||||
```bash
|
||||
cd <component>
|
||||
uv sync # Install dependencies
|
||||
uv add <package> # Add new dependency
|
||||
uv lock --upgrade # Update dependencies
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Code Style
|
||||
- Follow existing patterns in each component
|
||||
- Use structured logging and Pydantic models
|
||||
- Handle errors with circuit breakers for external services
|
||||
- Write tests for new functionality
|
||||
|
||||
### PR Labels
|
||||
- `integration-tests` - Triggers component integration tests
|
||||
- `full-integration` - Triggers full system test (use sparingly)
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
# Kubernetes commands
|
||||
kubectl logs -n crs -l app=<service> --tail=100
|
||||
kubectl exec -it -n crs <pod> -- /bin/bash
|
||||
kubectl port-forward -n crs service/buttercup-competition-api 31323:1323
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Environment issues?** Run `make validate` to check if your setup is ready
|
||||
- **Tests failing?** Ensure Redis is running (see [Test Prerequisites](#test-prerequisites)) and dependencies are installed with `cd <component> && uv sync`
|
||||
- **Missing system tools?** See the [Test Prerequisites](#test-prerequisites) table for component-specific dependencies
|
||||
|
||||
## Resources
|
||||
|
||||
- [Quick Reference](guides/QUICK_REFERENCE.md) - Common commands and troubleshooting
|
||||
- [Deployment Guide](deployment/README.md) - Detailed deployment information
|
||||
- [Custom Challenges](guides/CUSTOM_CHALLENGES.md) - Adding new test cases
|
||||
|
||||
## Questions?
|
||||
|
||||
Open an issue or reach out to the development team.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Makefile for Trail of Bits AIxCC Finals CRS
|
||||
|
||||
.PHONY: help setup-local setup-azure validate deploy deploy-local deploy-azure test undeploy install-cscope lint lint-component clean-local wait-crs check-crs crs-instance-id status send-integration-task
|
||||
.PHONY: help setup-local setup-azure validate deploy test undeploy install-cscope lint lint-component clean-local wait-crs check-crs crs-instance-id status send-integration-task
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -13,13 +13,12 @@ help:
|
||||
@echo ""
|
||||
@echo "Deployment:"
|
||||
@echo " deploy - Deploy to current environment (local or azure)"
|
||||
@echo " deploy-local - Deploy to local Minikube environment"
|
||||
@echo " deploy-azure - Deploy to production AKS environment"
|
||||
@echo ""
|
||||
@echo "Status:"
|
||||
@echo " status - Check the status of the deployment"
|
||||
@echo " crs-instance-id - Get the CRS instance ID"
|
||||
@echo " download-artifacts - Download submitted artifacts from the CRS"
|
||||
@echo " signoz-ui - Open the SigNoz UI"
|
||||
@echo ""
|
||||
@echo "Testing:"
|
||||
@echo " send-integration-task - Run integration-test task"
|
||||
@@ -48,16 +47,6 @@ validate:
|
||||
@echo "Validating setup..."
|
||||
./scripts/validate-setup.sh
|
||||
|
||||
# Deployment targets
|
||||
deploy:
|
||||
@echo "Deploying to current environment..."
|
||||
@if [ ! -f external/aixcc-cscope/configure.ac ]; then \
|
||||
echo "Error: The git submodules have not been initialized. Run 'git submodule update --init --recursive' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
cd deployment && make up
|
||||
make wait-crs
|
||||
|
||||
wait-crs:
|
||||
@echo "Waiting for CRS deployment to be ready..."
|
||||
@if ! kubectl get namespace $${BUTTERCUP_NAMESPACE:-crs} >/dev/null 2>&1; then \
|
||||
@@ -96,35 +85,32 @@ crs-instance-id:
|
||||
fi
|
||||
echo "CRS instance ID: $$(kubectl get configmap -n $${BUTTERCUP_NAMESPACE:-crs} crs-instance-id -o jsonpath='{.data.crs-instance-id}')"
|
||||
|
||||
deploy-local:
|
||||
@echo "Deploying to local Minikube environment..."
|
||||
deploy:
|
||||
@echo "Deploying environment..."
|
||||
@if [ ! -f deployment/env ]; then \
|
||||
echo "Error: Configuration file not found. Run 'make setup-local' first."; \
|
||||
echo "Error: Configuration file not found. Run 'make setup-local' or `make setup-azure` first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f external/aixcc-cscope/configure.ac ]; then \
|
||||
@if [ ! -f external/buttercup-cscope/configure.ac ]; then \
|
||||
echo "Error: The git submodules have not been initialized. Run 'git submodule update --init --recursive' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Deployment configuration:"
|
||||
@grep 'CLUSTER_TYPE=' deployment/env || echo "CLUSTER_TYPE not set"
|
||||
@grep 'K8S_VALUES_TEMPLATE' deployment/env || echo "K8S_VALUES_TEMPLATE not set"
|
||||
@if [ "$${FORCE:-false}" != "true" ]; then \
|
||||
echo "Are you sure you want to deploy with these settings? Type 'yes' to continue:"; \
|
||||
read ans; \
|
||||
ans_lc=$$(echo "$$ans" | tr '[:upper:]' '[:lower:]'); \
|
||||
if [ "$$ans_lc" != "yes" ] && [ "$$ans_lc" != "y" ]; then \
|
||||
echo "Aborted by user."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
cd deployment && make up
|
||||
make crs-instance-id
|
||||
make wait-crs
|
||||
|
||||
deploy-azure:
|
||||
@echo "Deploying to production AKS environment..."
|
||||
@if [ ! -f deployment/env ]; then \
|
||||
echo "Error: Configuration file not found. Run 'make setup-azure' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f external/aixcc-cscope/configure.ac ]; then \
|
||||
echo "Error: The git submodules have not been initialized. Run 'git submodule update --init --recursive' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
cd deployment && make up
|
||||
crs_instance_id=$$(make crs-instance-id)
|
||||
echo "CRS instance ID: $$crs_instance_id"
|
||||
make wait-crs
|
||||
|
||||
status:
|
||||
@echo "----------PODS------------"
|
||||
@kubectl get pods -n $${BUTTERCUP_NAMESPACE:-crs}
|
||||
@@ -147,11 +133,11 @@ send-integration-task:
|
||||
echo "Error: CRS namespace not found. Deploy first with 'make deploy'."; \
|
||||
exit 1; \
|
||||
fi
|
||||
kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 &
|
||||
@sleep 3
|
||||
./orchestrator/scripts/task_integration_test.sh
|
||||
pkill -f "kubectl port-forward" || true
|
||||
exit 0
|
||||
@kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 & \
|
||||
PORT_FORWARD_PID=$$!; \
|
||||
sleep 3; \
|
||||
./orchestrator/scripts/task_integration_test.sh; \
|
||||
kill $$PORT_FORWARD_PID 2>/dev/null || true
|
||||
|
||||
send-libpng-task:
|
||||
@echo "Running libpng task..."
|
||||
@@ -159,11 +145,11 @@ send-libpng-task:
|
||||
echo "Error: CRS namespace not found. Deploy first with 'make deploy'."; \
|
||||
exit 1; \
|
||||
fi
|
||||
kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 &
|
||||
@sleep 3
|
||||
./orchestrator/scripts/task_crs.sh
|
||||
pkill -f "kubectl port-forward" || true
|
||||
exit 0
|
||||
@kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 & \
|
||||
PORT_FORWARD_PID=$$!; \
|
||||
sleep 3; \
|
||||
./orchestrator/scripts/task_crs.sh; \
|
||||
kill $$PORT_FORWARD_PID 2>/dev/null || true
|
||||
|
||||
submit-project:
|
||||
@echo "Starting interactive custom challenge submission..."
|
||||
@@ -179,26 +165,24 @@ submit-project:
|
||||
# Development targets
|
||||
lint:
|
||||
@echo "Linting all Python code..."
|
||||
@for component in common orchestrator fuzzer program-model seed-gen patcher; do \
|
||||
@set -e; for component in common orchestrator fuzzer fuzzer_runner program-model seed-gen patcher; do \
|
||||
make --no-print-directory lint-component COMPONENT=$$component; \
|
||||
done
|
||||
|
||||
# Note: common, patcher, orchestrator, program-model, seed-gen run mypy
|
||||
# Note: all components run ty type checking
|
||||
lint-component:
|
||||
@if [ -z "$(COMPONENT)" ]; then \
|
||||
echo "Error: COMPONENT not specified. Usage: make lint-component COMPONENT=<component>"; \
|
||||
echo "Available components: common, fuzzer, orchestrator, patcher, program-model, seed-gen"; \
|
||||
echo "Available components: common, fuzzer, fuzzer_runner, orchestrator, patcher, program-model, seed-gen"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Linting $(COMPONENT)..."
|
||||
@cd $(COMPONENT) && uv sync -q --all-extras && uv run ruff format --check && uv run ruff check
|
||||
@if [ "$(COMPONENT)" = "common" ] || [ "$(COMPONENT)" = "patcher" ] || [ "$(COMPONENT)" = "orchestrator" ] || [ "$(COMPONENT)" = "program-model" ] || [ "$(COMPONENT)" = "seed-gen" ]; then \
|
||||
cd $(COMPONENT) && uv run mypy; \
|
||||
fi
|
||||
@cd $(COMPONENT) && uv run ty check src/
|
||||
|
||||
reformat:
|
||||
@echo "Reformatting all Python code..."
|
||||
@for component in common orchestrator fuzzer program-model seed-gen patcher; do \
|
||||
@for component in common orchestrator fuzzer fuzzer_runner program-model seed-gen patcher; do \
|
||||
make --no-print-directory reformat-component COMPONENT=$$component; \
|
||||
done
|
||||
|
||||
@@ -216,6 +200,10 @@ undeploy:
|
||||
@echo "Cleaning up deployment..."
|
||||
cd deployment && make down
|
||||
|
||||
undeploy-k8s:
|
||||
@echo "Cleaning up kubernetes resources..."
|
||||
cd deployment && make down-k8s
|
||||
|
||||
clean-local:
|
||||
@echo "Cleaning up local environment..."
|
||||
minikube delete || true
|
||||
@@ -223,20 +211,30 @@ clean-local:
|
||||
|
||||
# Additional targets migrated from justfile
|
||||
install-cscope:
|
||||
cd external/aixcc-cscope/ && autoreconf -i -s && ./configure && make && sudo make install
|
||||
cd external/buttercup-cscope/ && autoreconf -i -s && ./configure && make && sudo make install
|
||||
|
||||
web-ui:
|
||||
@echo "Opening web UI..."
|
||||
signoz-ui:
|
||||
@echo "Opening SigNoz UI..."
|
||||
@echo ""
|
||||
@echo "NOTE: If you plan to redeploy and access SigNoz, clear your browser cookies for http://localhost:33301 to avoid login issues."
|
||||
@echo ""
|
||||
@if ! kubectl get namespace $${BUTTERCUP_NAMESPACE:-crs} >/dev/null 2>&1; then \
|
||||
echo "Error: CRS namespace not found. Deploy first with 'make deploy'."; \
|
||||
exit 1; \
|
||||
fi
|
||||
kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 &
|
||||
@if ! kubectl get service/buttercup-signoz-frontend -n $${BUTTERCUP_NAMESPACE:-crs} >/dev/null 2>&1; then \
|
||||
echo "Error: SigNoz is not deployed. Set DEPLOY_SIGNOZ=true in deployment/env and redeploy."; \
|
||||
exit 1; \
|
||||
fi
|
||||
kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-signoz-frontend 33301:3301 &
|
||||
@sleep 3
|
||||
@if command -v xdg-open >/dev/null 2>&1; then \
|
||||
xdg-open http://localhost:31323; \
|
||||
xdg-open http://localhost:33301; \
|
||||
elif command -v open >/dev/null 2>&1; then \
|
||||
open http://localhost:31323; \
|
||||
open http://localhost:33301; \
|
||||
else \
|
||||
echo "Please open http://localhost:31323 in your browser."; \
|
||||
echo "Please open http://localhost:33301 in your browser."; \
|
||||
fi
|
||||
|
||||
web-ui:
|
||||
@./scripts/web_ui.sh
|
||||
|
||||
@@ -1,23 +1,72 @@
|
||||
# Buttercup Cyber Reasoning System (CRS)
|
||||
|
||||
**Buttercup** is a Cyber Reasoning System (CRS) developed by **Trail of Bits** for the **DARPA AIxCC (AI Cyber Challenge) competition**. It's a comprehensive automated vulnerability detection and patching system designed to compete in AI-driven cybersecurity challenges.
|
||||
[](https://github.com/trailofbits/buttercup/actions/workflows/tests.yml)
|
||||
[](https://github.com/trailofbits/buttercup/actions/workflows/comp-integration.yml)
|
||||
[](https://github.com/trailofbits/buttercup/actions/workflows/integration.yml)
|
||||
|
||||
**Buttercup** is a Cyber Reasoning System (CRS) developed by **Trail of Bits** for the **DARPA AIxCC (AI Cyber Challenge)**. Buttercup finds and patches software vulnerabilities in open-source code repositories like [example-libpng](https://github.com/tob-challenges/example-libpng). It starts by running an AI/ML-assisted fuzzing campaign (built on oss-fuzz) for the program. When vulnerabilities are found, Buttercup analyzes them and uses a multi-agent AI-driven patcher to repair the vulnerability. **Buttercup** system consists of several components:
|
||||
|
||||
- **Orchestrator**: Coordinates the overall task process and manages the workflow
|
||||
- **Seed Generator**: Creates inputs for vulnerability discovery
|
||||
- **Fuzzer**: Discovers vulnerabilities through intelligent fuzzing techniques
|
||||
- **Program Model**: Analyzes code structure and semantics for better understanding
|
||||
- **Patcher**: Generates and applies security patches to fix vulnerabilities
|
||||
|
||||
## System Requirements
|
||||
|
||||
### Minimum Requirements
|
||||
|
||||
- **CPU:** 8 cores
|
||||
- **Memory:** 16 GB RAM
|
||||
- **Storage:** 100 GB available disk space
|
||||
- **Network:** Stable internet connection for downloading dependencies
|
||||
|
||||
**Note:** Buttercup uses third-party AI providers (LLMs from companies like OpenAI, Anthropic and Google), which cost money. Please ensure that you manage per-deployment costs by using the built-in LLM budget setting.
|
||||
|
||||
**Note:** Buttercup works best with access to models from OpenAI **and** Anthropic, but can be run with at least one API key from one third-party provider. You can use a combination of OpenAI, Anthropic, and Google LLMs.
|
||||
|
||||
### Supported Systems
|
||||
|
||||
- **Linux x86_64** (fully supported)
|
||||
- **macOS** (local development supported via Docker Desktop or Colima)
|
||||
- **ARM64** (partial support for upstream Google OSS-Fuzz projects)
|
||||
|
||||
### Required System Packages
|
||||
|
||||
Before setup, ensure you have these packages installed:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y make curl git
|
||||
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum install -y make curl git
|
||||
# or
|
||||
sudo dnf install -y make curl git
|
||||
|
||||
# MacOS
|
||||
brew install make curl git
|
||||
```
|
||||
|
||||
### Supported Targets
|
||||
|
||||
Buttercup works with:
|
||||
|
||||
- **C source code repositories** that are OSS-Fuzz compatible
|
||||
- **Java source code repositories** that are OSS-Fuzz compatible
|
||||
- Projects that build successfully and have existing fuzzing harnesses
|
||||
|
||||
## Quick Start
|
||||
|
||||
Clone the repo with `--recurse-submodules` as some dependencies are submodules.
|
||||
1. Clone the repository with submodules:
|
||||
|
||||
Choose your deployment method:
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/trailofbits/buttercup.git
|
||||
cd buttercup
|
||||
```
|
||||
|
||||
- **[Local Development](#local-development)** - Quick setup for development and testing
|
||||
- **[Production AKS Deployment](#production-aks-deployment)** - Full production deployment on Azure Kubernetes Service
|
||||
|
||||
## Local Development
|
||||
|
||||
The fastest way to get started with the **Buttercup CRS** system for development and testing.
|
||||
|
||||
### Quick Setup (Recommended)
|
||||
|
||||
Use our automated setup script:
|
||||
1. Run automated setup (Recommended)
|
||||
|
||||
```bash
|
||||
make setup-local
|
||||
@@ -25,339 +74,86 @@ make setup-local
|
||||
|
||||
This script will install all dependencies, configure the environment, and guide you through the setup process.
|
||||
|
||||
### Manual Setup
|
||||
**Note:** If you prefer manual setup, see the [Manual Setup Guide](guides/MANUAL_SETUP.md).
|
||||
|
||||
If you prefer to set up manually, follow these steps:
|
||||
1. Ensure Docker is running before deploying:
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker $USER
|
||||
# Log out and back in for group changes to take effect
|
||||
|
||||
# kubectl
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||||
|
||||
# Helm
|
||||
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
# Minikube
|
||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
||||
|
||||
# Git LFS (for some tests)
|
||||
sudo apt-get install git-lfs
|
||||
git lfs install
|
||||
# Docker Desktop: start the application
|
||||
# Colima (macOS/Linux alternative):
|
||||
colima start --cpu 6 --memory 10 --disk 80
|
||||
```
|
||||
|
||||
#### Manual Configuration
|
||||
|
||||
1. **Create configuration file:**
|
||||
1. Start Buttercup locally
|
||||
|
||||
```bash
|
||||
cp deployment/env.template deployment/env
|
||||
make deploy
|
||||
```
|
||||
|
||||
2. **Configure the environment file** (`deployment/env`):
|
||||
|
||||
Look at the comments in the `deployment/env.template` for how to set variables.
|
||||
|
||||
### Start Local Development Environment
|
||||
|
||||
1. **Start the services:**
|
||||
|
||||
```bash
|
||||
make deploy-local
|
||||
```
|
||||
|
||||
2. **Verify deployment:**
|
||||
1. Verify local deployment:
|
||||
|
||||
```bash
|
||||
make status
|
||||
```
|
||||
|
||||
When the deployment is succesful, you should see something like
|
||||
When a deployment is successful, you should see all pods in "Running" or "Completed" status.
|
||||
|
||||
```shell
|
||||
$ make status
|
||||
----------PODS------------
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
buttercup-build-bot-845f5b96d9-7t8bz 1/1 Running 0 5m58s
|
||||
buttercup-build-bot-845f5b96d9-bfsq9 1/1 Running 0 5m58s
|
||||
buttercup-build-bot-845f5b96d9-npns4 1/1 Running 0 5m58s
|
||||
buttercup-build-bot-845f5b96d9-sv5fr 1/1 Running 0 5m58s
|
||||
buttercup-coverage-bot-6749f57b9d-4gzfd 1/1 Running 0 5m58s
|
||||
buttercup-dind-452s6 1/1 Running 0 5m58s
|
||||
buttercup-fuzzer-bot-74bc9b849d-2zkt6 1/1 Running 0 5m58s
|
||||
buttercup-image-preloader-97nfb 0/1 Completed 0 5m58s
|
||||
buttercup-litellm-5f87df944-2mq7z 1/1 Running 0 5m58s
|
||||
buttercup-litellm-migrations-ljjcl 0/1 Completed 0 5m58s
|
||||
buttercup-merger-bot-fz87v 1/1 Running 0 5m58s
|
||||
buttercup-patcher-7597c965b8-6968s 1/1 Running 0 5m58s
|
||||
buttercup-postgresql-0 1/1 Running 0 5m58s
|
||||
buttercup-pov-reproducer-5f948bd7cc-45rgp 1/1 Running 0 5m58s
|
||||
buttercup-program-model-67446b5cfc-24zfh 1/1 Running 0 5m58s
|
||||
buttercup-redis-master-0 1/1 Running 0 5m58s
|
||||
buttercup-registry-cache-5787f86896-czt9b 1/1 Running 0 5m58s
|
||||
buttercup-scheduler-7c49bf75c5-swqkb 1/1 Running 0 5m58s
|
||||
buttercup-scratch-cleaner-hdt6z 1/1 Running 0 5m58s
|
||||
buttercup-seed-gen-6fdb9c94c9-4xmrp 1/1 Running 0 5m57s
|
||||
buttercup-task-downloader-54cd9fb577-g4lbg 1/1 Running 0 5m58s
|
||||
buttercup-task-server-7d8cd7cf49-zkt69 1/1 Running 0 5m58s
|
||||
buttercup-tracer-bot-5b9fb6c8b5-zcmxd 1/1 Running 0 5m58s
|
||||
buttercup-ui-5dcf7dfb8-njglh 1/1 Running 0 5m58s
|
||||
----------SERVICES--------
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
buttercup-litellm ClusterIP 10.96.88.226 <none> 4000/TCP 5m58s
|
||||
buttercup-postgresql ClusterIP 10.111.161.207 <none> 5432/TCP 5m58s
|
||||
buttercup-postgresql-hl ClusterIP None <none> 5432/TCP 5m58s
|
||||
buttercup-redis-headless ClusterIP None <none> 6379/TCP 5m58s
|
||||
buttercup-redis-master ClusterIP 10.108.61.77 <none> 6379/TCP 5m58s
|
||||
buttercup-registry-cache ClusterIP 10.103.80.241 <none> 443/TCP 5m58s
|
||||
buttercup-task-server ClusterIP 10.104.206.197 <none> 8000/TCP 5m58s
|
||||
buttercup-ui ClusterIP 10.106.49.166 <none> 1323/TCP 5m58s
|
||||
All CRS pods up and running.
|
||||
```
|
||||
1. Send Buttercup a simple task
|
||||
|
||||
3. **Submit the libpng project to the CRS (for 30mins):**
|
||||
**Note:** When tasked, Buttercup will start consuming third-party AI resources.
|
||||
|
||||
This command will make Buttercup pull down an example repo [example-libpng](https://github.com/tob-challenges/example-libpng) with a known vulnerability. Buttercup will start fuzzing it to find and patch vulnerabilities.
|
||||
|
||||
```bash
|
||||
make send-libpng-task
|
||||
```
|
||||
|
||||
**Alternative manual commands:**
|
||||
1. Access Buttercup's web-based GUI
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
# Start services manually
|
||||
cd deployment && make up
|
||||
|
||||
# Port forward manually
|
||||
kubectl port-forward -n crs service/buttercup-ui 31323:1323
|
||||
|
||||
# Test manually
|
||||
./orchestrator/scripts/task_integration_test.sh
|
||||
make web-ui
|
||||
```
|
||||
|
||||
## Production AKS Deployment
|
||||
Then navigate to `http://localhost:31323` in your web browser.
|
||||
|
||||
> **⚠️ Notice:**
|
||||
> The following production deployment instructions have **not been fully tested**.
|
||||
> Please proceed with caution and verify each step in your environment.
|
||||
> If you encounter issues, consult the script comments and configuration files for troubleshooting.
|
||||
In the GUI you can monitor active tasks and see when Buttercup finds bugs and generates patches for them.
|
||||
|
||||
Full production deployment of the **Buttercup CRS** on Azure Kubernetes Service with proper networking, monitoring, and scaling for the DARPA AIxCC competition.
|
||||
1. Stop Buttercup
|
||||
|
||||
### Quick Setup (Recommended)
|
||||
|
||||
Use our automated setup script:
|
||||
|
||||
```bash
|
||||
make setup-azure
|
||||
```
|
||||
|
||||
This script will check prerequisites, help create service principals, configure the environment, and validate your setup.
|
||||
|
||||
#### Manual Setup
|
||||
|
||||
If you prefer to set up manually, follow these steps:
|
||||
|
||||
##### Prerequisites
|
||||
|
||||
- Azure CLI installed and configured
|
||||
- Terraform installed
|
||||
- Active Azure subscription
|
||||
- Access to competition Tailscale tailnet
|
||||
|
||||
##### Azure Setup
|
||||
|
||||
1. **Login to Azure:**
|
||||
|
||||
```bash
|
||||
az login --tenant <your-tenant-here>
|
||||
```
|
||||
|
||||
2. **Create Service Principal:**
|
||||
|
||||
```bash
|
||||
# Get your subscription ID
|
||||
az account show --query "{SubscriptionID:id}" --output table
|
||||
|
||||
# Create service principal (replace with your subscription ID)
|
||||
az ad sp create-for-rbac --name "ButtercupCRS" --role Contributor --scopes /subscriptions/<YOUR-SUBSCRIPTION-ID>
|
||||
```
|
||||
|
||||
##### Production Configuration
|
||||
|
||||
1. **Configure environment file:**
|
||||
|
||||
```bash
|
||||
cp deployment/env.template deployment/env
|
||||
```
|
||||
|
||||
2. **Update `deployment/env` for production:**
|
||||
|
||||
Look at the comments in the `deployment/env.template` for how to set variables.
|
||||
In particular, set `TF_VAR_*` variables, and `TAILSCALE_*` if used.
|
||||
|
||||
### Deploy to AKS
|
||||
|
||||
**Deploy the cluster and services:**
|
||||
|
||||
```bash
|
||||
make deploy-azure
|
||||
```
|
||||
|
||||
**Alternative manual command:**
|
||||
|
||||
```bash
|
||||
cd deployment && make up
|
||||
```
|
||||
|
||||
### Scaling and Management
|
||||
|
||||
- **Scale nodes:** Update `TF_VAR_usr_node_count` in your env file and run `make up`
|
||||
- **View logs:** `kubectl logs -n crs <pod-name>`
|
||||
- **Monitor resources:** `kubectl top pods -A`
|
||||
|
||||
## Run Challenges
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n crs service/buttercup-ui 31323:1323 &
|
||||
|
||||
./orchestrator/scripts/challenge.py
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
**Note:** This is an important step to ensure Buttercup shuts down and stops consuming third-party AI resources.
|
||||
|
||||
```bash
|
||||
make undeploy
|
||||
```
|
||||
|
||||
**Alternative manual command:**
|
||||
## Accessing Logs
|
||||
|
||||
Buttercup includes local SigNoz deployment by default for comprehensive system observability. You can access logs, traces, and metrics through the SigNoz UI:
|
||||
|
||||
```bash
|
||||
cd deployment && make down
|
||||
make signoz-ui
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
Then navigate to `http://localhost:33301` in your web browser to view:
|
||||
|
||||
### Using Makefile Shortcuts
|
||||
- Distributed traces
|
||||
- Application metrics
|
||||
- Error monitoring
|
||||
- Performance insights
|
||||
|
||||
The **Buttercup CRS** project includes a Makefile with convenient shortcuts for common tasks:
|
||||
If you configured LangFuse during setup, you can also monitor LLM usage and costs there.
|
||||
|
||||
```bash
|
||||
# View all available commands
|
||||
make help
|
||||
For additional log access methods, see the [Quick Reference Guide](guides/QUICK_REFERENCE.md).
|
||||
|
||||
# Setup
|
||||
make setup-local # Automated local development setup
|
||||
make setup-azure # Automated production AKS setup
|
||||
make validate # Validate current setup and configuration
|
||||
## Additional Resources
|
||||
|
||||
# Deployment
|
||||
make deploy # Deploy to current environment (local or azure)
|
||||
make deploy-local # Deploy to local Minikube environment
|
||||
make deploy-azure # Deploy to production AKS environment
|
||||
|
||||
# Status
|
||||
make status # Check the status of the deployment
|
||||
|
||||
# Testing
|
||||
make send-integration-task # Run integration test task
|
||||
make send-libpng-task # Run libpng test task
|
||||
|
||||
# Development
|
||||
make lint # Lint all Python code
|
||||
make lint-component COMPONENT=orchestrator # Lint specific component
|
||||
|
||||
# Cleanup
|
||||
make undeploy # Remove deployment and clean up resources
|
||||
make clean-local # Delete Minikube cluster and remove local config
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Lint all Python code
|
||||
make lint
|
||||
|
||||
# Lint specific component
|
||||
make lint-component COMPONENT=orchestrator
|
||||
```
|
||||
|
||||
**Alternative manual commands:**
|
||||
|
||||
```bash
|
||||
# Lint Python code
|
||||
make lint
|
||||
|
||||
# Run specific component tests
|
||||
make lint-component COMPONENT=orchestrator
|
||||
|
||||
# Test manually
|
||||
./orchestrator/scripts/task_upstream_libpng.sh
|
||||
./orchestrator/scripts/challenge.py
|
||||
```
|
||||
|
||||
|
||||
### Kubernetes Development
|
||||
|
||||
```bash
|
||||
# Port forward for local access
|
||||
kubectl port-forward -n crs service/buttercup-competition-api 31323:1323
|
||||
|
||||
# View logs
|
||||
kubectl logs -n crs -l app=scheduler --tail=-1 --prefix
|
||||
|
||||
# Debug pods
|
||||
kubectl exec -it -n crs <pod-name> -- /bin/bash
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Minikube won't start:**
|
||||
|
||||
```bash
|
||||
minikube delete
|
||||
```
|
||||
|
||||
2. **Docker permission issues:**
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
# Log out and back in
|
||||
```
|
||||
|
||||
3. **Helm chart issues:**
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm dependency update deployment/k8s/
|
||||
```
|
||||
|
||||
4. **Azure authentication:**
|
||||
|
||||
```bash
|
||||
az login --tenant <your-tenant>
|
||||
az account set --subscription <your-subscription-id>
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Validate your setup:** `make validate` - Check if your environment is ready
|
||||
- Check the [Quick Reference Guide](QUICK_REFERENCE.md) for common commands and troubleshooting
|
||||
- Check the [deployment README](deployment/README.md) for detailed deployment information
|
||||
- Check logs: `kubectl logs -n crs <pod-name>`
|
||||
|
||||
## Architecture
|
||||
|
||||
The **Buttercup CRS** system consists of several components designed to work together for automated vulnerability detection and patching:
|
||||
|
||||
- **Orchestrator**: Coordinates the overall repair process and manages the workflow
|
||||
- **Fuzzer**: Discovers vulnerabilities through intelligent fuzzing techniques
|
||||
- **Patcher**: Generates and applies security patches to fix vulnerabilities
|
||||
- **Program Model**: Analyzes code structure and semantics for better understanding
|
||||
- **Seed Generator**: Creates targeted test cases for vulnerability discovery
|
||||
- [Quick Reference Guide](guides/QUICK_REFERENCE.md) - Common commands and troubleshooting
|
||||
- [Manual Setup Guide](guides/MANUAL_SETUP.md) - Detailed manual installation steps
|
||||
- [AKS Deployment Guide](guides/AKS_DEPLOYMENT.md) - Production deployment on Azure
|
||||
- [Contributing Guidelines](CONTRIBUTING.md) - Development workflow and standards
|
||||
- [Deployment Documentation](deployment/README.md) - Advanced deployment configuration
|
||||
- [Writing Custom Challenges](guides/CUSTOM_CHALLENGES.md) - Custom project configuration and setup
|
||||
- [Unscored rounds](guides/UNSCORED.md) - Running unscored round challenges
|
||||
- [Scored round](guides/SCORED.md) - Parsing post-final round results
|
||||
|
||||
+75
-41
@@ -1,24 +1,32 @@
|
||||
[project]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [{ name = "Trail of Bits", email = "opensource@trailofbits" }]
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "Shared utilities and components for Buttercup CRS"
|
||||
authors = [{ name = "Trail of Bits", email = "opensource@trailofbits.com" }]
|
||||
license = "AGPL-3.0-only"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
"protobuf (>=3.20, <=3.20.3)",
|
||||
"pydantic-settings>=2.7.1",
|
||||
"pymongo>=4.10.1",
|
||||
"redis (>=5.2.1,<6.0.0)",
|
||||
"langchain-core~=0.3.32",
|
||||
"langchain-openai~=0.3.2",
|
||||
"langfuse ~= 2.59.2",
|
||||
"langchain ~= 0.3.18",
|
||||
"six>=1.17.0",
|
||||
"openlit == 1.32.12", # 1.33 currently fails to import opentelemetry.sdk._events
|
||||
"langchain-community ~= 0.3.20", # implicit dep of openlit if you instrument langchain
|
||||
"langchain-text-splitters ~= 0.3.7", # implicit dep of openlit if you instrument langchain
|
||||
"pydantic-settings ~=2.10.1",
|
||||
"pymongo ~=4.10.1",
|
||||
"redis ~=5.2.1",
|
||||
"langchain-core~=1.3.3",
|
||||
"langchain-openai ~=1.1.0",
|
||||
"langchain ~=1.2.0",
|
||||
"six ~=1.17.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
"protobuf>=5.0",
|
||||
"openlit ~=1.41.0",
|
||||
"langfuse ~=4.0.1",
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/trailofbits/buttercup"
|
||||
Issues = "https://github.com/trailofbits/buttercup/issues"
|
||||
|
||||
[project.scripts]
|
||||
buttercup-util = "buttercup.common.util_cli:main"
|
||||
buttercup-challenge-task = "buttercup.common.challenge_task_cli:main"
|
||||
@@ -34,17 +42,24 @@ build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.3.4",
|
||||
"ruff>=0.9.2",
|
||||
"mypy>=1.15.0",
|
||||
"types-requests",
|
||||
# Testing tools
|
||||
"pytest~=9.0.3",
|
||||
"pytest-cov ~=6.0.0",
|
||||
"dirty-equals ~=0.9.0",
|
||||
# Linting and type checking
|
||||
"ruff ~=0.14.0",
|
||||
"ty", # Astral type checker (replaces mypy)
|
||||
# Type stubs
|
||||
"types-requests ~=2.32.0",
|
||||
"types-PyYAML",
|
||||
"types-redis",
|
||||
"dirty-equals>=0.9.0",
|
||||
"types-redis ~=4.6.0",
|
||||
# Protobuf code generation
|
||||
"grpcio-tools ~=1.73.1",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
exclude = [
|
||||
"./src/buttercup/common/datastructures",
|
||||
"./src/buttercup/common/clusterfuzz_env",
|
||||
@@ -52,28 +67,47 @@ exclude = [
|
||||
"src/buttercup/common/clusterfuzz_utils.py",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ["pydantic.mypy"]
|
||||
mypy_path = "src"
|
||||
packages = "buttercup"
|
||||
allow_redefinition = true
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
no_implicit_optional = true
|
||||
show_error_codes = true
|
||||
sqlite_cache = true
|
||||
strict_equality = true
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "W", "UP"]
|
||||
ignore = [
|
||||
"COM812", "ISC001", # Formatter conflicts
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**/*.py" = ["S101", "D"] # Allow asserts, no docstrings in tests
|
||||
"*/__cli__.py" = ["T201"] # Allow print in CLI modules
|
||||
"*/util_cli.py" = ["T201"]
|
||||
"*/challenge_task_cli.py" = ["T201"]
|
||||
"*/task_registry.py" = ["T201"]
|
||||
"src/buttercup/common/node_local.py" = ["UP040"] # PEP 695 type aliases break runtime usage as constructors
|
||||
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/buttercup"]
|
||||
omit = ["*/tests/*", "*/__cli__.py", "*/_cli.py", "*/util_cli.py", "*/challenge_task_cli.py", "*/task_registry.py"]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"if __name__ == .__main__.:",
|
||||
"raise NotImplementedError",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
[tool.ty.environment]
|
||||
python-version = "3.12"
|
||||
|
||||
[tool.ty.src]
|
||||
# Explicitly include src directory
|
||||
include = ["src"]
|
||||
exclude = [
|
||||
"src/buttercup/common/clusterfuzz_parser",
|
||||
"src/buttercup/common/clusterfuzz_env",
|
||||
"src/buttercup/common/clusterfuzz_utils",
|
||||
"src/buttercup/common/clusterfuzz_utils.py",
|
||||
"src/buttercup/common/datastructures",
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
unresolved-import = "error"
|
||||
unresolved-attribute = "error"
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Callable, TypeVar, cast
|
||||
from os import PathLike
|
||||
from functools import wraps, cached_property
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import shlex
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
import tempfile
|
||||
import shutil
|
||||
import uuid
|
||||
import subprocess
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
from collections.abc import Callable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property, wraps
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar, cast
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
from buttercup.common import node_local
|
||||
from buttercup.common.constants import ARCHITECTURE
|
||||
from buttercup.common.stack_parsing import get_crash_token
|
||||
from buttercup.common.task_meta import TaskMeta
|
||||
from buttercup.common.utils import copyanything, get_diffs
|
||||
from buttercup.common.stack_parsing import get_crash_token
|
||||
from typing import Iterator
|
||||
import buttercup.common.node_local as node_local
|
||||
from buttercup.common.constants import ARCHITECTURE
|
||||
from packaging.version import Version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_tmp_dir(
|
||||
challenge: ChallengeTask, work_dir: Path | None, delete: bool = True, prefix: str | None = None
|
||||
challenge: ChallengeTask,
|
||||
work_dir: Path | None,
|
||||
delete: bool = True,
|
||||
prefix: str | None = None,
|
||||
) -> Iterator[Path]:
|
||||
"""Create a temporary directory inside a working dir and either keep or
|
||||
delete it after use."""
|
||||
delete it after use.
|
||||
"""
|
||||
if work_dir:
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -60,8 +67,6 @@ def create_tmp_dir(
|
||||
class ChallengeTaskError(Exception):
|
||||
"""Base class for Challenge Task errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
FAILURE_ERR_RESULT = 201
|
||||
TIMEOUT_ERR_RESULT = 124
|
||||
@@ -104,7 +109,7 @@ class ReproduceResult:
|
||||
"""Determine if the fuzzer at least ran"""
|
||||
return bool(
|
||||
(self.command_result.output and b"INFO: Seed: " in self.command_result.output)
|
||||
or (self.command_result.error and b"INFO: Seed: " in self.command_result.error)
|
||||
or (self.command_result.error and b"INFO: Seed: " in self.command_result.error),
|
||||
)
|
||||
|
||||
# This is intended to encapsulate heuristics for determining if a run caused a crash
|
||||
@@ -189,7 +194,8 @@ class ChallengeTask:
|
||||
def _local_ro_dir(self, path: PathLike[str] | str) -> Path:
|
||||
"""Return the local path to the read-only task directory.
|
||||
|
||||
If the path doesn't exist, it will be downloaded from the remote storage"""
|
||||
If the path doesn't exist, it will be downloaded from the remote storage
|
||||
"""
|
||||
lp = Path(path)
|
||||
if not lp.exists():
|
||||
try:
|
||||
@@ -226,12 +232,15 @@ class ChallengeTask:
|
||||
return self._find_first_dir(self.OSS_FUZZ_DIR)
|
||||
|
||||
def _task_dir_compose_path(
|
||||
self, subpath_method: Callable[[], Path | None], raise_on_none: bool = False
|
||||
self,
|
||||
subpath_method: Callable[[], Path | None],
|
||||
raise_on_none: bool = False,
|
||||
) -> Path | None:
|
||||
subpath = subpath_method()
|
||||
if subpath is None:
|
||||
if raise_on_none:
|
||||
raise ChallengeTaskError(f"Path not found: {subpath_method.__name__}")
|
||||
method_name = getattr(subpath_method, "__name__", repr(subpath_method))
|
||||
raise ChallengeTaskError(f"Path not found: {method_name}")
|
||||
return None
|
||||
return self.task_dir / subpath
|
||||
|
||||
@@ -260,10 +269,11 @@ class ChallengeTask:
|
||||
except Exception as e:
|
||||
raise ChallengeTaskError(f"Python executable couldn't be run: {self.python_path}") from e
|
||||
|
||||
def _workdir_from_lines(self, lines: list[str], default: Path = Path("/src")) -> Path:
|
||||
@staticmethod
|
||||
def _workdir_from_lines(lines: list[str], default: Path = Path("/src")) -> Path:
|
||||
"""Gets the WORKDIR from the given lines."""
|
||||
for line in reversed(lines): # reversed to get last WORKDIR.
|
||||
match = re.match(self.WORKDIR_REGEX, line)
|
||||
match = re.match(ChallengeTask.WORKDIR_REGEX, line)
|
||||
if match:
|
||||
workdir = match.group(1)
|
||||
workdir = workdir.replace("$SRC", "/src")
|
||||
@@ -322,7 +332,7 @@ class ChallengeTask:
|
||||
raise ChallengeTaskError("Challenge Task is read-only, cannot perform this operation")
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return cast(F, wrapper)
|
||||
return cast("F", wrapper)
|
||||
|
||||
def _add_optional_arg(self, cmd: list[str], flag: str, arg: Any | None) -> None:
|
||||
if arg is not None:
|
||||
@@ -364,49 +374,52 @@ class ChallengeTask:
|
||||
return current_line
|
||||
|
||||
def _run_cmd(
|
||||
self, cmd: list[str], cwd: Path | None = None, log: bool = True, env_helper: Dict[str, str] | None = None
|
||||
self,
|
||||
cmd: list[str],
|
||||
cwd: Path | None = None,
|
||||
log: bool = True,
|
||||
env_helper: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
try:
|
||||
if env_helper:
|
||||
logger.debug("Env helper: %s", env_helper)
|
||||
env_helper = {**os.environ, **env_helper}
|
||||
logger.debug(f"Running command (cwd={cwd}): {' '.join(cmd)}")
|
||||
process = subprocess.Popen( # noqa: S603
|
||||
with subprocess.Popen( # noqa: S603
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=cwd,
|
||||
env=env_helper,
|
||||
)
|
||||
) as process:
|
||||
# Poll process for new output until finished
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
current_output_line = b""
|
||||
current_error_line = b""
|
||||
while True:
|
||||
stdout_line = process.stdout.readline() if process.stdout else b""
|
||||
stderr_line = process.stderr.readline() if process.stderr else b""
|
||||
if stdout_line:
|
||||
current_output_line = self._log_output_line(current_output_line, stdout_line, log)
|
||||
stdout += stdout_line
|
||||
|
||||
# Poll process for new output until finished
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
current_output_line = b""
|
||||
current_error_line = b""
|
||||
while True:
|
||||
stdout_line = process.stdout.readline() if process.stdout else b""
|
||||
stderr_line = process.stderr.readline() if process.stderr else b""
|
||||
if stdout_line:
|
||||
current_output_line = self._log_output_line(current_output_line, stdout_line, log)
|
||||
stdout += stdout_line
|
||||
if stderr_line:
|
||||
current_error_line = self._log_output_line(current_error_line, stderr_line, log)
|
||||
stderr += stderr_line
|
||||
|
||||
if stderr_line:
|
||||
current_error_line = self._log_output_line(current_error_line, stderr_line, log)
|
||||
stderr += stderr_line
|
||||
# Break if process has finished and we've read all output
|
||||
if not stdout_line and not stderr_line and process.poll() is not None:
|
||||
break
|
||||
|
||||
# Break if process has finished and we've read all output
|
||||
if not stdout_line and not stderr_line and process.poll() is not None:
|
||||
break
|
||||
returncode = process.wait()
|
||||
|
||||
returncode = process.wait()
|
||||
|
||||
return CommandResult(
|
||||
success=returncode == 0,
|
||||
returncode=returncode,
|
||||
error=stderr,
|
||||
output=stdout,
|
||||
)
|
||||
return CommandResult(
|
||||
success=returncode == 0,
|
||||
returncode=returncode,
|
||||
error=stderr,
|
||||
output=stdout,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed (cwd={cwd}): {' '.join(cmd)}")
|
||||
return CommandResult(
|
||||
@@ -419,7 +432,7 @@ class ChallengeTask:
|
||||
logger.exception(f"Command failed (cwd={cwd}): {' '.join(cmd)}")
|
||||
return CommandResult(success=False, returncode=None, error=str(e).encode(), output=None)
|
||||
|
||||
def _run_helper_cmd(self, cmd: list[str], env_helper: Dict[str, str] | None = None) -> CommandResult:
|
||||
def _run_helper_cmd(self, cmd: list[str], env_helper: dict[str, str] | None = None) -> CommandResult:
|
||||
oss_fuzz_subpath = self.get_oss_fuzz_subpath()
|
||||
if oss_fuzz_subpath is None:
|
||||
raise ChallengeTaskError("OSS-Fuzz path not found")
|
||||
@@ -432,7 +445,7 @@ class ChallengeTask:
|
||||
try:
|
||||
result = self._run_helper_cmd(grep_cmd)
|
||||
except Exception as e:
|
||||
logger.exception(f"[task {self.task_dir}] Error grep'ing for base-runner version: {str(e)}")
|
||||
logger.exception(f"[task {self.task_dir}] Error grep'ing for base-runner version: {e!s}")
|
||||
return None
|
||||
if not result.success:
|
||||
return None
|
||||
@@ -448,11 +461,15 @@ class ChallengeTask:
|
||||
base_runner_str = m.group(1).strip(":v")
|
||||
return Version(base_runner_str)
|
||||
except Exception as e:
|
||||
logger.exception(f"[task {self.task_dir}] Error parsing base-runner version: {str(e)}")
|
||||
logger.exception(f"[task {self.task_dir}] Error parsing base-runner version: {e!s}")
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def oss_fuzz_container_org(self) -> str:
|
||||
# Check environment variable first
|
||||
if env_org := os.environ.get("OSS_FUZZ_CONTAINER_ORG"):
|
||||
return env_org
|
||||
|
||||
# Read the helper_path file and grep for the BASE_RUNNER_IMAGE line.
|
||||
result = "gcr.io/oss-fuzz"
|
||||
try:
|
||||
@@ -465,7 +482,7 @@ class ChallengeTask:
|
||||
if image.startswith("gcr.io/oss-fuzz"):
|
||||
logger.info(f"Using oss-fuzz container org: {result}")
|
||||
break
|
||||
elif image.startswith("ghcr.io/aixcc-finals"):
|
||||
if image.startswith("ghcr.io/aixcc-finals"):
|
||||
result = "aixcc-afc"
|
||||
logger.info(f"Using aixcc-afc container org: {result}")
|
||||
break
|
||||
@@ -478,8 +495,7 @@ class ChallengeTask:
|
||||
return f"{self.oss_fuzz_container_org}/{self.project_name}"
|
||||
|
||||
def container_src_dir(self) -> str:
|
||||
"""
|
||||
Name of the src directory in the container (e.g. /src/FreeRDP -> FreeRDP).
|
||||
"""Name of the src directory in the container (e.g. /src/FreeRDP -> FreeRDP).
|
||||
This assumes that the src directory is the same as the workdir.
|
||||
"""
|
||||
return self.workdir_from_dockerfile().parts[-1]
|
||||
@@ -493,7 +509,8 @@ class ChallengeTask:
|
||||
always_build_image: bool = False,
|
||||
) -> CommandResult:
|
||||
"""Execute a command inside a docker container. If not specified, the
|
||||
docker container is the oss-fuzz one."""
|
||||
docker container is the oss-fuzz one.
|
||||
"""
|
||||
return self.exec_docker_cmd_rw(cmd, mount_dirs, container_image, always_build_image=always_build_image)
|
||||
|
||||
def exec_docker_cmd_rw(
|
||||
@@ -564,8 +581,8 @@ class ChallengeTask:
|
||||
architecture: str | None = ARCHITECTURE,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
env_helper: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
env_helper: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Building fuzzers for project %s | architecture=%s | engine=%s | sanitizer=%s | env=%s | use_source_dir=%s",
|
||||
@@ -610,8 +627,8 @@ class ChallengeTask:
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
pull_latest_base_image: bool = True,
|
||||
env: Dict[str, str] | None = None,
|
||||
env_helper: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
env_helper: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
check_build_res = self.check_build(architecture=architecture, engine=engine, sanitizer=sanitizer, env=env)
|
||||
if check_build_res.success:
|
||||
@@ -639,7 +656,7 @@ class ChallengeTask:
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
pull_latest_base_image: bool = True,
|
||||
env: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
env_helper = {
|
||||
"OSS_FUZZ_SAVE_CONTAINERS_NAME": container_name,
|
||||
@@ -662,7 +679,7 @@ class ChallengeTask:
|
||||
architecture: str | None = ARCHITECTURE,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Checking build for project %s | architecture=%s | engine=%s | sanitizer=%s | env=%s",
|
||||
@@ -691,10 +708,11 @@ class ChallengeTask:
|
||||
fuzzer_args: list[str] | None = None,
|
||||
*,
|
||||
architecture: str | None = ARCHITECTURE,
|
||||
env: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> ReproduceResult:
|
||||
logger.info(
|
||||
"Reproducing POV for project %s | fuzzer_name=%s | crash_path=%s | fuzzer_args=%s | architecture=%s | env=%s",
|
||||
"Reproducing POV for project %s | fuzzer_name=%s | crash_path=%s | "
|
||||
"fuzzer_args=%s | architecture=%s | env=%s",
|
||||
self.project_name,
|
||||
fuzzer_name,
|
||||
crash_path,
|
||||
@@ -739,10 +757,11 @@ class ChallengeTask:
|
||||
architecture: str | None = ARCHITECTURE,
|
||||
engine: str | None = None,
|
||||
sanitizer: str | None = None,
|
||||
env: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Running fuzzer for project %s | harness_name=%s | fuzzer_args=%s | corpus_dir=%s | architecture=%s | engine=%s | sanitizer=%s | env=%s",
|
||||
"Running fuzzer for project %s | harness_name=%s | fuzzer_args=%s | "
|
||||
"corpus_dir=%s | architecture=%s | engine=%s | sanitizer=%s | env=%s",
|
||||
self.project_name,
|
||||
harness_name,
|
||||
fuzzer_args,
|
||||
@@ -774,7 +793,7 @@ class ChallengeTask:
|
||||
harness_name: str,
|
||||
corpus_dir: str,
|
||||
architecture: str | None = ARCHITECTURE,
|
||||
env: Dict[str, str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> CommandResult:
|
||||
logger.info(
|
||||
"Running coverage for project %s | harness_name=%s | corpus_dir=%s | architecture=%s | env=%s",
|
||||
@@ -835,17 +854,129 @@ class ChallengeTask:
|
||||
|
||||
return True
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"[task {self.task_dir}] File not found: {str(e)}")
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] File not found: {str(e)}") from e
|
||||
logger.error(f"[task {self.task_dir}] File not found: {e!s}")
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] File not found: {e!s}") from e
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"[task {self.task_dir}] Error applying diff: {str(e)}")
|
||||
logger.error(f"[task {self.task_dir}] Error applying diff: {e!s}")
|
||||
logger.debug(f"[task {self.task_dir}] Error returncode: {e.returncode}")
|
||||
logger.debug(f"[task {self.task_dir}] Error stdout: {e.stdout}")
|
||||
logger.debug(f"[task {self.task_dir}] Error stderr: {e.stderr}")
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] Error applying diff: {str(e)}") from e
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] Error applying diff: {e!s}") from e
|
||||
except Exception as e:
|
||||
logger.exception(f"[task {self.task_dir}] Error applying diff: {str(e)}")
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] Error applying diff: {str(e)}") from e
|
||||
logger.exception(f"[task {self.task_dir}] Error applying diff: {e!s}")
|
||||
raise ChallengeTaskError(f"[task {self.task_dir}] Error applying diff: {e!s}") from e
|
||||
|
||||
def _hack_oss_fuzz_aarch64_dockerfile(self, task: ChallengeTask) -> None:
|
||||
# We find the oss-fuzz/projects/<project>/Dockerfile and make sure the
|
||||
# base image has `:manifest-arm64v8` tag
|
||||
dockerfile_path = task.get_oss_fuzz_path() / "projects" / task.project_name / "Dockerfile"
|
||||
if not dockerfile_path.exists():
|
||||
return
|
||||
|
||||
dockerfile_content = dockerfile_path.read_text()
|
||||
|
||||
# Regex to match FROM gcr.io/oss-fuzz-base/base-builder* [optional tag] [optional as builder]
|
||||
# Only patch base-builder variants, not base-clang or others that may not have manifest-arm64v8 tag
|
||||
def _replace_from(match: re.Match) -> str:
|
||||
image = match.group(1)
|
||||
as_clause = match.group(2) or ""
|
||||
# Always ensure tag is :manifest-arm64v8 regardless if there was a tag before
|
||||
return f"FROM {image}:manifest-arm64v8{as_clause}"
|
||||
|
||||
# This regex matches FROM lines with base-builder images only
|
||||
pattern = r"^FROM\s+(gcr\.io/oss-fuzz-base/base-builder(?:[^\s:]*)?)(?::[^\s]+)?(\s+as\s+\w+)?\s*$"
|
||||
new_content = re.sub(pattern, _replace_from, dockerfile_content, flags=re.MULTILINE)
|
||||
|
||||
if new_content != dockerfile_content:
|
||||
dockerfile_path.write_text(new_content)
|
||||
logger.info("Patched oss-fuzz %s/Dockerfile to use the :manifest-arm64v8 tag", task.project_name)
|
||||
|
||||
def _hack_oss_fuzz_runner(self, task: ChallengeTask, architecture: str) -> None:
|
||||
"""Patch OSS-Fuzz helper.py with architecture-specific and common fixes.
|
||||
|
||||
Common patches (all architectures):
|
||||
- reproduce_impl: Pass architecture as keyword arg instead of err_result
|
||||
- Debug mode: Insert -debug before tag, not after
|
||||
- Tag handling: Check for existing tag before appending
|
||||
|
||||
ARM64-specific patches:
|
||||
- Add :manifest-arm64v8 tags to image_name and BASE_RUNNER_IMAGE
|
||||
"""
|
||||
helper_path = task.get_oss_fuzz_path() / "infra" / "helper.py"
|
||||
if not helper_path.exists():
|
||||
return
|
||||
|
||||
content = helper_path.read_text()
|
||||
replaced = False
|
||||
|
||||
# Common patches for all architectures
|
||||
|
||||
def _replace_debug_append(match: re.Match) -> str:
|
||||
nonlocal replaced
|
||||
replaced = True
|
||||
indent = match.group(1)
|
||||
return f"{indent}image = image.replace(':', '-debug:', 1) if ':' in image else image + '-debug'"
|
||||
|
||||
pattern_debug_append = r"^(\s+)image\s*\+=\s*['\"]-debug['\"]\s*$"
|
||||
content = re.sub(pattern_debug_append, _replace_debug_append, content, flags=re.MULTILINE)
|
||||
|
||||
def _replace_get_base_runner_return(match: re.Match) -> str:
|
||||
nonlocal replaced
|
||||
replaced = True
|
||||
indent = match.group(1)
|
||||
return f"{indent}return image if ':' in image else f'{{image}}:{{tag}}'"
|
||||
|
||||
pattern_get_base_runner = r"^(\s+)return f(['\"])\{image\}:\{tag\}\2\s*$"
|
||||
content = re.sub(pattern_get_base_runner, _replace_get_base_runner_return, content, flags=re.MULTILINE)
|
||||
|
||||
def _replace_reproduce_run_function(match: re.Match) -> str:
|
||||
nonlocal replaced
|
||||
replaced = True
|
||||
indent = match.group(1)
|
||||
return f"{indent}return run_function(run_args, architecture=architecture)"
|
||||
|
||||
pattern_reproduce_run = r"^(\s+)return run_function\(run_args,\s*err_result\)\s*$"
|
||||
content = re.sub(pattern_reproduce_run, _replace_reproduce_run_function, content, flags=re.MULTILINE)
|
||||
|
||||
# ARM64-specific patches
|
||||
if architecture == "aarch64":
|
||||
|
||||
def _replace_image_name(match: re.Match) -> str:
|
||||
nonlocal replaced
|
||||
replaced = True
|
||||
original = match.group(0)
|
||||
if "base-runner-debug" in original:
|
||||
return f"{match.group(1)}image_name = 'base-runner-debug:manifest-arm64v8'"
|
||||
else:
|
||||
return f"{match.group(1)}image_name = 'base-runner:manifest-arm64v8'"
|
||||
|
||||
pattern_img = r"(\s*)image_name\s*=\s*['\"]base-runner(?:-debug)?['\"]"
|
||||
content = re.sub(pattern_img, _replace_image_name, content, flags=re.MULTILINE)
|
||||
|
||||
def _replace_base_runner_image(match: re.Match) -> str:
|
||||
nonlocal replaced
|
||||
replaced = True
|
||||
prefix = match.group(1)
|
||||
image = match.group(2)
|
||||
suffix = match.group(3) or ""
|
||||
if ":manifest-arm64v8" not in image:
|
||||
if ":" in image:
|
||||
image = image.rsplit(":", 1)[0]
|
||||
image = image + ":manifest-arm64v8"
|
||||
return f"{prefix}BASE_RUNNER_IMAGE = '{image}'{suffix}"
|
||||
|
||||
pattern_base_img = (
|
||||
r"(^\s*)BASE_RUNNER_IMAGE\s*=\s*['\"](gcr\.io/oss-fuzz-base/base-runner(?:[^\s'\"]*)?)['\"](\s*)"
|
||||
)
|
||||
content = re.sub(pattern_base_img, _replace_base_runner_image, content, flags=re.MULTILINE)
|
||||
|
||||
if replaced:
|
||||
helper_path.write_text(content)
|
||||
logger.info("Patched oss-fuzz helper.py for %s", architecture)
|
||||
|
||||
def _hack_oss_fuzz_aarch64(self, task: ChallengeTask) -> None:
|
||||
self._hack_oss_fuzz_aarch64_dockerfile(task)
|
||||
self._hack_oss_fuzz_runner(task, "aarch64")
|
||||
|
||||
@contextmanager
|
||||
def get_rw_copy(self, work_dir: PathLike | None, delete: bool = True) -> Iterator[ChallengeTask]:
|
||||
@@ -855,6 +986,7 @@ class ChallengeTask:
|
||||
Example:
|
||||
with task.get_rw_copy(work_dir) as local_task:
|
||||
local_task.build_fuzzers()
|
||||
|
||||
"""
|
||||
work_dir = Path(work_dir) if work_dir else Path(node_local.scratch_path())
|
||||
work_dir = work_dir / self.task_meta.task_id
|
||||
@@ -871,6 +1003,13 @@ class ChallengeTask:
|
||||
python_path=self.python_path,
|
||||
local_task_dir=tmp_dir,
|
||||
)
|
||||
# HACK: Apply OSS-Fuzz patches
|
||||
# For aarch64: Dockerfile + helper.py with :manifest-arm64v8 tags + common fixes
|
||||
# For other arches: helper.py with common fixes only
|
||||
if ARCHITECTURE == "aarch64":
|
||||
self._hack_oss_fuzz_aarch64(copied_task)
|
||||
else:
|
||||
self._hack_oss_fuzz_runner(copied_task, ARCHITECTURE)
|
||||
|
||||
try:
|
||||
yield copied_task
|
||||
@@ -902,7 +1041,7 @@ class ChallengeTask:
|
||||
raise ChallengeTaskError("Failed to commit task") from e
|
||||
|
||||
logger.error(
|
||||
f"Failed to commit task {self.local_task_dir} to {new_name}. Retrying with a random suffix..."
|
||||
f"Failed to commit task {self.local_task_dir} to {new_name}. Retrying with a random suffix...",
|
||||
)
|
||||
suffix = None
|
||||
|
||||
@@ -911,7 +1050,8 @@ class ChallengeTask:
|
||||
@read_write_decorator
|
||||
def restore(self) -> None:
|
||||
"""Restore the task from the original read-only task directory (if
|
||||
different from the local task directory)."""
|
||||
different from the local task directory).
|
||||
"""
|
||||
if self.read_only_task_dir == self.local_task_dir:
|
||||
raise ChallengeTaskError("Task cannot be restored, it doesn't have a local task directory")
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from pydantic_settings import BaseSettings, CliSubCommand, get_subcommand, CliImplicitFlag
|
||||
from pydantic import BaseModel
|
||||
from buttercup.common.challenge_task import ChallengeTask, CommandResult, ReproduceResult
|
||||
from buttercup.common.logger import setup_package_logger
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic_settings import BaseSettings, CliImplicitFlag, CliSubCommand, SettingsConfigDict, get_subcommand
|
||||
|
||||
from buttercup.common.challenge_task import ChallengeTask, CommandResult, ReproduceResult
|
||||
from buttercup.common.constants import ARCHITECTURE
|
||||
from buttercup.common.logger import setup_package_logger
|
||||
|
||||
|
||||
class BuildImageCommand(BaseModel):
|
||||
pull_latest_base_image: bool = False
|
||||
cache: bool | None = None
|
||||
architecture: str | None = None
|
||||
architecture: str = ARCHITECTURE
|
||||
|
||||
|
||||
class ApplyPatchCommand(BaseModel):
|
||||
@@ -19,29 +21,38 @@ class ApplyPatchCommand(BaseModel):
|
||||
|
||||
|
||||
class BuildFuzzersCommand(BaseModel):
|
||||
architecture: str | None = None
|
||||
architecture: str = ARCHITECTURE
|
||||
engine: str | None = None
|
||||
sanitizer: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
env: dict[str, str] | None = None
|
||||
use_cache: bool = True
|
||||
|
||||
|
||||
class CheckBuildCommand(BaseModel):
|
||||
architecture: str | None = None
|
||||
architecture: str = ARCHITECTURE
|
||||
engine: str | None = None
|
||||
sanitizer: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
env: dict[str, str] | None = None
|
||||
|
||||
|
||||
class ReproducePovCommand(BaseModel):
|
||||
fuzzer_name: str
|
||||
crash_path: Path
|
||||
fuzzer_args: list[str] | None = None
|
||||
architecture: str | None = None
|
||||
env: Dict[str, str] | None = None
|
||||
architecture: str = ARCHITECTURE
|
||||
env: dict[str, str] | None = None
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BUTTERCUP_CHALLENGE_TASK_",
|
||||
env_file=".env",
|
||||
cli_parse_args=True,
|
||||
nested_model_default_partial_update=True,
|
||||
env_nested_delimiter="__",
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
task_dir: Path
|
||||
python_path: Path = Path("python")
|
||||
rw: CliImplicitFlag[bool] = False
|
||||
@@ -52,14 +63,6 @@ class Settings(BaseSettings):
|
||||
check_build: CliSubCommand[CheckBuildCommand]
|
||||
reproduce_pov: CliSubCommand[ReproducePovCommand]
|
||||
|
||||
class Config:
|
||||
env_prefix = "BUTTERCUP_CHALLENGE_TASK_"
|
||||
env_file = ".env"
|
||||
cli_parse_args = True
|
||||
nested_model_default_partial_update = True
|
||||
env_nested_delimiter = "__"
|
||||
extra = "allow"
|
||||
|
||||
|
||||
def handle_subcommand(task: ChallengeTask, subcommand: BaseModel | None) -> CommandResult | ReproduceResult:
|
||||
if isinstance(subcommand, BuildImageCommand):
|
||||
@@ -68,7 +71,7 @@ def handle_subcommand(task: ChallengeTask, subcommand: BaseModel | None) -> Comm
|
||||
cache=subcommand.cache,
|
||||
architecture=subcommand.architecture,
|
||||
)
|
||||
elif isinstance(subcommand, BuildFuzzersCommand):
|
||||
if isinstance(subcommand, BuildFuzzersCommand):
|
||||
if subcommand.use_cache:
|
||||
return task.build_fuzzers_with_cache(
|
||||
architecture=subcommand.architecture,
|
||||
@@ -76,21 +79,20 @@ def handle_subcommand(task: ChallengeTask, subcommand: BaseModel | None) -> Comm
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
else:
|
||||
return task.build_fuzzers(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
elif isinstance(subcommand, CheckBuildCommand):
|
||||
return task.build_fuzzers(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
if isinstance(subcommand, CheckBuildCommand):
|
||||
return task.check_build(
|
||||
architecture=subcommand.architecture,
|
||||
engine=subcommand.engine,
|
||||
sanitizer=subcommand.sanitizer,
|
||||
env=subcommand.env,
|
||||
)
|
||||
elif isinstance(subcommand, ReproducePovCommand):
|
||||
if isinstance(subcommand, ReproducePovCommand):
|
||||
return task.reproduce_pov(
|
||||
fuzzer_name=subcommand.fuzzer_name,
|
||||
crash_path=subcommand.crash_path,
|
||||
@@ -98,10 +100,9 @@ def handle_subcommand(task: ChallengeTask, subcommand: BaseModel | None) -> Comm
|
||||
architecture=subcommand.architecture,
|
||||
env=subcommand.env,
|
||||
).command_result
|
||||
elif isinstance(subcommand, ApplyPatchCommand):
|
||||
if isinstance(subcommand, ApplyPatchCommand):
|
||||
return CommandResult(success=task.apply_patch_diff(diff_file=subcommand.diff_file))
|
||||
else:
|
||||
raise ValueError(f"Unknown subcommand: {subcommand}")
|
||||
raise ValueError(f"Unknown subcommand: {subcommand}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -114,7 +115,7 @@ def get_task_copy(task: ChallengeTask, use_copy: bool = False) -> Iterator[Chall
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = Settings()
|
||||
settings = Settings() # type: ignore[call-arg]
|
||||
setup_package_logger("challenge-task-cli", __name__, "DEBUG")
|
||||
if settings.rw:
|
||||
task = ChallengeTask(
|
||||
|
||||
@@ -5,10 +5,9 @@ def _detect_architecture() -> str:
|
||||
machine = platform.machine().lower()
|
||||
if machine in ("x86_64", "amd64"):
|
||||
return "x86_64"
|
||||
elif machine in ("aarch64", "arm64"):
|
||||
if machine in ("aarch64", "arm64"):
|
||||
return "aarch64"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported architecture: {machine}")
|
||||
raise RuntimeError(f"Unsupported architecture: {machine}")
|
||||
|
||||
|
||||
CORPUS_DIR_NAME = "buttercup_corpus"
|
||||
|
||||
@@ -1,21 +1,54 @@
|
||||
import logging
|
||||
from typing import BinaryIO, List
|
||||
import buttercup.common.node_local as node_local
|
||||
from buttercup.common.constants import CORPUS_DIR_NAME, CRASH_DIR_NAME
|
||||
import os
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from redis import Redis
|
||||
from buttercup.common.sets import MergedCorpusSet
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common import node_local
|
||||
from buttercup.common.constants import CORPUS_DIR_NAME, CRASH_DIR_NAME
|
||||
from buttercup.common.sets import MergedCorpusSet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: this file is one of the few files that uses os.path.join. Switch to Path.
|
||||
|
||||
|
||||
def _get_corpus_storage_path(wdir: str, corpus_subpath: str) -> tuple[str, Path]:
|
||||
"""Calculate the local storage path and remote path for corpus.
|
||||
|
||||
When tmpfs is enabled, the local path is on tmpfs but the remote path
|
||||
is still calculated relative to NODE_DATA_DIR.
|
||||
|
||||
Args:
|
||||
wdir: The working directory (typically NODE_DATA_DIR)
|
||||
corpus_subpath: The corpus subdirectory (e.g., "{task_id}/buttercup_corpus_{harness}")
|
||||
|
||||
Returns:
|
||||
Tuple of (local_path, remote_path)
|
||||
"""
|
||||
tmpfs_path = node_local.get_corpus_tmpfs_path()
|
||||
|
||||
if tmpfs_path:
|
||||
# Store corpus on tmpfs
|
||||
local_path = os.path.join(str(tmpfs_path), corpus_subpath)
|
||||
# Remote path is still calculated relative to NODE_DATA_DIR structure
|
||||
# We compute it as if the corpus were stored in wdir
|
||||
canonical_path = os.path.join(wdir, corpus_subpath)
|
||||
remote_path = node_local.remote_path(Path(canonical_path))
|
||||
logger.debug(f"Using tmpfs for corpus: local={local_path}, remote={remote_path}")
|
||||
else:
|
||||
# Standard behavior - store corpus in wdir
|
||||
local_path = os.path.join(wdir, corpus_subpath)
|
||||
remote_path = node_local.remote_path(Path(local_path))
|
||||
|
||||
return local_path, remote_path
|
||||
|
||||
|
||||
def hash_file(fl: BinaryIO) -> str:
|
||||
h = hashlib.new("sha256")
|
||||
bts = fl.read(100)
|
||||
@@ -26,27 +59,55 @@ def hash_file(fl: BinaryIO) -> str:
|
||||
|
||||
|
||||
class InputDir:
|
||||
def __init__(self, wdir: str, name: str, copy_corpus_max_size: int | None = None):
|
||||
self.path = os.path.join(wdir, name)
|
||||
self.remote_path = node_local.remote_path(Path(self.path))
|
||||
def __init__(
|
||||
self,
|
||||
wdir: str,
|
||||
name: str,
|
||||
copy_corpus_max_size: int | None = None,
|
||||
*,
|
||||
override_local_path: str | None = None,
|
||||
override_remote_path: Path | None = None,
|
||||
):
|
||||
"""Initialize an InputDir for corpus/crash storage.
|
||||
|
||||
Args:
|
||||
wdir: Working directory (typically NODE_DATA_DIR)
|
||||
name: Subdirectory name within wdir
|
||||
copy_corpus_max_size: Maximum size for copied corpus files
|
||||
override_local_path: If provided, use this as the local path instead of wdir/name
|
||||
override_remote_path: If provided, use this as the remote path
|
||||
"""
|
||||
if override_local_path is not None:
|
||||
self.path = override_local_path
|
||||
else:
|
||||
self.path = os.path.join(wdir, name)
|
||||
|
||||
if override_remote_path is not None:
|
||||
self.remote_path = override_remote_path
|
||||
else:
|
||||
self.remote_path = node_local.remote_path(Path(self.path))
|
||||
|
||||
self.copy_corpus_max_size = copy_corpus_max_size
|
||||
os.makedirs(self.path, exist_ok=True)
|
||||
|
||||
def basename(self) -> str:
|
||||
return os.path.basename(self.path)
|
||||
|
||||
def copy_file(self, src_file: str) -> str:
|
||||
def copy_file(self, src_file: str, only_local: bool = False) -> str:
|
||||
with open(src_file, "rb") as f:
|
||||
nm = hash_file(f)
|
||||
dst = os.path.join(self.path, nm)
|
||||
dst_remote = os.path.join(self.remote_path, nm)
|
||||
os.makedirs(self.remote_path, exist_ok=True)
|
||||
# Make the file available both node-local and remote
|
||||
# Copy to local corpus
|
||||
shutil.copy(src_file, dst)
|
||||
shutil.copy(dst, dst_remote)
|
||||
if not only_local:
|
||||
dst_remote = os.path.join(self.remote_path, nm)
|
||||
os.makedirs(self.remote_path, exist_ok=True)
|
||||
# Copy to remote corpus
|
||||
shutil.copy(dst, dst_remote)
|
||||
return dst
|
||||
|
||||
def copy_corpus(self, src_dir: str) -> list[str]:
|
||||
"""Copy files from src_dir to local corpus only."""
|
||||
files = []
|
||||
for file in os.listdir(src_dir):
|
||||
file_path = os.path.join(src_dir, file)
|
||||
@@ -58,7 +119,7 @@ class InputDir:
|
||||
self.copy_corpus_max_size,
|
||||
)
|
||||
continue
|
||||
files.append(self.copy_file(file_path))
|
||||
files.append(self.copy_file(file_path, only_local=True))
|
||||
return files
|
||||
|
||||
def local_corpus_size(self) -> int:
|
||||
@@ -97,7 +158,7 @@ class InputDir:
|
||||
return len(name) == 64 and all(c in "0123456789abcdef" for c in name)
|
||||
|
||||
@classmethod
|
||||
def hash_corpus(cls, path: str) -> List[str]:
|
||||
def hash_corpus(cls, path: str) -> list[str]:
|
||||
hashed_files = []
|
||||
for file in os.listdir(path):
|
||||
# If the file is already a hash, skip it
|
||||
@@ -105,9 +166,12 @@ class InputDir:
|
||||
continue
|
||||
file_path = os.path.join(path, file)
|
||||
try:
|
||||
assert os.path.isfile(file_path)
|
||||
with open(file_path, "rb") as f:
|
||||
hash_filename = hash_file(f)
|
||||
os.rename(file_path, os.path.join(path, hash_filename))
|
||||
dst_path = os.path.join(path, hash_filename)
|
||||
# Use shutil.move which handles cross-filesystem operations
|
||||
shutil.move(file_path, dst_path)
|
||||
hashed_files.append(hash_filename)
|
||||
except Exception as e:
|
||||
# Likely already hashed by another pod
|
||||
@@ -130,7 +194,7 @@ class InputDir:
|
||||
"--exclude=*",
|
||||
str(src_path) + "/",
|
||||
str(dst_path) + "/",
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
def sync_to_remote(self) -> None:
|
||||
@@ -139,11 +203,11 @@ class InputDir:
|
||||
self._do_sync(self.path, self.remote_path)
|
||||
|
||||
def sync_specific_files_to_remote(self, files: list[str]) -> None:
|
||||
"""
|
||||
Sync only specific files to remote storage.
|
||||
"""Sync only specific files to remote storage.
|
||||
|
||||
Args:
|
||||
files: List of filenames (basename only, not full path) to sync to remote
|
||||
|
||||
"""
|
||||
self.hash_new_corpus()
|
||||
os.makedirs(self.remote_path, exist_ok=True)
|
||||
@@ -166,7 +230,7 @@ class InputDir:
|
||||
f"--files-from={file_list.name}",
|
||||
str(self.path) + "/",
|
||||
str(self.remote_path) + "/",
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
def sync_from_remote(self) -> None:
|
||||
@@ -219,7 +283,17 @@ class Corpus(InputDir):
|
||||
self.task_id = task_id
|
||||
self.harness_name = harness_name
|
||||
self.corpus_dir = os.path.join(task_id, f"{CORPUS_DIR_NAME}_{harness_name}")
|
||||
super().__init__(wdir, self.corpus_dir, copy_corpus_max_size=copy_corpus_max_size)
|
||||
|
||||
# Get the storage paths, potentially using tmpfs for the local path
|
||||
local_path, remote_path = _get_corpus_storage_path(wdir, self.corpus_dir)
|
||||
|
||||
super().__init__(
|
||||
wdir,
|
||||
self.corpus_dir,
|
||||
copy_corpus_max_size=copy_corpus_max_size,
|
||||
override_local_path=local_path,
|
||||
override_remote_path=remote_path,
|
||||
)
|
||||
|
||||
def remove_any_merged(self, redis: Redis) -> None:
|
||||
merged_corpus_set = MergedCorpusSet(redis, self.task_id, self.harness_name)
|
||||
|
||||
@@ -8,12 +8,13 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.maps import CoverageMap, FunctionCoverage, HarnessWeights
|
||||
|
||||
# Add matplotlib import for visualization
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.dates as mdates # type: ignore[unresolved-import]
|
||||
import matplotlib.pyplot as plt # type: ignore[unresolved-import]
|
||||
|
||||
MATPLOTLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -22,12 +23,12 @@ except ImportError:
|
||||
|
||||
# Move _print_coverage_metrics to be a free function
|
||||
def print_coverage_metrics(func_coverage_list: list[FunctionCoverage], snapshot_count: int) -> None:
|
||||
"""
|
||||
Print coverage metrics for the given list of function coverage objects.
|
||||
"""Print coverage metrics for the given list of function coverage objects.
|
||||
|
||||
Args:
|
||||
func_coverage_list: List of FunctionCoverage objects
|
||||
snapshot_count: The current snapshot count
|
||||
|
||||
"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
total_functions = len(func_coverage_list)
|
||||
@@ -50,8 +51,7 @@ def print_coverage_metrics(func_coverage_list: list[FunctionCoverage], snapshot_
|
||||
|
||||
|
||||
def coverage_data_equal(old_data: list[dict], new_data: list[dict]) -> bool:
|
||||
"""
|
||||
Compare two coverage data lists to check if they are equal.
|
||||
"""Compare two coverage data lists to check if they are equal.
|
||||
|
||||
Args:
|
||||
old_data: Previous coverage data
|
||||
@@ -59,6 +59,7 @@ def coverage_data_equal(old_data: list[dict], new_data: list[dict]) -> bool:
|
||||
|
||||
Returns:
|
||||
True if coverage data is the same, False otherwise
|
||||
|
||||
"""
|
||||
if len(old_data) != len(new_data):
|
||||
return False
|
||||
@@ -67,7 +68,7 @@ def coverage_data_equal(old_data: list[dict], new_data: list[dict]) -> bool:
|
||||
old_sorted = sorted(old_data, key=lambda x: x["function_name"])
|
||||
new_sorted = sorted(new_data, key=lambda x: x["function_name"])
|
||||
|
||||
for old_item, new_item in zip(old_sorted, new_sorted):
|
||||
for old_item, new_item in zip(old_sorted, new_sorted, strict=False):
|
||||
if (
|
||||
old_item["function_name"] != new_item["function_name"]
|
||||
or old_item["total_lines"] != new_item["total_lines"]
|
||||
@@ -106,14 +107,14 @@ class CoverageMonitor:
|
||||
}
|
||||
|
||||
def monitor_coverage(self, duration_seconds: int | None = None) -> str:
|
||||
"""
|
||||
Monitor function coverage over time and save results to a file for all packages and harnesses.
|
||||
"""Monitor function coverage over time and save results to a file for all packages and harnesses.
|
||||
|
||||
Args:
|
||||
duration_seconds: How long to run the monitor. If None, run indefinitely.
|
||||
|
||||
Returns:
|
||||
Path to the output file.
|
||||
|
||||
"""
|
||||
coverage_snapshots = []
|
||||
start_time = time.time()
|
||||
@@ -243,8 +244,7 @@ class CoverageMonitor:
|
||||
def _extract_metrics(
|
||||
coverage_snapshots: list[dict],
|
||||
) -> tuple[list[datetime], list[int], list[int], list[int], list[float]]:
|
||||
"""
|
||||
Extract metrics from coverage snapshots for analysis and visualization.
|
||||
"""Extract metrics from coverage snapshots for analysis and visualization.
|
||||
|
||||
Returns:
|
||||
Tuple containing:
|
||||
@@ -253,6 +253,7 @@ class CoverageMonitor:
|
||||
- total_lines: List of total line counts for each snapshot
|
||||
- covered_lines: List of covered line counts for each snapshot
|
||||
- coverage_percentages: List of coverage percentages for each snapshot
|
||||
|
||||
"""
|
||||
timestamps = []
|
||||
function_counts = []
|
||||
@@ -286,11 +287,11 @@ class CoverageMonitor:
|
||||
covered_lines: list[int],
|
||||
coverage_percentages: list[float],
|
||||
) -> str | None:
|
||||
"""
|
||||
Create a visualization of coverage metrics over time.
|
||||
"""Create a visualization of coverage metrics over time.
|
||||
|
||||
Returns:
|
||||
Path to the generated image file, or None if visualization failed.
|
||||
|
||||
"""
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
print("Matplotlib is not available. Install with 'pip install matplotlib' to enable visualization.")
|
||||
@@ -355,10 +356,12 @@ class CoverageMonitor:
|
||||
|
||||
@staticmethod
|
||||
def analyze_coverage_file(
|
||||
file_path: str, harness_key: str | None = None, visualize: bool = False, list_only: bool = False
|
||||
file_path: str,
|
||||
harness_key: str | None = None,
|
||||
visualize: bool = False,
|
||||
list_only: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Analyze a previously recorded coverage file and print summary statistics.
|
||||
"""Analyze a previously recorded coverage file and print summary statistics.
|
||||
|
||||
Args:
|
||||
file_path: Path to the coverage data file.
|
||||
@@ -366,9 +369,10 @@ class CoverageMonitor:
|
||||
If None, analyze all harnesses in the file.
|
||||
visualize: Whether to generate a visualization of the data.
|
||||
list_only: If True, only list the available harnesses without analyzing.
|
||||
|
||||
"""
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
with open(file_path) as f:
|
||||
coverage_snapshots = json.load(f)
|
||||
|
||||
if not coverage_snapshots:
|
||||
@@ -382,10 +386,9 @@ class CoverageMonitor:
|
||||
|
||||
# List all available harnesses
|
||||
print(f"Coverage file: {file_path}")
|
||||
print(
|
||||
f"Time range: {datetime.fromtimestamp(coverage_snapshots[0]['timestamp']).strftime('%Y-%m-%d %H:%M:%S')} - "
|
||||
f"{datetime.fromtimestamp(coverage_snapshots[-1]['timestamp']).strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
start_time = datetime.fromtimestamp(coverage_snapshots[0]["timestamp"]).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_time = datetime.fromtimestamp(coverage_snapshots[-1]["timestamp"]).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"Time range: {start_time} - {end_time}")
|
||||
print(f"Total snapshots: {len(coverage_snapshots)}")
|
||||
print("\nAvailable harnesses:")
|
||||
|
||||
@@ -527,7 +530,9 @@ def main() -> None:
|
||||
monitor_parser.add_argument("--task-id", help="Task ID to filter harnesses (optional)")
|
||||
monitor_parser.add_argument("--interval", type=int, default=10, help="Interval between snapshots in seconds")
|
||||
monitor_parser.add_argument(
|
||||
"--duration", type=int, help="Duration to run the monitor in seconds (default: run indefinitely)"
|
||||
"--duration",
|
||||
type=int,
|
||||
help="Duration to run the monitor in seconds (default: run indefinitely)",
|
||||
)
|
||||
|
||||
# Analyze command
|
||||
@@ -535,7 +540,10 @@ def main() -> None:
|
||||
analyze_parser.add_argument("file_path", help="Path to the coverage data file")
|
||||
analyze_parser.add_argument("--harness-key", help="Harness key to analyze in format 'package_name-harness_name'")
|
||||
analyze_parser.add_argument(
|
||||
"--visualize", "-v", action="store_true", help="Generate visualization of coverage metrics"
|
||||
"--visualize",
|
||||
"-v",
|
||||
action="store_true",
|
||||
help="Generate visualization of coverage metrics",
|
||||
)
|
||||
analyze_parser.add_argument("--list", "-l", action="store_true", help="List available harnesses in the file")
|
||||
|
||||
@@ -553,7 +561,10 @@ def main() -> None:
|
||||
|
||||
elif args.command == "analyze":
|
||||
CoverageMonitor.analyze_coverage_file(
|
||||
args.file_path, harness_key=args.harness_key, visualize=args.visualize, list_only=args.list
|
||||
args.file_path,
|
||||
harness_key=args.harness_key,
|
||||
visualize=args.visualize,
|
||||
list_only=args.list,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,301 +2,318 @@ from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
|
||||
ACCEPTED: SubmissionResult
|
||||
COVERAGE: BuildType
|
||||
DEADLINE_EXCEEDED: SubmissionResult
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
ERRORED: SubmissionResult
|
||||
FAILED: SubmissionResult
|
||||
|
||||
class BuildType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
FUZZER: _ClassVar[BuildType]
|
||||
COVERAGE: _ClassVar[BuildType]
|
||||
TRACER_NO_DIFF: _ClassVar[BuildType]
|
||||
PATCH: _ClassVar[BuildType]
|
||||
|
||||
class SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
NONE: _ClassVar[SubmissionResult]
|
||||
ACCEPTED: _ClassVar[SubmissionResult]
|
||||
PASSED: _ClassVar[SubmissionResult]
|
||||
FAILED: _ClassVar[SubmissionResult]
|
||||
DEADLINE_EXCEEDED: _ClassVar[SubmissionResult]
|
||||
ERRORED: _ClassVar[SubmissionResult]
|
||||
INCONCLUSIVE: _ClassVar[SubmissionResult]
|
||||
FUZZER: BuildType
|
||||
INCONCLUSIVE: SubmissionResult
|
||||
NONE: SubmissionResult
|
||||
PASSED: SubmissionResult
|
||||
PATCH: BuildType
|
||||
COVERAGE: BuildType
|
||||
TRACER_NO_DIFF: BuildType
|
||||
PATCH: BuildType
|
||||
NONE: SubmissionResult
|
||||
ACCEPTED: SubmissionResult
|
||||
PASSED: SubmissionResult
|
||||
FAILED: SubmissionResult
|
||||
DEADLINE_EXCEEDED: SubmissionResult
|
||||
ERRORED: SubmissionResult
|
||||
INCONCLUSIVE: SubmissionResult
|
||||
|
||||
class BuildOutput(_message.Message):
|
||||
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "sanitizer", "task_dir", "task_id"]
|
||||
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
ENGINE_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
class Task(_message.Message):
|
||||
__slots__ = ("message_id", "message_time", "task_id", "task_type", "sources", "deadline", "cancelled", "project_name", "focus", "metadata")
|
||||
class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
TASK_TYPE_FULL: _ClassVar[Task.TaskType]
|
||||
TASK_TYPE_DELTA: _ClassVar[Task.TaskType]
|
||||
TASK_TYPE_FULL: Task.TaskType
|
||||
TASK_TYPE_DELTA: Task.TaskType
|
||||
class MetadataEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: str
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
|
||||
MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
MESSAGE_TIME_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
apply_diff: bool
|
||||
build_type: BuildType
|
||||
engine: str
|
||||
internal_patch_id: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
TASK_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCES_FIELD_NUMBER: _ClassVar[int]
|
||||
DEADLINE_FIELD_NUMBER: _ClassVar[int]
|
||||
CANCELLED_FIELD_NUMBER: _ClassVar[int]
|
||||
PROJECT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
FOCUS_FIELD_NUMBER: _ClassVar[int]
|
||||
METADATA_FIELD_NUMBER: _ClassVar[int]
|
||||
message_id: str
|
||||
message_time: int
|
||||
task_id: str
|
||||
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class BuildRequest(_message.Message):
|
||||
__slots__ = ["apply_diff", "build_type", "engine", "internal_patch_id", "patch", "sanitizer", "task_dir", "task_id"]
|
||||
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
ENGINE_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
apply_diff: bool
|
||||
build_type: BuildType
|
||||
engine: str
|
||||
internal_patch_id: str
|
||||
patch: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Bundle(_message.Message):
|
||||
__slots__ = ["bundle_id", "competition_patch_id", "competition_pov_id", "competition_sarif_id", "task_id"]
|
||||
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_SARIF_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
bundle_id: str
|
||||
competition_patch_id: str
|
||||
competition_pov_id: str
|
||||
competition_sarif_id: str
|
||||
task_id: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., competition_pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., competition_sarif_id: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class ConfirmedVulnerability(_message.Message):
|
||||
__slots__ = ["crashes", "internal_patch_id"]
|
||||
CRASHES_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
crashes: _containers.RepeatedCompositeFieldContainer[TracedCrash]
|
||||
internal_patch_id: str
|
||||
def __init__(self, crashes: _Optional[_Iterable[_Union[TracedCrash, _Mapping]]] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Crash(_message.Message):
|
||||
__slots__ = ["crash_input_path", "crash_token", "harness_name", "stacktrace", "target"]
|
||||
CRASH_INPUT_PATH_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASH_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
STACKTRACE_FIELD_NUMBER: _ClassVar[int]
|
||||
TARGET_FIELD_NUMBER: _ClassVar[int]
|
||||
crash_input_path: str
|
||||
crash_token: str
|
||||
harness_name: str
|
||||
stacktrace: str
|
||||
target: BuildOutput
|
||||
def __init__(self, target: _Optional[_Union[BuildOutput, _Mapping]] = ..., harness_name: _Optional[str] = ..., crash_input_path: _Optional[str] = ..., stacktrace: _Optional[str] = ..., crash_token: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class CrashWithId(_message.Message):
|
||||
__slots__ = ["competition_pov_id", "crash", "result"]
|
||||
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASH_FIELD_NUMBER: _ClassVar[int]
|
||||
RESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
competition_pov_id: str
|
||||
crash: TracedCrash
|
||||
result: SubmissionResult
|
||||
def __init__(self, crash: _Optional[_Union[TracedCrash, _Mapping]] = ..., competition_pov_id: _Optional[str] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
|
||||
|
||||
class FunctionCoverage(_message.Message):
|
||||
__slots__ = ["covered_lines", "function_name", "function_paths", "total_lines"]
|
||||
COVERED_LINES_FIELD_NUMBER: _ClassVar[int]
|
||||
FUNCTION_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
FUNCTION_PATHS_FIELD_NUMBER: _ClassVar[int]
|
||||
TOTAL_LINES_FIELD_NUMBER: _ClassVar[int]
|
||||
covered_lines: int
|
||||
function_name: str
|
||||
function_paths: _containers.RepeatedScalarFieldContainer[str]
|
||||
total_lines: int
|
||||
def __init__(self, function_name: _Optional[str] = ..., function_paths: _Optional[_Iterable[str]] = ..., total_lines: _Optional[int] = ..., covered_lines: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class IndexOutput(_message.Message):
|
||||
__slots__ = ["build_type", "package_name", "sanitizer", "task_dir", "task_id"]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
build_type: BuildType
|
||||
package_name: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class IndexRequest(_message.Message):
|
||||
__slots__ = ["build_type", "package_name", "sanitizer", "task_dir", "task_id"]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
build_type: BuildType
|
||||
package_name: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class POVReproduceRequest(_message.Message):
|
||||
__slots__ = ["harness_name", "internal_patch_id", "pov_path", "sanitizer", "task_id"]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
POV_PATH_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
harness_name: str
|
||||
internal_patch_id: str
|
||||
pov_path: str
|
||||
sanitizer: str
|
||||
task_id: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., harness_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., pov_path: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class POVReproduceResponse(_message.Message):
|
||||
__slots__ = ["did_crash", "request"]
|
||||
DID_CRASH_FIELD_NUMBER: _ClassVar[int]
|
||||
REQUEST_FIELD_NUMBER: _ClassVar[int]
|
||||
did_crash: bool
|
||||
request: POVReproduceRequest
|
||||
def __init__(self, request: _Optional[_Union[POVReproduceRequest, _Mapping]] = ..., did_crash: bool = ...) -> None: ...
|
||||
|
||||
class Patch(_message.Message):
|
||||
__slots__ = ["internal_patch_id", "patch", "task_id"]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
internal_patch_id: str
|
||||
patch: str
|
||||
task_id: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., patch: _Optional[str] = ...) -> None: ...
|
||||
task_type: Task.TaskType
|
||||
sources: _containers.RepeatedCompositeFieldContainer[SourceDetail]
|
||||
deadline: int
|
||||
cancelled: bool
|
||||
project_name: str
|
||||
focus: str
|
||||
metadata: _containers.ScalarMap[str, str]
|
||||
def __init__(self, message_id: _Optional[str] = ..., message_time: _Optional[int] = ..., task_id: _Optional[str] = ..., task_type: _Optional[_Union[Task.TaskType, str]] = ..., sources: _Optional[_Iterable[_Union[SourceDetail, _Mapping]]] = ..., deadline: _Optional[int] = ..., cancelled: bool = ..., project_name: _Optional[str] = ..., focus: _Optional[str] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ...
|
||||
|
||||
class SourceDetail(_message.Message):
|
||||
__slots__ = ["sha256", "source_type", "url"]
|
||||
__slots__ = ("sha256", "source_type", "url")
|
||||
class SourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
SHA256_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_TYPE_DIFF: SourceDetail.SourceType
|
||||
SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_TYPE_FUZZ_TOOLING: SourceDetail.SourceType
|
||||
__slots__ = ()
|
||||
SOURCE_TYPE_REPO: _ClassVar[SourceDetail.SourceType]
|
||||
SOURCE_TYPE_FUZZ_TOOLING: _ClassVar[SourceDetail.SourceType]
|
||||
SOURCE_TYPE_DIFF: _ClassVar[SourceDetail.SourceType]
|
||||
SOURCE_TYPE_REPO: SourceDetail.SourceType
|
||||
SOURCE_TYPE_FUZZ_TOOLING: SourceDetail.SourceType
|
||||
SOURCE_TYPE_DIFF: SourceDetail.SourceType
|
||||
SHA256_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
URL_FIELD_NUMBER: _ClassVar[int]
|
||||
sha256: str
|
||||
source_type: SourceDetail.SourceType
|
||||
url: str
|
||||
def __init__(self, sha256: _Optional[str] = ..., source_type: _Optional[_Union[SourceDetail.SourceType, str]] = ..., url: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class SubmissionEntry(_message.Message):
|
||||
__slots__ = ["bundles", "crashes", "patch_idx", "patch_submission_attempts", "patches", "stop"]
|
||||
BUNDLES_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASHES_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCHES_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_IDX_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_SUBMISSION_ATTEMPTS_FIELD_NUMBER: _ClassVar[int]
|
||||
STOP_FIELD_NUMBER: _ClassVar[int]
|
||||
bundles: _containers.RepeatedCompositeFieldContainer[Bundle]
|
||||
crashes: _containers.RepeatedCompositeFieldContainer[CrashWithId]
|
||||
patch_idx: int
|
||||
patch_submission_attempts: int
|
||||
patches: _containers.RepeatedCompositeFieldContainer[SubmissionEntryPatch]
|
||||
stop: bool
|
||||
def __init__(self, stop: bool = ..., crashes: _Optional[_Iterable[_Union[CrashWithId, _Mapping]]] = ..., bundles: _Optional[_Iterable[_Union[Bundle, _Mapping]]] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempts: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class SubmissionEntryPatch(_message.Message):
|
||||
__slots__ = ["build_outputs", "competition_patch_id", "internal_patch_id", "patch", "result"]
|
||||
BUILD_OUTPUTS_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
RESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
build_outputs: _containers.RepeatedCompositeFieldContainer[BuildOutput]
|
||||
competition_patch_id: str
|
||||
internal_patch_id: str
|
||||
patch: str
|
||||
result: SubmissionResult
|
||||
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
|
||||
|
||||
class Task(_message.Message):
|
||||
__slots__ = ["cancelled", "deadline", "focus", "message_id", "message_time", "metadata", "project_name", "sources", "task_id", "task_type"]
|
||||
class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
class MetadataEntry(_message.Message):
|
||||
__slots__ = ["key", "value"]
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: str
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
|
||||
CANCELLED_FIELD_NUMBER: _ClassVar[int]
|
||||
DEADLINE_FIELD_NUMBER: _ClassVar[int]
|
||||
FOCUS_FIELD_NUMBER: _ClassVar[int]
|
||||
MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
MESSAGE_TIME_FIELD_NUMBER: _ClassVar[int]
|
||||
METADATA_FIELD_NUMBER: _ClassVar[int]
|
||||
PROJECT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SOURCES_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_TYPE_DELTA: Task.TaskType
|
||||
TASK_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_TYPE_FULL: Task.TaskType
|
||||
cancelled: bool
|
||||
deadline: int
|
||||
focus: str
|
||||
message_id: str
|
||||
message_time: int
|
||||
metadata: _containers.ScalarMap[str, str]
|
||||
project_name: str
|
||||
sources: _containers.RepeatedCompositeFieldContainer[SourceDetail]
|
||||
task_id: str
|
||||
task_type: Task.TaskType
|
||||
def __init__(self, message_id: _Optional[str] = ..., message_time: _Optional[int] = ..., task_id: _Optional[str] = ..., task_type: _Optional[_Union[Task.TaskType, str]] = ..., sources: _Optional[_Iterable[_Union[SourceDetail, _Mapping]]] = ..., deadline: _Optional[int] = ..., cancelled: bool = ..., project_name: _Optional[str] = ..., focus: _Optional[str] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ...
|
||||
|
||||
class TaskDelete(_message.Message):
|
||||
__slots__ = ["all", "received_at", "task_id"]
|
||||
ALL_FIELD_NUMBER: _ClassVar[int]
|
||||
RECEIVED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
all: bool
|
||||
received_at: float
|
||||
task_id: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., all: bool = ..., received_at: _Optional[float] = ...) -> None: ...
|
||||
|
||||
class TaskDownload(_message.Message):
|
||||
__slots__ = ["task"]
|
||||
__slots__ = ("task",)
|
||||
TASK_FIELD_NUMBER: _ClassVar[int]
|
||||
task: Task
|
||||
def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class TaskReady(_message.Message):
|
||||
__slots__ = ["task"]
|
||||
__slots__ = ("task",)
|
||||
TASK_FIELD_NUMBER: _ClassVar[int]
|
||||
task: Task
|
||||
def __init__(self, task: _Optional[_Union[Task, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class TaskDelete(_message.Message):
|
||||
__slots__ = ("task_id", "all", "received_at")
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
ALL_FIELD_NUMBER: _ClassVar[int]
|
||||
RECEIVED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||
task_id: str
|
||||
all: bool
|
||||
received_at: float
|
||||
def __init__(self, task_id: _Optional[str] = ..., all: bool = ..., received_at: _Optional[float] = ...) -> None: ...
|
||||
|
||||
class BuildRequest(_message.Message):
|
||||
__slots__ = ("engine", "sanitizer", "task_dir", "task_id", "build_type", "apply_diff", "patch", "internal_patch_id")
|
||||
ENGINE_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
engine: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
build_type: BuildType
|
||||
apply_diff: bool
|
||||
patch: str
|
||||
internal_patch_id: str
|
||||
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class BuildOutput(_message.Message):
|
||||
__slots__ = ("engine", "sanitizer", "task_dir", "task_id", "build_type", "apply_diff", "internal_patch_id")
|
||||
ENGINE_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
APPLY_DIFF_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
engine: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
build_type: BuildType
|
||||
apply_diff: bool
|
||||
internal_patch_id: str
|
||||
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class WeightedHarness(_message.Message):
|
||||
__slots__ = ("weight", "package_name", "harness_name", "task_id")
|
||||
WEIGHT_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
weight: float
|
||||
package_name: str
|
||||
harness_name: str
|
||||
task_id: str
|
||||
def __init__(self, weight: _Optional[float] = ..., package_name: _Optional[str] = ..., harness_name: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Crash(_message.Message):
|
||||
__slots__ = ("target", "harness_name", "crash_input_path", "stacktrace", "crash_token")
|
||||
TARGET_FIELD_NUMBER: _ClassVar[int]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASH_INPUT_PATH_FIELD_NUMBER: _ClassVar[int]
|
||||
STACKTRACE_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASH_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
||||
target: BuildOutput
|
||||
harness_name: str
|
||||
crash_input_path: str
|
||||
stacktrace: str
|
||||
crash_token: str
|
||||
def __init__(self, target: _Optional[_Union[BuildOutput, _Mapping]] = ..., harness_name: _Optional[str] = ..., crash_input_path: _Optional[str] = ..., stacktrace: _Optional[str] = ..., crash_token: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class TracedCrash(_message.Message):
|
||||
__slots__ = ["crash", "tracer_stacktrace"]
|
||||
__slots__ = ("crash", "tracer_stacktrace")
|
||||
CRASH_FIELD_NUMBER: _ClassVar[int]
|
||||
TRACER_STACKTRACE_FIELD_NUMBER: _ClassVar[int]
|
||||
crash: Crash
|
||||
tracer_stacktrace: str
|
||||
def __init__(self, crash: _Optional[_Union[Crash, _Mapping]] = ..., tracer_stacktrace: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class WeightedHarness(_message.Message):
|
||||
__slots__ = ["harness_name", "package_name", "task_id", "weight"]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
class ConfirmedVulnerability(_message.Message):
|
||||
__slots__ = ("crashes", "internal_patch_id")
|
||||
CRASHES_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
crashes: _containers.RepeatedCompositeFieldContainer[TracedCrash]
|
||||
internal_patch_id: str
|
||||
def __init__(self, crashes: _Optional[_Iterable[_Union[TracedCrash, _Mapping]]] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Patch(_message.Message):
|
||||
__slots__ = ("task_id", "internal_patch_id", "patch")
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
WEIGHT_FIELD_NUMBER: _ClassVar[int]
|
||||
harness_name: str
|
||||
package_name: str
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
task_id: str
|
||||
weight: float
|
||||
def __init__(self, weight: _Optional[float] = ..., package_name: _Optional[str] = ..., harness_name: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
internal_patch_id: str
|
||||
patch: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., patch: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class BuildType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
class IndexRequest(_message.Message):
|
||||
__slots__ = ("build_type", "package_name", "sanitizer", "task_dir", "task_id")
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
build_type: BuildType
|
||||
package_name: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
class IndexOutput(_message.Message):
|
||||
__slots__ = ("build_type", "package_name", "sanitizer", "task_dir", "task_id")
|
||||
BUILD_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKAGE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_DIR_FIELD_NUMBER: _ClassVar[int]
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
build_type: BuildType
|
||||
package_name: str
|
||||
sanitizer: str
|
||||
task_dir: str
|
||||
task_id: str
|
||||
def __init__(self, build_type: _Optional[_Union[BuildType, str]] = ..., package_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class FunctionCoverage(_message.Message):
|
||||
__slots__ = ("function_name", "function_paths", "total_lines", "covered_lines")
|
||||
FUNCTION_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
FUNCTION_PATHS_FIELD_NUMBER: _ClassVar[int]
|
||||
TOTAL_LINES_FIELD_NUMBER: _ClassVar[int]
|
||||
COVERED_LINES_FIELD_NUMBER: _ClassVar[int]
|
||||
function_name: str
|
||||
function_paths: _containers.RepeatedScalarFieldContainer[str]
|
||||
total_lines: int
|
||||
covered_lines: int
|
||||
def __init__(self, function_name: _Optional[str] = ..., function_paths: _Optional[_Iterable[str]] = ..., total_lines: _Optional[int] = ..., covered_lines: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class SubmissionEntryPatch(_message.Message):
|
||||
__slots__ = ("patch", "internal_patch_id", "competition_patch_id", "build_outputs", "result")
|
||||
PATCH_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_OUTPUTS_FIELD_NUMBER: _ClassVar[int]
|
||||
RESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
patch: str
|
||||
internal_patch_id: str
|
||||
competition_patch_id: str
|
||||
build_outputs: _containers.RepeatedCompositeFieldContainer[BuildOutput]
|
||||
result: SubmissionResult
|
||||
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
|
||||
|
||||
class Bundle(_message.Message):
|
||||
__slots__ = ("task_id", "competition_pov_id", "competition_patch_id", "competition_sarif_id", "bundle_id")
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_SARIF_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
task_id: str
|
||||
competition_pov_id: str
|
||||
competition_patch_id: str
|
||||
competition_sarif_id: str
|
||||
bundle_id: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., competition_pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., competition_sarif_id: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class CrashWithId(_message.Message):
|
||||
__slots__ = ("crash", "competition_pov_id", "result")
|
||||
CRASH_FIELD_NUMBER: _ClassVar[int]
|
||||
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
RESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
crash: TracedCrash
|
||||
competition_pov_id: str
|
||||
result: SubmissionResult
|
||||
def __init__(self, crash: _Optional[_Union[TracedCrash, _Mapping]] = ..., competition_pov_id: _Optional[str] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
|
||||
|
||||
class SubmissionEntry(_message.Message):
|
||||
__slots__ = ("stop", "crashes", "bundles", "patches", "patch_idx", "patch_submission_attempts")
|
||||
STOP_FIELD_NUMBER: _ClassVar[int]
|
||||
CRASHES_FIELD_NUMBER: _ClassVar[int]
|
||||
BUNDLES_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCHES_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_IDX_FIELD_NUMBER: _ClassVar[int]
|
||||
PATCH_SUBMISSION_ATTEMPTS_FIELD_NUMBER: _ClassVar[int]
|
||||
stop: bool
|
||||
crashes: _containers.RepeatedCompositeFieldContainer[CrashWithId]
|
||||
bundles: _containers.RepeatedCompositeFieldContainer[Bundle]
|
||||
patches: _containers.RepeatedCompositeFieldContainer[SubmissionEntryPatch]
|
||||
patch_idx: int
|
||||
patch_submission_attempts: int
|
||||
def __init__(self, stop: bool = ..., crashes: _Optional[_Iterable[_Union[CrashWithId, _Mapping]]] = ..., bundles: _Optional[_Iterable[_Union[Bundle, _Mapping]]] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempts: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class POVReproduceRequest(_message.Message):
|
||||
__slots__ = ("task_id", "internal_patch_id", "harness_name", "sanitizer", "pov_path")
|
||||
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
HARNESS_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
SANITIZER_FIELD_NUMBER: _ClassVar[int]
|
||||
POV_PATH_FIELD_NUMBER: _ClassVar[int]
|
||||
task_id: str
|
||||
internal_patch_id: str
|
||||
harness_name: str
|
||||
sanitizer: str
|
||||
pov_path: str
|
||||
def __init__(self, task_id: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., harness_name: _Optional[str] = ..., sanitizer: _Optional[str] = ..., pov_path: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class POVReproduceResponse(_message.Message):
|
||||
__slots__ = ("request", "did_crash")
|
||||
REQUEST_FIELD_NUMBER: _ClassVar[int]
|
||||
DID_CRASH_FIELD_NUMBER: _ClassVar[int]
|
||||
request: POVReproduceRequest
|
||||
did_crash: bool
|
||||
def __init__(self, request: _Optional[_Union[POVReproduceRequest, _Mapping]] = ..., did_crash: bool = ...) -> None: ...
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from redis import Redis
|
||||
from buttercup.common.utils import serve_loop
|
||||
from buttercup.common.datastructures.msg_pb2 import WeightedHarness, BuildOutput, BuildType
|
||||
from buttercup.common.maps import HarnessWeights, BuildMap
|
||||
|
||||
import random
|
||||
import logging
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput, BuildType, WeightedHarness
|
||||
from buttercup.common.maps import BuildMap, HarnessWeights
|
||||
from buttercup.common.utils import serve_loop
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
import requests
|
||||
import functools
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_openai.chat_models import ChatOpenAI
|
||||
from langfuse.callback import CallbackHandler
|
||||
from langchain.callbacks.base import BaseCallbackHandler
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from langchain_core.callbacks import BaseCallbackHandler
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.runnables import ConfigurableField, Runnable
|
||||
from langchain_openai.chat_models import ChatOpenAI
|
||||
from langfuse.langchain import CallbackHandler
|
||||
from pydantic import SecretStr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,9 +30,12 @@ class ButtercupLLM(Enum):
|
||||
OPENAI_GPT_4_1_NANO = "openai-gpt-4.1-nano"
|
||||
OPENAI_GPT_4_1_MINI = "openai-gpt-4.1-mini"
|
||||
OPENAI_GPT_4_1 = "openai-gpt-4.1"
|
||||
CLAUDE_3_5_SONNET = "claude-3.5-sonnet"
|
||||
CLAUDE_3_7_SONNET = "claude-3.7-sonnet"
|
||||
CLAUDE_4_SONNET = "claude-4-sonnet"
|
||||
CLAUDE_4_5_SONNET = "claude-4.5-sonnet"
|
||||
GEMINI_PRO = "gemini-pro"
|
||||
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
||||
GEMINI_2_5_FLASH_EXP = "gemini-2.5-flash-exp"
|
||||
|
||||
|
||||
@functools.cache
|
||||
@@ -43,7 +47,7 @@ def is_langfuse_available() -> bool:
|
||||
return False
|
||||
try:
|
||||
response = requests.post(f"{langfuse_host}/api/public/ingestion", timeout=2)
|
||||
return bool(response.status_code == 401) # expect that we aren't authenticated
|
||||
return response.status_code == 401 # expect that we aren't authenticated
|
||||
except requests.RequestException:
|
||||
return False
|
||||
|
||||
@@ -62,9 +66,11 @@ def langfuse_auth_check() -> bool:
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{langfuse_host}/api/public/ingestion", timeout=2, auth=(langfuse_public_key, langfuse_secret_key)
|
||||
f"{langfuse_host}/api/public/ingestion",
|
||||
timeout=2,
|
||||
auth=(langfuse_public_key, langfuse_secret_key),
|
||||
)
|
||||
return bool(response.status_code == 400) # expect that we authenticate, but the request is invalid
|
||||
return response.status_code == 400 # expect that we authenticate, but the request is invalid
|
||||
except requests.RequestException:
|
||||
return False
|
||||
|
||||
@@ -74,11 +80,7 @@ def get_langfuse_callbacks() -> list[BaseCallbackHandler]:
|
||||
"""Get Langchain callbacks for monitoring LLM calls with LangFuse, if available."""
|
||||
if is_langfuse_available():
|
||||
try:
|
||||
langfuse_handler = CallbackHandler(
|
||||
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
|
||||
secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
|
||||
host=os.getenv("LANGFUSE_HOST"),
|
||||
)
|
||||
langfuse_handler = CallbackHandler()
|
||||
if langfuse_auth_check():
|
||||
logger.info("Tracing with LangFuse enabled")
|
||||
return [langfuse_handler]
|
||||
@@ -92,31 +94,41 @@ def get_langfuse_callbacks() -> list[BaseCallbackHandler]:
|
||||
return []
|
||||
|
||||
|
||||
def create_default_llm(**kwargs: Any) -> BaseChatModel:
|
||||
def create_default_llm(**kwargs: Any) -> Runnable:
|
||||
"""Create an LLM object with the default configuration."""
|
||||
fallback_models = kwargs.pop("fallback_models", [])
|
||||
fallback_models = [create_default_llm(**{**kwargs, "model_name": m.value}) for m in fallback_models]
|
||||
return create_llm(
|
||||
model_name=kwargs.pop("model_name", ButtercupLLM.OPENAI_GPT_4_1.value),
|
||||
temperature=kwargs.pop("temperature", 0.1),
|
||||
timeout=420.0,
|
||||
max_retries=3,
|
||||
**kwargs,
|
||||
)
|
||||
).with_fallbacks(fallback_models)
|
||||
|
||||
|
||||
def create_default_llm_with_temperature(**kwargs: Any) -> BaseChatModel:
|
||||
def create_default_llm_with_temperature(**kwargs: Any) -> Runnable:
|
||||
"""Create an LLM object with the default configuration and temperature."""
|
||||
return create_llm( # type: ignore
|
||||
model_name=kwargs.pop("model_name", ButtercupLLM.OPENAI_GPT_4_1.value),
|
||||
temperature=kwargs.pop("temperature", 0.1),
|
||||
timeout=420.0,
|
||||
max_retries=3,
|
||||
**kwargs,
|
||||
).configurable_fields(
|
||||
temperature=ConfigurableField(
|
||||
id="llm_temperature",
|
||||
name="LLM temperature",
|
||||
description="The temperature for the LLM model",
|
||||
),
|
||||
fallback_models = kwargs.pop("fallback_models", [])
|
||||
fallback_models = [
|
||||
create_default_llm_with_temperature(**{**kwargs, "model_name": m.value}) for m in fallback_models
|
||||
]
|
||||
return (
|
||||
create_llm(
|
||||
model_name=kwargs.pop("model_name", ButtercupLLM.OPENAI_GPT_4_1.value),
|
||||
temperature=kwargs.pop("temperature", 0.1),
|
||||
timeout=420.0,
|
||||
max_retries=3,
|
||||
**kwargs,
|
||||
)
|
||||
.configurable_fields(
|
||||
temperature=ConfigurableField(
|
||||
id="llm_temperature",
|
||||
name="LLM temperature",
|
||||
description="The temperature for the LLM model",
|
||||
),
|
||||
)
|
||||
.with_fallbacks(fallback_models)
|
||||
)
|
||||
|
||||
|
||||
@@ -124,6 +136,6 @@ def create_llm(**kwargs: Any) -> BaseChatModel:
|
||||
"""Create an LLM object with the given configuration."""
|
||||
return ChatOpenAI(
|
||||
openai_api_base=os.environ["BUTTERCUP_LITELLM_HOSTNAME"],
|
||||
openai_api_key=os.environ["BUTTERCUP_LITELLM_KEY"],
|
||||
openai_api_key=SecretStr(os.environ["BUTTERCUP_LITELLM_KEY"]),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
import atexit
|
||||
import os
|
||||
|
||||
if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc":
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
else:
|
||||
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter # type: ignore
|
||||
try:
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
|
||||
if os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") == "grpc":
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
else:
|
||||
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
||||
|
||||
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
|
||||
_opentelemetry_enabled = True
|
||||
except ImportError:
|
||||
_opentelemetry_enabled = False
|
||||
|
||||
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from buttercup.common.telemetry import crs_instance_id, service_instance_id
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from buttercup.common.telemetry import crs_instance_id, service_instance_id
|
||||
|
||||
_is_initialized = False
|
||||
PACKAGE_LOGGER_NAME = "buttercup"
|
||||
|
||||
@@ -30,7 +39,10 @@ class MaxLengthFormatter(logging.Formatter):
|
||||
|
||||
|
||||
def setup_package_logger(
|
||||
application_name: str, logger_name: str, log_level: str = "info", max_line_length: int | None = None
|
||||
application_name: str,
|
||||
logger_name: str,
|
||||
log_level: str = "info",
|
||||
max_line_length: int | None = None,
|
||||
) -> logging.Logger:
|
||||
global _is_initialized
|
||||
|
||||
@@ -42,26 +54,27 @@ def setup_package_logger(
|
||||
root.removeHandler(handler)
|
||||
|
||||
# Create resource with service and environment information
|
||||
resource = Resource.create(
|
||||
attributes={
|
||||
"service.name": application_name,
|
||||
"service.instance.id": service_instance_id,
|
||||
"crs.instance.id": crs_instance_id,
|
||||
}
|
||||
)
|
||||
|
||||
# Initialize the LoggerProvider with the created resource.
|
||||
logger_provider = LoggerProvider(resource=resource)
|
||||
|
||||
# Configure the span exporter and processor based on whether the endpoint is effectively set.
|
||||
otlp_handler = None
|
||||
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
||||
set_logger_provider(logger_provider)
|
||||
exporter = OTLPLogExporter()
|
||||
if _opentelemetry_enabled:
|
||||
resource = Resource.create(
|
||||
attributes={
|
||||
"service.name": application_name,
|
||||
"service.instance.id": service_instance_id,
|
||||
"crs.instance.id": crs_instance_id,
|
||||
}
|
||||
)
|
||||
|
||||
# add the batch processors to the trace provider
|
||||
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
|
||||
otlp_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
|
||||
# Initialize the LoggerProvider with the created resource.
|
||||
logger_provider = LoggerProvider(resource=resource)
|
||||
|
||||
# Configure the span exporter and processor based on whether the endpoint is effectively set.
|
||||
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
||||
set_logger_provider(logger_provider)
|
||||
exporter = OTLPLogExporter()
|
||||
|
||||
# add the batch processors to the trace provider
|
||||
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
|
||||
otlp_handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
|
||||
|
||||
persistent_log_dir = os.getenv("PERSISTENT_LOG_DIR", None)
|
||||
|
||||
@@ -86,6 +99,9 @@ def setup_package_logger(
|
||||
handlers=handlers,
|
||||
)
|
||||
|
||||
# Ensure all logging handlers are properly closed at program exit
|
||||
atexit.register(logging.shutdown)
|
||||
|
||||
_package_logger = logging.getLogger(PACKAGE_LOGGER_NAME)
|
||||
_package_logger.setLevel(log_level.upper())
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from typing import Generic, TypeVar, Type, Iterator
|
||||
from buttercup.common.datastructures.msg_pb2 import WeightedHarness, BuildOutput, FunctionCoverage, BuildType
|
||||
from redis import Redis
|
||||
from bson.json_util import dumps, CANONICAL_JSON_OPTIONS
|
||||
from collections.abc import Iterator
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from bson.json_util import CANONICAL_JSON_OPTIONS, dumps
|
||||
from google.protobuf.message import Message
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput, BuildType, FunctionCoverage, WeightedHarness
|
||||
from buttercup.common.sets import RedisSet
|
||||
|
||||
# ruff: noqa: UP046
|
||||
|
||||
MsgType = TypeVar("MsgType", bound=Message)
|
||||
MSG_FIELD_NAME = b"msg"
|
||||
|
||||
|
||||
class RedisMap(Generic[MsgType]):
|
||||
def __init__(self, redis: Redis, hash_name: str, msg_builder: Type[MsgType]):
|
||||
def __init__(self, redis: Redis, hash_name: str, msg_builder: type[MsgType]):
|
||||
self.redis = redis
|
||||
self.msg_builder = msg_builder
|
||||
self.hash_name = hash_name
|
||||
@@ -22,7 +27,7 @@ class RedisMap(Generic[MsgType]):
|
||||
|
||||
msg = self.msg_builder()
|
||||
msg.ParseFromString(it)
|
||||
return msg # type: ignore[no-any-return]
|
||||
return msg
|
||||
|
||||
def set(self, key: str, value: MsgType) -> None:
|
||||
self.redis.hset(self.hash_name, key, value.SerializeToString())
|
||||
@@ -52,7 +57,8 @@ class BuildMap:
|
||||
|
||||
def _build_output_key(self, task_id: str, build_type: BuildType, san: str, internal_patch_id: str) -> str:
|
||||
return dumps(
|
||||
[task_id, BUILD_SAN_MAP_NAME, build_type, san, internal_patch_id], json_options=CANONICAL_JSON_OPTIONS
|
||||
[task_id, BUILD_SAN_MAP_NAME, build_type, san, internal_patch_id],
|
||||
json_options=CANONICAL_JSON_OPTIONS,
|
||||
)
|
||||
|
||||
def add_build(self, build: BuildOutput) -> None:
|
||||
@@ -78,7 +84,11 @@ class BuildMap:
|
||||
return builds
|
||||
|
||||
def get_build_from_san(
|
||||
self, task_id: str, build_type: BuildType, san: str, internal_patch_id: str = ""
|
||||
self,
|
||||
task_id: str,
|
||||
build_type: BuildType,
|
||||
san: str,
|
||||
internal_patch_id: str = "",
|
||||
) -> BuildOutput | None:
|
||||
if internal_patch_id != "":
|
||||
assert build_type == BuildType.PATCH, "internal_patch_id is only valid for PATCH builds"
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
# Store the node local path for subsequent use
|
||||
from contextlib import contextmanager
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import NamedTemporaryFile
|
||||
import tempfile
|
||||
from typing import Any, Iterator, TypeAlias
|
||||
from collections.abc import Iterator
|
||||
from contextlib import AbstractContextManager, contextmanager
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
node_local_path = os.getenv("NODE_DATA_DIR")
|
||||
|
||||
# Optional tmpfs path for corpus storage
|
||||
# When set, the fuzzing corpus will be stored on this tmpfs mount for improved performance
|
||||
corpus_tmpfs_path = os.getenv("CORPUS_TMPFS_PATH")
|
||||
|
||||
|
||||
NodeLocalPath: TypeAlias = Path
|
||||
RemotePath: TypeAlias = Path
|
||||
@@ -23,6 +29,18 @@ def _get_root_path() -> NodeLocalPath:
|
||||
return NodeLocalPath(node_local_path)
|
||||
|
||||
|
||||
def get_corpus_tmpfs_path() -> Path | None:
|
||||
"""Return the tmpfs path for corpus storage if configured, otherwise None."""
|
||||
if corpus_tmpfs_path:
|
||||
return Path(corpus_tmpfs_path)
|
||||
return None
|
||||
|
||||
|
||||
def is_corpus_tmpfs_enabled() -> bool:
|
||||
"""Check if tmpfs corpus storage is enabled."""
|
||||
return corpus_tmpfs_path is not None
|
||||
|
||||
|
||||
class TmpDir:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
@@ -47,7 +65,10 @@ def temp_dir(root_path: Path) -> Iterator[TmpDir]:
|
||||
|
||||
|
||||
def rename_atomically(src: Path, dst: Path) -> Path | None:
|
||||
"""Rename a file atomically"""
|
||||
"""Rename a file atomically.
|
||||
|
||||
Falls back to copy+delete for cross-filesystem operations.
|
||||
"""
|
||||
src = Path(src)
|
||||
dst = Path(dst)
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -56,13 +77,50 @@ def rename_atomically(src: Path, dst: Path) -> Path | None:
|
||||
except OSError as e:
|
||||
# If the path already exists, it means another pod already downloaded it
|
||||
# we can just ignore this error and return None to signify that the path already exists
|
||||
if e.errno == 39:
|
||||
if e.errno == errno.ENOTEMPTY: # 39 = Directory not empty
|
||||
logger.debug(f"Local path {dst} already exists for {src}")
|
||||
return None
|
||||
# Handle cross-filesystem rename (errno 18 = EXDEV = Invalid cross-device link)
|
||||
if e.errno == errno.EXDEV:
|
||||
logger.debug(f"Cross-filesystem rename from {src} to {dst}, using copy+delete")
|
||||
return _copy_and_delete(src, dst)
|
||||
raise e
|
||||
return dst
|
||||
|
||||
|
||||
def _copy_and_delete(src: Path, dst: Path) -> Path | None:
|
||||
"""Copy a file/directory and delete the source. Used for cross-filesystem operations.
|
||||
|
||||
Note: Unlike os.rename, this intentionally returns None if dst already exists
|
||||
rather than overwriting. This supports the "first pod wins" pattern used by
|
||||
callers in distributed scenarios where concurrent pods may race to create the same file.
|
||||
"""
|
||||
try:
|
||||
if src.is_dir():
|
||||
# For directories, use copytree
|
||||
if dst.exists():
|
||||
logger.debug(f"Destination {dst} already exists, skipping copy")
|
||||
shutil.rmtree(src, ignore_errors=True)
|
||||
return None
|
||||
shutil.copytree(src, dst)
|
||||
shutil.rmtree(src, ignore_errors=True)
|
||||
else:
|
||||
# For files, use copy2 to preserve metadata
|
||||
if dst.exists():
|
||||
logger.debug(f"Destination {dst} already exists, skipping copy")
|
||||
try:
|
||||
os.unlink(src)
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
shutil.copy2(src, dst)
|
||||
os.unlink(src)
|
||||
return dst
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to copy {src} to {dst}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def remote_path(local_path: NodeLocalPath) -> RemotePath:
|
||||
"""Convert the node local path to a remote path"""
|
||||
local_path = Path(local_path)
|
||||
@@ -89,7 +147,7 @@ def scratch_path() -> NodeLocalPath:
|
||||
return scratch_dir
|
||||
|
||||
|
||||
def scratch_dir() -> Any:
|
||||
def scratch_dir() -> AbstractContextManager[TmpDir]:
|
||||
"""Return a temporary directory in the scratch directory"""
|
||||
return temp_dir(scratch_path())
|
||||
|
||||
@@ -187,6 +245,7 @@ def dir_to_remote_archive(local_path: NodeLocalPath) -> RemotePath:
|
||||
def lopen(local_path: NodeLocalPath, mode: str) -> Any:
|
||||
"""Open a file in the node local storage
|
||||
|
||||
If it doesn't exist, it will be downloaded from the remote storage"""
|
||||
If it doesn't exist, it will be downloaded from the remote storage
|
||||
"""
|
||||
make_locally_available(local_path)
|
||||
return open(local_path, mode)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from dataclasses import dataclass, field
|
||||
import yaml
|
||||
from buttercup.common.challenge_task import ChallengeTask
|
||||
from enum import Enum
|
||||
|
||||
import yaml
|
||||
|
||||
from buttercup.common.challenge_task import ChallengeTask
|
||||
|
||||
|
||||
class Language(str, Enum):
|
||||
C = "c"
|
||||
CPP = "cpp"
|
||||
JAVA = "java"
|
||||
|
||||
|
||||
@@ -43,9 +46,11 @@ class ProjectYaml:
|
||||
@property
|
||||
def unified_language(self) -> Language:
|
||||
"""Language field but with a more consistent naming convention."""
|
||||
if self.language.lower() in ["c", "c++", "cpp"]:
|
||||
if self.language.lower() in ["c"]:
|
||||
return Language.C
|
||||
elif self.language.lower() in ["java", "jvm"]:
|
||||
if self.language.lower() in ["c++", "cpp"]:
|
||||
return Language.CPP
|
||||
if self.language.lower() in ["java", "jvm"]:
|
||||
return Language.JAVA
|
||||
|
||||
raise ValueError(f"Unsupported language: {self.language}")
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from redis import Redis, RedisError
|
||||
from google.protobuf.message import Message
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
from typing import Any, Generic, Literal, TypeVar, cast, overload
|
||||
|
||||
from google.protobuf.message import Message
|
||||
from redis import Redis, RedisError
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import (
|
||||
BuildRequest,
|
||||
BuildOutput,
|
||||
Crash,
|
||||
TaskDownload,
|
||||
TaskReady,
|
||||
TaskDelete,
|
||||
Patch,
|
||||
BuildRequest,
|
||||
ConfirmedVulnerability,
|
||||
IndexRequest,
|
||||
Crash,
|
||||
IndexOutput,
|
||||
TracedCrash,
|
||||
IndexRequest,
|
||||
Patch,
|
||||
POVReproduceRequest,
|
||||
POVReproduceResponse,
|
||||
TaskDelete,
|
||||
TaskDownload,
|
||||
TaskReady,
|
||||
TracedCrash,
|
||||
)
|
||||
import logging
|
||||
from typing import Type, Generic, TypeVar, Literal, overload, Callable, cast
|
||||
import uuid
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
# ruff: noqa: UP046
|
||||
|
||||
TIMES_DELIVERED_FIELD = "times_delivered"
|
||||
|
||||
@@ -86,9 +89,7 @@ MsgType = TypeVar("MsgType", bound=Message)
|
||||
|
||||
@dataclass
|
||||
class RQItem(Generic[MsgType]):
|
||||
"""
|
||||
A single item in a reliable queue.
|
||||
"""
|
||||
"""A single item in a reliable queue."""
|
||||
|
||||
item_id: str
|
||||
deserialized: MsgType
|
||||
@@ -96,13 +97,11 @@ class RQItem(Generic[MsgType]):
|
||||
|
||||
@dataclass
|
||||
class ReliableQueue(Generic[MsgType]):
|
||||
"""
|
||||
A queue that is reliable and can be used to process tasks in a distributed environment.
|
||||
"""
|
||||
"""A queue that is reliable and can be used to process tasks in a distributed environment."""
|
||||
|
||||
redis: Redis
|
||||
queue_name: str
|
||||
msg_builder: Type[MsgType]
|
||||
msg_builder: type[MsgType]
|
||||
group_name: str | None = None
|
||||
task_timeout_ms: int = 180000
|
||||
reader_name: str | None = None
|
||||
@@ -113,7 +112,7 @@ class ReliableQueue(Generic[MsgType]):
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.reader_name is None:
|
||||
self.reader_name = f"rqueue_{str(uuid.uuid4())}"
|
||||
self.reader_name = f"rqueue_{uuid.uuid4()!s}"
|
||||
|
||||
if self.group_name is not None:
|
||||
# Create consumer group if it doesn't exist
|
||||
@@ -125,9 +124,10 @@ class ReliableQueue(Generic[MsgType]):
|
||||
pass
|
||||
else:
|
||||
logger.exception(
|
||||
"Failed to create consumer group %s for queue %s", self.group_name, self.queue_name
|
||||
"Failed to create consumer group %s for queue %s",
|
||||
self.group_name,
|
||||
self.queue_name,
|
||||
)
|
||||
pass
|
||||
|
||||
def size(self) -> int:
|
||||
return self.redis.xlen(self.queue_name)
|
||||
@@ -145,7 +145,7 @@ class ReliableQueue(Generic[MsgType]):
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return cast(F, wrapper)
|
||||
return cast("F", wrapper)
|
||||
|
||||
@_ensure_group_name
|
||||
def pop(self) -> RQItem[MsgType] | None:
|
||||
@@ -205,7 +205,7 @@ class ReliableQueue(Generic[MsgType]):
|
||||
if pending is None or len(pending) == 0:
|
||||
return 0
|
||||
|
||||
return cast(int, pending[0][TIMES_DELIVERED_FIELD])
|
||||
return cast("int", pending[0][TIMES_DELIVERED_FIELD])
|
||||
|
||||
@_ensure_group_name
|
||||
def claim_item(self, item_id: str, min_idle_time: int = 0) -> None:
|
||||
@@ -215,7 +215,7 @@ class ReliableQueue(Generic[MsgType]):
|
||||
@dataclass
|
||||
class QueueConfig:
|
||||
queue_name: QueueNames
|
||||
msg_builder: Type
|
||||
msg_builder: type
|
||||
task_timeout_ms: int
|
||||
group_names: list[GroupNames] = field(default_factory=list)
|
||||
|
||||
@@ -305,81 +305,126 @@ class QueueFactory:
|
||||
POV_REPRODUCER_RESPONSES_TASK_TIMEOUT_MS,
|
||||
[GroupNames.ORCHESTRATOR],
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.BUILD], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.BUILD],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[BuildRequest]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.BUILD_OUTPUT], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.BUILD_OUTPUT],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[BuildOutput]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.DOWNLOAD_TASKS], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.DOWNLOAD_TASKS],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[TaskDownload]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.READY_TASKS], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.READY_TASKS],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[TaskReady]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.CRASH], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.CRASH],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[Crash]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.TRACED_VULNERABILITIES], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.TRACED_VULNERABILITIES],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[TracedCrash]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.CONFIRMED_VULNERABILITIES], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.CONFIRMED_VULNERABILITIES],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[ConfirmedVulnerability]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.DELETE_TASK], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.DELETE_TASK],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[TaskDelete]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.PATCHES], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.PATCHES],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[Patch]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.INDEX], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.INDEX],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[IndexRequest]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.INDEX_OUTPUT], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.INDEX_OUTPUT],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[IndexOutput]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.POV_REPRODUCER_REQUESTS], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.POV_REPRODUCER_REQUESTS],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[POVReproduceRequest]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: Literal[QueueNames.POV_REPRODUCER_RESPONSES], group_name: GroupNames, **kwargs: Any
|
||||
self,
|
||||
queue_name: Literal[QueueNames.POV_REPRODUCER_RESPONSES],
|
||||
group_name: GroupNames,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[POVReproduceResponse]: ...
|
||||
|
||||
@overload
|
||||
def create(
|
||||
self, queue_name: QueueNames, group_name: GroupNames | None = None, **kwargs: Any
|
||||
self,
|
||||
queue_name: QueueNames,
|
||||
group_name: GroupNames | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[MsgType]: ...
|
||||
|
||||
def create(
|
||||
self, queue_name: QueueNames, group_name: GroupNames | None = None, **kwargs: Any
|
||||
self,
|
||||
queue_name: QueueNames,
|
||||
group_name: GroupNames | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ReliableQueue[MsgType]:
|
||||
if queue_name not in self._config:
|
||||
raise ValueError(f"Invalid queue name: {queue_name}")
|
||||
@@ -399,19 +444,3 @@ class QueueFactory:
|
||||
|
||||
queue_args.update(kwargs)
|
||||
return ReliableQueue(**queue_args) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FuzzConfiguration:
|
||||
corpus_dir: str
|
||||
target_path: str
|
||||
engine: str
|
||||
sanitizer: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildConfiguration:
|
||||
project_id: str
|
||||
engine: str
|
||||
sanitizer: str
|
||||
source_path: str | None
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput
|
||||
from buttercup.common.challenge_task import ReproduceResult, ChallengeTask
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
from contextlib import contextmanager
|
||||
import contextlib
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from buttercup.common.challenge_task import ChallengeTask, ReproduceResult
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReproduceMultiple:
|
||||
def __init__(
|
||||
self, wdir: Path, build_outputs: list[BuildOutput], build_cache: list[ChallengeTask] | None = None
|
||||
self,
|
||||
wdir: Path,
|
||||
build_outputs: list[BuildOutput],
|
||||
build_cache: list[ChallengeTask] | None = None,
|
||||
) -> None:
|
||||
self.build_outputs = build_outputs
|
||||
self.wdir = wdir
|
||||
@@ -32,11 +36,13 @@ class ReproduceMultiple:
|
||||
pass
|
||||
|
||||
def attempt_reproduce(
|
||||
self, pov: Path, harness_name: str
|
||||
self,
|
||||
pov: Path,
|
||||
harness_name: str,
|
||||
) -> Generator[tuple[BuildOutput, ReproduceResult], None, None]:
|
||||
if self.builds_cache is None:
|
||||
raise RuntimeError("Build cache is not populated")
|
||||
for build, task in zip(self.build_outputs, self.builds_cache):
|
||||
for build, task in zip(self.build_outputs, self.builds_cache, strict=False):
|
||||
yield (build, task.reproduce_pov(harness_name, pov))
|
||||
|
||||
def get_first_crash(self, pov: Path, harness_name: str) -> tuple[BuildOutput, ReproduceResult] | None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from redis import Redis
|
||||
|
||||
@@ -8,11 +9,12 @@ from redis import Redis
|
||||
class SARIFBroadcastDetail(BaseModel):
|
||||
"""Model for SARIF broadcast details, matches the model in types.py"""
|
||||
|
||||
metadata: Dict[str, Any] = Field(
|
||||
metadata: dict[str, Any] = Field(
|
||||
...,
|
||||
description="String to string map containing data that should be attached to outputs like log messages and OpenTelemetry trace attributes for traceability",
|
||||
description="String to string map containing data that should be attached to outputs "
|
||||
"like log messages and OpenTelemetry trace attributes for traceability",
|
||||
)
|
||||
sarif: Dict[str, Any] = Field(..., description="SARIF Report compliant with provided schema")
|
||||
sarif: dict[str, Any] = Field(..., description="SARIF Report compliant with provided schema")
|
||||
sarif_id: str
|
||||
task_id: str
|
||||
|
||||
@@ -21,47 +23,47 @@ class SARIFStore:
|
||||
"""Store and retrieve SARIF objects in Redis"""
|
||||
|
||||
def __init__(self, redis: Redis):
|
||||
"""
|
||||
Initialize the SARIF store with a Redis connection.
|
||||
"""Initialize the SARIF store with a Redis connection.
|
||||
|
||||
Args:
|
||||
redis: Redis connection
|
||||
|
||||
"""
|
||||
self.redis = redis
|
||||
self.key_prefix = "sarif:"
|
||||
|
||||
def _get_key(self, task_id: str) -> str:
|
||||
"""
|
||||
Get the Redis key for a task_id.
|
||||
"""Get the Redis key for a task_id.
|
||||
|
||||
Args:
|
||||
task_id: Task ID
|
||||
|
||||
Returns:
|
||||
Redis key
|
||||
|
||||
"""
|
||||
return f"{self.key_prefix}{task_id.lower()}"
|
||||
|
||||
def _decode_key(self, key: str | bytes) -> str:
|
||||
"""
|
||||
Decode a Redis key if it's bytes, otherwise return as is.
|
||||
"""Decode a Redis key if it's bytes, otherwise return as is.
|
||||
|
||||
Args:
|
||||
key: Redis key, either bytes or string
|
||||
|
||||
Returns:
|
||||
Decoded key as string
|
||||
|
||||
"""
|
||||
if isinstance(key, bytes):
|
||||
return key.decode("utf-8")
|
||||
return key
|
||||
|
||||
def store(self, sarif_detail: SARIFBroadcastDetail) -> None:
|
||||
"""
|
||||
Store a SARIF broadcast detail in Redis.
|
||||
"""Store a SARIF broadcast detail in Redis.
|
||||
|
||||
Args:
|
||||
sarif_detail: The SARIF broadcast detail to store
|
||||
|
||||
"""
|
||||
task_id = sarif_detail.task_id
|
||||
key = self._get_key(task_id)
|
||||
@@ -73,12 +75,12 @@ class SARIFStore:
|
||||
# Add to the list for this task
|
||||
self.redis.rpush(key, sarif_json)
|
||||
|
||||
def get_all(self) -> List[SARIFBroadcastDetail]:
|
||||
"""
|
||||
Retrieve all SARIF objects from Redis.
|
||||
def get_all(self) -> list[SARIFBroadcastDetail]:
|
||||
"""Retrieve all SARIF objects from Redis.
|
||||
|
||||
Returns:
|
||||
List of SARIF broadcast details
|
||||
|
||||
"""
|
||||
# Get all SARIF keys in Redis
|
||||
all_keys = self.redis.keys(f"{self.key_prefix}*")
|
||||
@@ -97,15 +99,15 @@ class SARIFStore:
|
||||
|
||||
return result
|
||||
|
||||
def get_by_task_id(self, task_id: str) -> List[SARIFBroadcastDetail]:
|
||||
"""
|
||||
Retrieve all SARIF objects for a specific task.
|
||||
def get_by_task_id(self, task_id: str) -> list[SARIFBroadcastDetail]:
|
||||
"""Retrieve all SARIF objects for a specific task.
|
||||
|
||||
Args:
|
||||
task_id: Task ID
|
||||
|
||||
Returns:
|
||||
List of SARIF broadcast details for this task
|
||||
|
||||
"""
|
||||
key = self._get_key(task_id)
|
||||
sarif_list = self.redis.lrange(key, 0, -1)
|
||||
@@ -118,14 +120,14 @@ class SARIFStore:
|
||||
return result
|
||||
|
||||
def delete_by_task_id(self, task_id: str) -> int:
|
||||
"""
|
||||
Remove all SARIF objects for a specific task.
|
||||
"""Remove all SARIF objects for a specific task.
|
||||
|
||||
Args:
|
||||
task_id: Task ID
|
||||
|
||||
Returns:
|
||||
Number of removed keys (0 or 1)
|
||||
|
||||
"""
|
||||
key = self._get_key(task_id)
|
||||
return self.redis.delete(key)
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"""
|
||||
Utility script for SARIF storage and retrieval operations.
|
||||
"""
|
||||
"""Utility script for SARIF storage and retrieval operations."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.sarif_store import SARIFStore
|
||||
|
||||
|
||||
def list_all_sarifs(redis_url: str, verbose: bool = False) -> None:
|
||||
"""
|
||||
List all SARIF objects in the database.
|
||||
"""List all SARIF objects in the database.
|
||||
|
||||
Args:
|
||||
redis_url: Redis URL
|
||||
verbose: Whether to print the full SARIF object
|
||||
|
||||
"""
|
||||
redis_client = Redis.from_url(redis_url)
|
||||
sarif_store = SARIFStore(redis_client)
|
||||
@@ -32,13 +31,13 @@ def list_all_sarifs(redis_url: str, verbose: bool = False) -> None:
|
||||
|
||||
|
||||
def list_task_sarifs(redis_url: str, task_id: str, verbose: bool = False) -> None:
|
||||
"""
|
||||
List all SARIF objects for a specific task.
|
||||
"""List all SARIF objects for a specific task.
|
||||
|
||||
Args:
|
||||
redis_url: Redis URL
|
||||
task_id: Task ID
|
||||
verbose: Whether to print the full SARIF object
|
||||
|
||||
"""
|
||||
redis_client = Redis.from_url(redis_url)
|
||||
sarif_store = SARIFStore(redis_client)
|
||||
@@ -55,12 +54,12 @@ def list_task_sarifs(redis_url: str, task_id: str, verbose: bool = False) -> Non
|
||||
|
||||
|
||||
def delete_task_sarifs(redis_url: str, task_id: str) -> None:
|
||||
"""
|
||||
Delete all SARIF objects for a specific task.
|
||||
"""Delete all SARIF objects for a specific task.
|
||||
|
||||
Args:
|
||||
redis_url: Redis URL
|
||||
task_id: Task ID
|
||||
|
||||
"""
|
||||
redis_client = Redis.from_url(redis_url)
|
||||
sarif_store = SARIFStore(redis_client)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from typing import Iterator
|
||||
import json
|
||||
import random
|
||||
from collections.abc import Generator, Iterator
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
|
||||
from bson.json_util import CANONICAL_JSON_OPTIONS, dumps
|
||||
from redis import Redis
|
||||
from redis.exceptions import ResponseError
|
||||
from bson.json_util import dumps, CANONICAL_JSON_OPTIONS
|
||||
from contextlib import contextmanager
|
||||
import random
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from typing import Generator
|
||||
|
||||
# Import POVReproduceRequest for the refactored PoVReproduceStatus
|
||||
from buttercup.common.datastructures.msg_pb2 import POVReproduceRequest, POVReproduceResponse
|
||||
@@ -64,7 +64,7 @@ class RedisLock:
|
||||
@contextmanager
|
||||
def acquire(self) -> Generator[None, None, None]:
|
||||
if not self.redis.set(self.key, "1", ex=self.lock_timeout_seconds, nx=True):
|
||||
raise FailedToAcquireLock()
|
||||
raise FailedToAcquireLock
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
@@ -116,6 +116,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
False if mitigated (didn't crash), True if non-mitigated (did crash), None if not in final states
|
||||
|
||||
"""
|
||||
pipeline = self.redis.pipeline()
|
||||
pipeline.sismember(POV_REPRODUCE_MITIGATED_SET_NAME, key)
|
||||
@@ -124,10 +125,9 @@ class PoVReproduceStatus:
|
||||
|
||||
if result[0]:
|
||||
return False # Mitigated - didn't crash
|
||||
elif result[1]:
|
||||
if result[1]:
|
||||
return True # Non-mitigated - did crash
|
||||
else:
|
||||
return None # Not in final states
|
||||
return None # Not in final states
|
||||
|
||||
def _make_key(self, request: POVReproduceRequest) -> str:
|
||||
"""Create a unique key from a POVReproduceRequest by serializing it to string."""
|
||||
@@ -144,6 +144,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
None if pending, POVReproduceResponse if completed
|
||||
|
||||
"""
|
||||
key = self._make_key(request)
|
||||
|
||||
@@ -161,13 +162,13 @@ class PoVReproduceStatus:
|
||||
|
||||
if result[0]:
|
||||
return None # Pending
|
||||
elif result[1]:
|
||||
if result[1]:
|
||||
return POVReproduceResponse(request=request, did_crash=False) # Completed and mitigated
|
||||
elif result[2]:
|
||||
if result[2]:
|
||||
return POVReproduceResponse(request=request, did_crash=True) # Completed and not mitigated
|
||||
else: # First time, schedule it for testing
|
||||
self.redis.sadd(POV_REPRODUCE_PENDING_SET_NAME, key)
|
||||
return None
|
||||
# First time, schedule it for testing
|
||||
self.redis.sadd(POV_REPRODUCE_PENDING_SET_NAME, key)
|
||||
return None
|
||||
|
||||
def mark_mitigated(self, request: POVReproduceRequest) -> bool:
|
||||
"""Mark a POV reproduction as mitigated (patch successfully prevented the crash).
|
||||
@@ -177,6 +178,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
True if the item was moved from pending to mitigated, False if item wasn't pending.
|
||||
|
||||
"""
|
||||
key = self._make_key(request)
|
||||
moved_count = self.redis.smove(POV_REPRODUCE_PENDING_SET_NAME, POV_REPRODUCE_MITIGATED_SET_NAME, key)
|
||||
@@ -190,6 +192,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
True if the item was moved from pending to non-mitigated, False if item wasn't pending.
|
||||
|
||||
"""
|
||||
key = self._make_key(request)
|
||||
moved_count = self.redis.smove(POV_REPRODUCE_PENDING_SET_NAME, POV_REPRODUCE_NON_MITIGATED_SET_NAME, key)
|
||||
@@ -203,6 +206,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
True if the item was moved from pending to expired, False if item wasn't pending.
|
||||
|
||||
"""
|
||||
# NOTE: This function isn't strictly needed. We could just have keys expire after a certain time.
|
||||
# However, this allows us to track which items have expired.
|
||||
@@ -215,6 +219,7 @@ class PoVReproduceStatus:
|
||||
|
||||
Returns:
|
||||
POVReproduceRequest if one is available, None otherwise
|
||||
|
||||
"""
|
||||
pending_set = self.redis.smembers(POV_REPRODUCE_PENDING_SET_NAME)
|
||||
if len(pending_set) == 0:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import re
|
||||
from buttercup.common.clusterfuzz_parser import StackParser, CrashInfo
|
||||
import logging
|
||||
from buttercup.common.sets import RedisSet
|
||||
import re
|
||||
|
||||
from bson.json_util import CANONICAL_JSON_OPTIONS, dumps
|
||||
from redis import Redis
|
||||
from bson.json_util import dumps, CANONICAL_JSON_OPTIONS
|
||||
|
||||
from buttercup.common.clusterfuzz_parser import CrashInfo, StackParser
|
||||
from buttercup.common.sets import RedisSet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass, asdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict, dataclass
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -27,6 +27,7 @@ class TaskMeta:
|
||||
Raises:
|
||||
FileNotFoundError: If the file doesn't exist
|
||||
JSONDecodeError: If the file contains invalid JSON
|
||||
|
||||
"""
|
||||
path = Path(directory) / cls.METADATA_FILENAME
|
||||
with path.open() as f:
|
||||
@@ -38,6 +39,7 @@ class TaskMeta:
|
||||
|
||||
Args:
|
||||
directory: Directory where to save the metadata file
|
||||
|
||||
"""
|
||||
path = Path(directory) / self.METADATA_FILENAME
|
||||
with path.open("w") as f:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from functools import lru_cache
|
||||
from buttercup.common.datastructures.msg_pb2 import Task
|
||||
from redis import Redis
|
||||
from buttercup.common.queues import HashNames
|
||||
from dataclasses import dataclass
|
||||
from google.protobuf import text_format
|
||||
import builtins
|
||||
import time
|
||||
from typing import Set, Iterator
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
from google.protobuf import text_format
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import Task
|
||||
from buttercup.common.queues import HashNames
|
||||
|
||||
# Redis set keys for tracking task states
|
||||
CANCELLED_TASKS_SET = "cancelled_tasks"
|
||||
@@ -68,6 +71,7 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
Task object if found, None otherwise
|
||||
|
||||
"""
|
||||
prepared_key = self._prepare_key(task_id)
|
||||
task_bytes = self.redis.hget(self.hash_name, prepared_key)
|
||||
@@ -87,6 +91,7 @@ class TaskRegistry:
|
||||
|
||||
Args:
|
||||
task_id: The ID of the task to delete
|
||||
|
||||
"""
|
||||
prepared_key = self._prepare_key(task_id)
|
||||
|
||||
@@ -104,6 +109,7 @@ class TaskRegistry:
|
||||
|
||||
Args:
|
||||
task_or_id: Either a Task object or a task ID string to be added to the cancelled set
|
||||
|
||||
"""
|
||||
# Extract task_id based on the type of input
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -119,6 +125,7 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
True if the task is in the cancelled tasks set, False otherwise
|
||||
|
||||
"""
|
||||
# Get task_id
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -140,6 +147,7 @@ class TaskRegistry:
|
||||
Returns:
|
||||
True if the task is expired (deadline has passed), False otherwise.
|
||||
Returns False if the task doesn't exist.
|
||||
|
||||
"""
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
@@ -166,6 +174,7 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
list[Task]: List of active tasks
|
||||
|
||||
"""
|
||||
# Iterate through all tasks, filtering out cancelled and expired ones
|
||||
# The cancelled flag is already set correctly by the __iter__ method
|
||||
@@ -176,13 +185,14 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
list[str]: List of task IDs that are in the cancelled tasks set
|
||||
|
||||
"""
|
||||
# Get all cancelled task IDs from the Redis set
|
||||
cancelled_ids = self.redis.smembers(CANCELLED_TASKS_SET)
|
||||
# Decode bytes to strings if needed
|
||||
return [task_id.decode("utf-8") if isinstance(task_id, bytes) else task_id for task_id in cancelled_ids]
|
||||
|
||||
def should_stop_processing(self, task_or_id: str | Task, cancelled_ids: Set[str] | None = None) -> bool:
|
||||
def should_stop_processing(self, task_or_id: str | Task, cancelled_ids: builtins.set[str] | None = None) -> bool:
|
||||
"""Check if a task should no longer be processed due to cancellation or expiration.
|
||||
|
||||
Args:
|
||||
@@ -193,8 +203,8 @@ class TaskRegistry:
|
||||
Returns:
|
||||
bool: True if the task should not be processed (is cancelled or expired),
|
||||
False otherwise
|
||||
"""
|
||||
|
||||
"""
|
||||
# Extract task_id for cancelled IDs check
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
|
||||
@@ -223,6 +233,7 @@ class TaskRegistry:
|
||||
|
||||
Args:
|
||||
task_or_id: Either a Task object or a task ID string to be added to the successful set
|
||||
|
||||
"""
|
||||
# Extract task_id based on the type of input
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -238,6 +249,7 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
True if the task is in the successful tasks set, False otherwise
|
||||
|
||||
"""
|
||||
# Get task_id
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -255,6 +267,7 @@ class TaskRegistry:
|
||||
|
||||
Args:
|
||||
task_or_id: Either a Task object or a task ID string to be added to the errored set
|
||||
|
||||
"""
|
||||
# Extract task_id based on the type of input
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -270,6 +283,7 @@ class TaskRegistry:
|
||||
|
||||
Returns:
|
||||
True if the task is in the errored tasks set, False otherwise
|
||||
|
||||
"""
|
||||
# Get task_id
|
||||
task_id = task_or_id.task_id if isinstance(task_or_id, Task) else task_or_id
|
||||
@@ -282,20 +296,22 @@ class TaskRegistry:
|
||||
|
||||
def task_registry_cli() -> None:
|
||||
"""CLI for the task registry"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class TaskRegistrySettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BUTTERCUP_TASK_REGISTRY_",
|
||||
env_file=".env",
|
||||
cli_parse_args=True,
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
redis_url: Annotated[str, Field(default="redis://localhost:6379", description="Redis URL")]
|
||||
|
||||
class Config:
|
||||
env_prefix = "BUTTERCUP_TASK_REGISTRY_"
|
||||
env_file = ".env"
|
||||
cli_parse_args = True
|
||||
extra = "allow"
|
||||
|
||||
settings = TaskRegistrySettings()
|
||||
settings = TaskRegistrySettings() # type: ignore[call-arg]
|
||||
redis = Redis.from_url(settings.redis_url, decode_responses=False)
|
||||
registry = TaskRegistry(redis)
|
||||
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import os
|
||||
import logging
|
||||
from enum import Enum
|
||||
import os
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import openlit
|
||||
import opentelemetry.attributes
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import Span, Tracer, Status, StatusCode
|
||||
from langchain_core.prompt_values import ChatPromptValue
|
||||
|
||||
# Monkey patch the _clean_attribute function to handle ChatPromptValue
|
||||
_clean_attribute_orig = opentelemetry.attributes._clean_attribute
|
||||
|
||||
|
||||
def _clean_attribute_wrapper(key: str, value: Any, max_len: int | None = None) -> Any:
|
||||
"""Wrapper around _clean_attribute to add custom behavior"""
|
||||
if isinstance(value, ChatPromptValue):
|
||||
value = value.to_string()
|
||||
|
||||
return _clean_attribute_orig(key, value, max_len)
|
||||
|
||||
|
||||
opentelemetry.attributes._clean_attribute = _clean_attribute_wrapper
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import openlit
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import Span, Status, StatusCode, Tracer
|
||||
|
||||
_opentelemetry_enabled = True
|
||||
except ImportError:
|
||||
logger.warning("OpenTelemetry is not installed, skipping telemetry")
|
||||
|
||||
Span: type = Any
|
||||
Tracer: type = Any
|
||||
|
||||
_opentelemetry_enabled = False
|
||||
|
||||
crs_instance_id = os.getenv("CRS_INSTANCE_ID", str(uuid.uuid4()))
|
||||
service_instance_id = str(uuid.uuid4())
|
||||
|
||||
@@ -47,6 +41,9 @@ class CRSActionCategory(Enum):
|
||||
|
||||
def init_telemetry(application_name: str) -> None:
|
||||
"""Initialize the telemetry for the application."""
|
||||
if not _opentelemetry_enabled:
|
||||
return
|
||||
|
||||
logger.info("Initializing telemetry for %s", application_name)
|
||||
if not os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
||||
logger.error("OTEL_EXPORTER_OTLP_ENDPOINT not set, disabling telemetry")
|
||||
@@ -75,6 +72,9 @@ def set_crs_attributes(
|
||||
task_metadata: dict,
|
||||
extra_attributes: dict | None = None,
|
||||
) -> None:
|
||||
if not _opentelemetry_enabled:
|
||||
return
|
||||
|
||||
extra_attributes = extra_attributes or {}
|
||||
span.set_attribute("crs.action.category", crs_action_category.value)
|
||||
span.set_attribute("crs.action.name", crs_action_name)
|
||||
@@ -95,6 +95,9 @@ def log_crs_action_ok(
|
||||
task_metadata: dict,
|
||||
extra_attributes: dict | None = None,
|
||||
) -> None:
|
||||
if not _opentelemetry_enabled:
|
||||
return
|
||||
|
||||
extra_attributes = extra_attributes or {}
|
||||
with tracer.start_as_current_span(crs_action_name) as span:
|
||||
span.set_attribute("crs.action.category", crs_action_category.value)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class FuzzConfiguration:
|
||||
corpus_dir: str
|
||||
target_path: str
|
||||
engine: str
|
||||
sanitizer: str
|
||||
@@ -1,26 +1,29 @@
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from uuid import uuid4
|
||||
|
||||
from google.protobuf.text_format import Parse
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, CliPositionalArg, CliSubCommand, SettingsConfigDict, get_subcommand
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import (
|
||||
BuildOutput,
|
||||
BuildType,
|
||||
SubmissionEntry,
|
||||
SubmissionResult,
|
||||
WeightedHarness,
|
||||
)
|
||||
from buttercup.common.logger import setup_package_logger
|
||||
from buttercup.common.maps import (
|
||||
BuildMap,
|
||||
HarnessWeights,
|
||||
)
|
||||
from buttercup.common.datastructures.msg_pb2 import (
|
||||
BuildOutput,
|
||||
BuildType,
|
||||
WeightedHarness,
|
||||
SubmissionEntry,
|
||||
SubmissionResult,
|
||||
)
|
||||
from buttercup.common.queues import QueueFactory, QueueNames, ReliableQueue
|
||||
from buttercup.common.task_registry import TaskRegistry
|
||||
from uuid import uuid4
|
||||
from redis import Redis
|
||||
from pydantic_settings import BaseSettings, CliSubCommand, CliPositionalArg, get_subcommand
|
||||
from pydantic import BaseModel
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
from pathlib import Path
|
||||
from google.protobuf.text_format import Parse
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -95,6 +98,7 @@ class ReadBuildsSettings(BaseModel):
|
||||
|
||||
|
||||
class ReadSubmissionsSettings(BaseModel):
|
||||
task_id: str = Field(default="", description="Task ID")
|
||||
verbose: bool = Field(False, description="Show full stacktraces instead of truncated versions")
|
||||
filter_stop: bool = Field(False, description="Filter out submissions that are stopped")
|
||||
|
||||
@@ -112,7 +116,26 @@ class DeleteSettings(BaseModel):
|
||||
item_id: Annotated[str | None, Field(description="Item ID")] = None
|
||||
|
||||
|
||||
class ExtractPovsSettings(BaseModel):
|
||||
output_dir: CliPositionalArg[Path] = Field(
|
||||
description="Output directory for extracted PoVs, stack traces, and patches"
|
||||
)
|
||||
task_id: str = Field(default="", description="Filter by task ID (optional)")
|
||||
passed_only: bool = Field(default=False, description="Only extract vulnerabilities with PASSED PoV result")
|
||||
namespace: str = Field(default="crs", description="Kubernetes namespace")
|
||||
pod_label: str = Field(default="app=scheduler", description="Label selector for pod to copy files from")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="BUTTERCUP_MSG_PUBLISHER_",
|
||||
env_file=".env",
|
||||
cli_parse_args=True,
|
||||
nested_model_default_partial_update=True,
|
||||
env_nested_delimiter="__",
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
redis_url: Annotated[str, Field(default="redis://localhost:6379", description="Redis URL")]
|
||||
log_level: Annotated[str, Field(default="info", description="Log level")]
|
||||
send_queue: CliSubCommand[SendSettings]
|
||||
@@ -124,14 +147,210 @@ class Settings(BaseSettings):
|
||||
read_harnesses: CliSubCommand[ReadHarnessWeightSettings]
|
||||
read_builds: CliSubCommand[ReadBuildsSettings]
|
||||
read_submissions: CliSubCommand[ReadSubmissionsSettings]
|
||||
extract_povs: CliSubCommand[ExtractPovsSettings]
|
||||
|
||||
class Config:
|
||||
env_prefix = "BUTTERCUP_MSG_PUBLISHER_"
|
||||
env_file = ".env"
|
||||
cli_parse_args = True
|
||||
nested_model_default_partial_update = True
|
||||
env_nested_delimiter = "__"
|
||||
extra = "allow"
|
||||
|
||||
def get_pod_name(namespace: str, label: str) -> str | None:
|
||||
"""Get the name of a pod matching the label selector."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["kubectl", "get", "pods", "-n", namespace, "-l", label, "-o", "jsonpath={.items[0].metadata.name}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
pod_name = result.stdout.strip()
|
||||
return pod_name if pod_name else None
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to get pod name: {e.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
def kubectl_cp(namespace: str, pod_name: str, remote_path: str, local_path: Path) -> bool:
|
||||
"""Copy a file from a pod using kubectl cp."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["kubectl", "cp", "-n", namespace, f"{pod_name}:{remote_path}", str(local_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"kubectl cp failed for {remote_path}: {result.stderr}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"kubectl cp exception for {remote_path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def extract_povs(redis: Redis, command: ExtractPovsSettings) -> None:
|
||||
"""Extract PoVs, stack traces, and patches into a directory structure.
|
||||
|
||||
Directory structure:
|
||||
output_dir/
|
||||
project_name/
|
||||
task_id/
|
||||
vuln_NNN/
|
||||
crashes/
|
||||
crash_001/
|
||||
pov.bin
|
||||
stacktrace.txt
|
||||
tracer_stacktrace.txt
|
||||
metadata.json
|
||||
patches/
|
||||
patch_001.patch
|
||||
patch_002.patch
|
||||
metadata.json
|
||||
"""
|
||||
SUBMISSIONS_KEY = "submissions"
|
||||
raw_submissions: list = redis.lrange(SUBMISSIONS_KEY, 0, -1)
|
||||
registry = TaskRegistry(redis)
|
||||
|
||||
if not raw_submissions:
|
||||
logger.info("No submissions found")
|
||||
return
|
||||
|
||||
# Get pod name for kubectl cp
|
||||
pod_name = get_pod_name(command.namespace, command.pod_label)
|
||||
if not pod_name:
|
||||
logger.error(f"No pod found matching label '{command.pod_label}' in namespace '{command.namespace}'")
|
||||
return
|
||||
|
||||
logger.info(f"Using pod '{pod_name}' for file extraction")
|
||||
|
||||
output_dir = command.output_dir.resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"Found {len(raw_submissions)} submissions, extracting to {output_dir}")
|
||||
|
||||
vuln_counter: dict[str, int] = {} # task_id -> vulnerability counter
|
||||
stats = {"povs_copied": 0, "povs_failed": 0}
|
||||
|
||||
for i, raw in enumerate(raw_submissions):
|
||||
try:
|
||||
submission = SubmissionEntry.FromString(raw)
|
||||
|
||||
if submission.stop:
|
||||
logger.debug(f"Skipping stopped submission {i}")
|
||||
continue
|
||||
|
||||
if not submission.crashes:
|
||||
logger.debug(f"Skipping submission {i} with no crashes")
|
||||
continue
|
||||
|
||||
# Get task info from the first crash
|
||||
first_crash = submission.crashes[0].crash.crash
|
||||
task_id = first_crash.target.task_id
|
||||
|
||||
if command.task_id and task_id != command.task_id:
|
||||
logger.debug(f"Skipping submission {i} for task {task_id}")
|
||||
continue
|
||||
|
||||
# Check if any crash passed (if passed_only filter is set)
|
||||
if command.passed_only:
|
||||
has_passed = any(c.result == SubmissionResult.PASSED for c in submission.crashes)
|
||||
if not has_passed:
|
||||
logger.debug(f"Skipping submission {i} - no PASSED crashes")
|
||||
continue
|
||||
|
||||
# Get task metadata
|
||||
task = registry.get(task_id)
|
||||
project_name = task.project_name if task else "unknown"
|
||||
|
||||
# Create vulnerability directory
|
||||
if task_id not in vuln_counter:
|
||||
vuln_counter[task_id] = 0
|
||||
vuln_counter[task_id] += 1
|
||||
vuln_num = vuln_counter[task_id]
|
||||
|
||||
vuln_dir = output_dir / project_name / task_id / f"vuln_{vuln_num:03d}"
|
||||
crashes_dir = vuln_dir / "crashes"
|
||||
patches_dir = vuln_dir / "patches"
|
||||
|
||||
crashes_dir.mkdir(parents=True, exist_ok=True)
|
||||
patches_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Extract crashes
|
||||
for crash_idx, crash_with_id in enumerate(submission.crashes, start=1):
|
||||
crash = crash_with_id.crash
|
||||
crash_dir = crashes_dir / f"crash_{crash_idx:03d}"
|
||||
crash_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy PoV file using kubectl cp
|
||||
pov_remote_path = crash.crash.crash_input_path
|
||||
pov_local_path = crash_dir / "pov.bin"
|
||||
|
||||
if kubectl_cp(command.namespace, pod_name, pov_remote_path, pov_local_path):
|
||||
stats["povs_copied"] += 1
|
||||
else:
|
||||
stats["povs_failed"] += 1
|
||||
# Store the path for reference
|
||||
(crash_dir / "pov_path.txt").write_text(pov_remote_path)
|
||||
|
||||
# Write stacktrace
|
||||
if crash.crash.stacktrace:
|
||||
(crash_dir / "stacktrace.txt").write_text(crash.crash.stacktrace)
|
||||
|
||||
# Write tracer stacktrace
|
||||
if crash.tracer_stacktrace:
|
||||
(crash_dir / "tracer_stacktrace.txt").write_text(crash.tracer_stacktrace)
|
||||
|
||||
# Write crash metadata
|
||||
crash_metadata = {
|
||||
"competition_pov_id": crash_with_id.competition_pov_id,
|
||||
"result": SubmissionResult.Name(crash_with_id.result) if crash_with_id.result else "NONE",
|
||||
"harness_name": crash.crash.harness_name,
|
||||
"crash_token": crash.crash.crash_token,
|
||||
"crash_input_path": crash.crash.crash_input_path,
|
||||
"sanitizer": crash.crash.target.sanitizer,
|
||||
"engine": crash.crash.target.engine,
|
||||
}
|
||||
(crash_dir / "metadata.json").write_text(json.dumps(crash_metadata, indent=2))
|
||||
|
||||
# Extract patches (skip empty patch trackers - these are placeholders that never received content)
|
||||
patch_num = 0
|
||||
for patch_entry in submission.patches:
|
||||
if not patch_entry.patch:
|
||||
continue # Skip empty patch trackers
|
||||
patch_num += 1
|
||||
patch_file = patches_dir / f"patch_{patch_num:03d}.patch"
|
||||
patch_file.write_text(patch_entry.patch)
|
||||
|
||||
# Write patch metadata
|
||||
patch_metadata = {
|
||||
"internal_patch_id": patch_entry.internal_patch_id,
|
||||
"competition_patch_id": patch_entry.competition_patch_id,
|
||||
"result": SubmissionResult.Name(patch_entry.result) if patch_entry.result else "NONE",
|
||||
}
|
||||
(patches_dir / f"patch_{patch_num:03d}_metadata.json").write_text(json.dumps(patch_metadata, indent=2))
|
||||
|
||||
# Write vulnerability metadata
|
||||
vuln_metadata = {
|
||||
"task_id": task_id,
|
||||
"project_name": project_name,
|
||||
"num_crashes": len(submission.crashes),
|
||||
"num_patches": patch_num, # Only count non-empty patches
|
||||
"num_bundles": len(submission.bundles),
|
||||
"patch_idx": submission.patch_idx,
|
||||
"stopped": submission.stop,
|
||||
}
|
||||
(vuln_dir / "metadata.json").write_text(json.dumps(vuln_metadata, indent=2))
|
||||
|
||||
logger.info(
|
||||
f"Extracted vulnerability {vuln_num} for {project_name}/{task_id}: "
|
||||
f"{len(submission.crashes)} crashes, {patch_num} patches"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process submission {i}: {e}")
|
||||
continue
|
||||
|
||||
# Print summary
|
||||
total_vulns = sum(vuln_counter.values())
|
||||
logger.info(f"Extraction complete: {total_vulns} vulnerabilities across {len(vuln_counter)} tasks")
|
||||
logger.info(f"PoV files: {stats['povs_copied']} copied, {stats['povs_failed']} failed")
|
||||
for task_id, count in vuln_counter.items():
|
||||
logger.info(f" {task_id}: {count} vulnerabilities")
|
||||
|
||||
|
||||
def handle_subcommand(redis: Redis, command: BaseModel | None) -> None:
|
||||
@@ -227,9 +446,15 @@ def handle_subcommand(redis: Redis, command: BaseModel | None) -> None:
|
||||
logger.error(f"Task {task_id} not found in registry")
|
||||
continue
|
||||
|
||||
if command.task_id and task_id != command.task_id:
|
||||
logger.info(f"Skipping submission {i} for task {task_id} (not {command.task_id})")
|
||||
continue
|
||||
|
||||
if task_id not in result:
|
||||
result[task_id] = TaskResult(
|
||||
task_id=task_id, project_name=task.project_name, mode=str(task.task_type)
|
||||
task_id=task_id,
|
||||
project_name=task.project_name,
|
||||
mode=str(task.task_type),
|
||||
)
|
||||
c = next((c for c in submission.crashes if c.result == SubmissionResult.PASSED), None)
|
||||
if c:
|
||||
@@ -240,9 +465,8 @@ def handle_subcommand(redis: Redis, command: BaseModel | None) -> None:
|
||||
result[task_id].n_patches += 1
|
||||
assert c is not None
|
||||
result[task_id].patched_vulnerabilities.append(c.competition_pov_id)
|
||||
else:
|
||||
if c:
|
||||
result[task_id].non_patched_vulnerabilities.append(c.competition_pov_id)
|
||||
elif c:
|
||||
result[task_id].non_patched_vulnerabilities.append(c.competition_pov_id)
|
||||
|
||||
b = next((b for b in submission.bundles), None)
|
||||
if b:
|
||||
@@ -297,13 +521,15 @@ def handle_subcommand(redis: Redis, command: BaseModel | None) -> None:
|
||||
|
||||
print()
|
||||
logger.info("Done")
|
||||
elif isinstance(command, ExtractPovsSettings):
|
||||
extract_povs(redis, command)
|
||||
elif isinstance(command, ListSettings):
|
||||
print("Available queues:")
|
||||
print("\n".join([f"- {name}" for name in get_queue_names()]))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = Settings()
|
||||
settings = Settings() # type: ignore[call-arg]
|
||||
setup_package_logger("util-cli", __name__, settings.log_level)
|
||||
|
||||
redis = Redis.from_url(settings.redis_url, decode_responses=False)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import shutil
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
from pathlib import Path
|
||||
from os import PathLike
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +25,9 @@ def copyanything(src: PathLike, dst: PathLike, **kwargs: Any) -> None:
|
||||
"""
|
||||
src, dst = Path(src), Path(dst)
|
||||
try:
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True, **kwargs)
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True, ignore_dangling_symlinks=True, **kwargs)
|
||||
except shutil.Error:
|
||||
logger.exception(f"Some errors occurred while copying {src} to {dst}, continuing anyway...")
|
||||
except OSError as exc: # python >2.5
|
||||
if exc.errno in (errno.ENOTDIR, errno.EINVAL):
|
||||
shutil.copy(src, dst)
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from dirty_equals import IsStr
|
||||
import subprocess
|
||||
import os
|
||||
import base64
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsStr
|
||||
|
||||
from buttercup.common.challenge_task import (
|
||||
ChallengeTask,
|
||||
ChallengeTaskError,
|
||||
ReproduceResult,
|
||||
CommandResult,
|
||||
ReproduceResult,
|
||||
)
|
||||
from buttercup.common.task_meta import TaskMeta
|
||||
import tempfile
|
||||
|
||||
# ruff: noqa: E501, W291
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -105,8 +110,10 @@ def get_mock_popen(returncode: int, stdout: list[bytes], stderr: list[bytes]):
|
||||
mock_process.poll.side_effect = [None, None, returncode]
|
||||
mock_process.wait.return_value = returncode
|
||||
|
||||
# Make Popen return our mock process
|
||||
# Make Popen return our mock process and work as context manager
|
||||
mock_popen.return_value = mock_process
|
||||
mock_popen.return_value.__enter__ = MagicMock(return_value=mock_process)
|
||||
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
||||
yield mock_popen
|
||||
|
||||
|
||||
@@ -373,7 +380,7 @@ def libjpeg_oss_fuzz_task_dir(tmp_path: Path) -> Path:
|
||||
metadata={"task_id": "task-id-libjpeg-turbo", "round_id": "testing", "team_id": "tob"},
|
||||
).save(tmp_path)
|
||||
|
||||
yield tmp_path
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -457,6 +464,121 @@ def test_real_reproduce_pov(libjpeg_oss_fuzz_task_rw: ChallengeTask, libjpeg_cra
|
||||
assert result.did_crash(), "Reproduce POV failed"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize(
|
||||
"oss_fuzz_commit,description",
|
||||
[
|
||||
("eb47f56bd15626ab4ae8727bc435148dd2f9857c^", "before_bug"),
|
||||
("eb47f56bd15626ab4ae8727bc435148dd2f9857c", "bug_introduced"),
|
||||
("HEAD", "current_head"),
|
||||
],
|
||||
)
|
||||
def test_helper_py_patching_across_commits(
|
||||
tmp_path: Path, libjpeg_crash_testcase: Path, oss_fuzz_commit: str, description: str
|
||||
):
|
||||
"""Test helper.py patching across different OSS-Fuzz commits.
|
||||
|
||||
This test validates that helper.py patching works correctly across different
|
||||
OSS-Fuzz commits:
|
||||
- Before the bug: eb47f56^ (should work without needing reproduce_impl patch)
|
||||
- Bug introduced: eb47f56bd15626ab4ae8727bc435148dd2f9857c (needs patching)
|
||||
- Current HEAD: Latest OSS-Fuzz (needs patching)
|
||||
|
||||
The patching should ensure reproduce_pov runs successfully on all commits.
|
||||
"""
|
||||
# Detect current architecture
|
||||
current_arch = platform.machine()
|
||||
# Normalize architecture names
|
||||
if current_arch in ("arm64", "aarch64"):
|
||||
current_arch = "aarch64"
|
||||
elif current_arch in ("x86_64", "amd64"):
|
||||
current_arch = "x86_64"
|
||||
|
||||
# Only test ARM64 on ARM64 platforms, x86_64 on x86_64 platforms
|
||||
# For this test we use the current architecture
|
||||
|
||||
# Create task directory structure
|
||||
task_dir = tmp_path / f"libjpeg-turbo-{description}"
|
||||
task_dir.mkdir(parents=True)
|
||||
|
||||
oss_fuzz_dir = task_dir / "fuzz-tooling"
|
||||
oss_fuzz_dir.mkdir(parents=True)
|
||||
source_dir = task_dir / "src"
|
||||
source_dir.mkdir(parents=True)
|
||||
|
||||
# Clone OSS-Fuzz at specific commit
|
||||
subprocess.run(["git", "-C", str(oss_fuzz_dir), "clone", "https://github.com/google/oss-fuzz.git"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(oss_fuzz_dir / "oss-fuzz"), "checkout", oss_fuzz_commit],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Clone libjpeg-turbo source (same commit as existing test)
|
||||
libjpeg_url = "https://github.com/libjpeg-turbo/libjpeg-turbo"
|
||||
subprocess.run(["git", "-C", str(source_dir), "clone", libjpeg_url], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(source_dir / "libjpeg-turbo"), "checkout", "6d91e950c871103a11bac2f10c63bf998796c719"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create task metadata
|
||||
TaskMeta(
|
||||
project_name="libjpeg-turbo",
|
||||
focus="libjpeg-turbo",
|
||||
task_id=f"task-id-libjpeg-turbo-{description}",
|
||||
metadata={"task_id": f"task-id-libjpeg-turbo-{description}", "round_id": "testing", "team_id": "tob"},
|
||||
).save(task_dir)
|
||||
|
||||
# Create challenge task
|
||||
task = ChallengeTask(
|
||||
read_only_task_dir=task_dir,
|
||||
)
|
||||
|
||||
# Get RW copy and test
|
||||
with task.get_rw_copy(None) as local_task:
|
||||
# Build image and fuzzers
|
||||
result = local_task.build_image(pull_latest_base_image=False, architecture=current_arch)
|
||||
assert result.success is True, f"Build image failed: {result.error}"
|
||||
|
||||
result = local_task.build_fuzzers(engine="libfuzzer", sanitizer="address", architecture=current_arch)
|
||||
assert result.success is True, f"Build fuzzers failed: {result.error}"
|
||||
|
||||
# Reproduce the POV - this should work with patching (the fuzzer should run successfully)
|
||||
# Note: We're testing that reproduce_pov runs without errors, not necessarily that
|
||||
# this specific crash test case triggers a crash (compatibility may vary by version)
|
||||
result = local_task.reproduce_pov(
|
||||
fuzzer_name="libjpeg_turbo_fuzzer",
|
||||
crash_path=libjpeg_crash_testcase,
|
||||
)
|
||||
|
||||
# The key is that reproduce_pov should RUN successfully (did_run returns True)
|
||||
# This confirms the patching fixed the reproduce_impl issue
|
||||
assert result.did_run(), (
|
||||
f"Reproduce POV did not run for commit {oss_fuzz_commit} ({description}). "
|
||||
f"Patching may not have worked correctly. Output: {result.command_result.output[:500] if result.command_result.output else 'None'}"
|
||||
)
|
||||
|
||||
# Verify that helper.py was patched correctly
|
||||
helper_path = local_task.get_oss_fuzz_path() / "infra" / "helper.py"
|
||||
assert helper_path.exists(), "helper.py not found"
|
||||
|
||||
helper_content = helper_path.read_text()
|
||||
|
||||
# Check for the common fix: architecture=architecture instead of err_result
|
||||
# The fix should change: return run_function(run_args, err_result)
|
||||
# To: return run_function(run_args, architecture=architecture)
|
||||
assert "architecture=architecture" in helper_content, (
|
||||
f"helper.py patching failed: 'architecture=architecture' not found in helper.py for commit {oss_fuzz_commit}"
|
||||
)
|
||||
|
||||
# On ARM64, verify ARM64-specific patches were applied
|
||||
if current_arch == "aarch64":
|
||||
# Check for ARM64 manifest tags
|
||||
assert ":manifest-arm64v8" in helper_content, (
|
||||
f"helper.py patching failed: ARM64 manifest tags not found for commit {oss_fuzz_commit}"
|
||||
)
|
||||
|
||||
|
||||
def test_copy_task(challenge_task_readonly: ChallengeTask, mock_subprocess):
|
||||
"""Test copying a challenge task to a temporary directory."""
|
||||
with patch.object(ChallengeTask, "_check_python_path"):
|
||||
@@ -717,7 +839,10 @@ def mock_node_local(monkeypatch, tmp_path: Path):
|
||||
@patch("buttercup.common.node_local._get_root_path")
|
||||
@patch("buttercup.common.node_local.remote_archive_to_dir")
|
||||
def test_challenge_task_with_node_local_storage_existing(
|
||||
mock_remote_archive_to_dir, mock_get_root_path, mock_node_local_storage, task_dir
|
||||
mock_remote_archive_to_dir,
|
||||
mock_get_root_path,
|
||||
mock_node_local_storage,
|
||||
task_dir,
|
||||
):
|
||||
"""Test ChallengeTask behavior when using node_local and path exists."""
|
||||
mock_get_root_path.return_value = Path(mock_node_local_storage)
|
||||
@@ -770,7 +895,10 @@ def test_challenge_task_with_node_local_storage_existing(
|
||||
@patch("buttercup.common.node_local._get_root_path")
|
||||
@patch("buttercup.common.node_local.remote_archive_to_dir")
|
||||
def test_challenge_task_with_node_local_storage_download(
|
||||
mock_remote_archive_to_dir, mock_get_root_path, mock_node_local_storage, task_dir
|
||||
mock_remote_archive_to_dir,
|
||||
mock_get_root_path,
|
||||
mock_node_local_storage,
|
||||
task_dir,
|
||||
):
|
||||
"""Test ChallengeTask behavior when using node_local and path doesn't exist."""
|
||||
mock_get_root_path.return_value = Path(mock_node_local_storage)
|
||||
@@ -866,7 +994,7 @@ def mock_node_local_storage(tmp_path: Path):
|
||||
metadata={"task_id": "task-id-challenge-task", "round_id": "testing", "team_id": "tob"},
|
||||
).save(local_task_path)
|
||||
|
||||
yield node_data_dir
|
||||
return node_data_dir
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -891,7 +1019,7 @@ def test_reproduce_result_stacktrace():
|
||||
# Test case 1: Short string (under 1MB limit)
|
||||
short_output = b"INFO: Seed: 12345\nRunning normally\n==ERROR: AddressSanitizer: heap-buffer-overflow"
|
||||
result1 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=short_output, error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=short_output, error=None),
|
||||
)
|
||||
stacktrace1 = result1.stacktrace()
|
||||
assert stacktrace1 is not None
|
||||
@@ -906,7 +1034,7 @@ def test_reproduce_result_stacktrace():
|
||||
)
|
||||
|
||||
result2 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=large_content, error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=large_content, error=None),
|
||||
)
|
||||
stacktrace2 = result2.stacktrace()
|
||||
assert stacktrace2 is not None
|
||||
@@ -938,7 +1066,7 @@ def test_reproduce_result_stacktrace():
|
||||
exact_size_content = prefix + b"B" * (MAX_OUTPUT_LEN - len(prefix) - len(suffix)) + suffix
|
||||
|
||||
result4 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=exact_size_content, error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=exact_size_content, error=None),
|
||||
)
|
||||
stacktrace4 = result4.stacktrace()
|
||||
assert stacktrace4 is not None
|
||||
@@ -954,7 +1082,7 @@ def test_reproduce_result_stacktrace():
|
||||
)
|
||||
|
||||
result5 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=over_limit_content, error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=over_limit_content, error=None),
|
||||
)
|
||||
stacktrace5 = result5.stacktrace()
|
||||
assert stacktrace5 is not None
|
||||
@@ -969,7 +1097,7 @@ def test_reproduce_result_stacktrace():
|
||||
)
|
||||
|
||||
result6 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=very_large_content, error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=very_large_content, error=None),
|
||||
)
|
||||
stacktrace6 = result6.stacktrace()
|
||||
assert stacktrace6 is not None
|
||||
@@ -989,15 +1117,18 @@ def test_reproduce_result_methods():
|
||||
# Test case 1: Successful run, no crash
|
||||
result1 = ReproduceResult(
|
||||
command_result=CommandResult(
|
||||
success=True, returncode=0, output=b"INFO: Seed: 12345\nRunning normally", error=None
|
||||
)
|
||||
success=True,
|
||||
returncode=0,
|
||||
output=b"INFO: Seed: 12345\nRunning normally",
|
||||
error=None,
|
||||
),
|
||||
)
|
||||
assert result1.did_run() is True
|
||||
assert result1.did_crash() is False
|
||||
|
||||
# Test case 2: Failed run, no crash (fuzzer didn't start)
|
||||
result2 = ReproduceResult(
|
||||
command_result=CommandResult(success=False, returncode=1, output=b"Error: Could not start fuzzer", error=None)
|
||||
command_result=CommandResult(success=False, returncode=1, output=b"Error: Could not start fuzzer", error=None),
|
||||
)
|
||||
assert result2.did_run() is False
|
||||
assert result2.did_crash() is False
|
||||
@@ -1009,7 +1140,7 @@ def test_reproduce_result_methods():
|
||||
returncode=1,
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n==ERROR: AddressSanitizer: heap-buffer-overflow",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result3.did_run() is True
|
||||
assert result3.did_crash() is True
|
||||
@@ -1021,7 +1152,7 @@ def test_reproduce_result_methods():
|
||||
returncode=1,
|
||||
output=None,
|
||||
error=b"INFO: Seed: 12345\nRunning normally\n==ERROR: AddressSanitizer: heap-buffer-overflow",
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result4.did_run() is True
|
||||
assert result4.did_crash() is True
|
||||
@@ -1029,8 +1160,11 @@ def test_reproduce_result_methods():
|
||||
# Test case 5: Run with None returncode
|
||||
result5 = ReproduceResult(
|
||||
command_result=CommandResult(
|
||||
success=False, returncode=None, output=b"INFO: Seed: 12345\nRunning normally", error=None
|
||||
)
|
||||
success=False,
|
||||
returncode=None,
|
||||
output=b"INFO: Seed: 12345\nRunning normally",
|
||||
error=None,
|
||||
),
|
||||
)
|
||||
assert result5.did_run() is True
|
||||
assert result5.did_crash() is False
|
||||
@@ -1042,7 +1176,7 @@ def test_reproduce_result_methods():
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n==ERROR: AddressSanitizer: heap-buffer-overflow\nTimeout occurred",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result6.did_run() is True
|
||||
assert result6.did_crash() is True # Should detect crash due to crash token in stacktrace
|
||||
@@ -1054,7 +1188,7 @@ def test_reproduce_result_methods():
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\nTimeout occurred\nNo crash detected",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result7.did_run() is True
|
||||
assert result7.did_crash() is False # Should not detect crash due to no crash token
|
||||
@@ -1102,7 +1236,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n" + output + b"\nTimeout occurred",
|
||||
error=b"",
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result8.did_run() is True
|
||||
assert result8.did_crash() is True # Should detect crash due to crash token in error output
|
||||
@@ -1114,7 +1248,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=None,
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result9.did_run() is False
|
||||
assert result9.did_crash() is False # Should not detect crash due to no output and no crash token
|
||||
@@ -1126,7 +1260,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nTimeout occurred",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result10.did_run() is True
|
||||
assert result10.did_crash() is False # Should not detect crash due to no crash token
|
||||
@@ -1138,7 +1272,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=201, # FAILURE_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n==ERROR: AddressSanitizer: heap-buffer-overflow",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result11.did_run() is True
|
||||
assert result11.did_crash() is False # Should not detect crash due to FAILURE_ERR_RESULT
|
||||
@@ -1150,7 +1284,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n" + output + b"\nTimeout occurred",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result12.did_run() is True
|
||||
assert result12.did_crash() is True # Should detect crash due to UBSan error in stacktrace
|
||||
@@ -1162,7 +1296,7 @@ subprocess command returned a non-zero exit status: 1"""
|
||||
returncode=124, # TIMEOUT_ERR_RESULT
|
||||
output=b"INFO: Seed: 12345\nRunning normally\n" + output + b"\n" + output + b"\nTimeout occurred",
|
||||
error=None,
|
||||
)
|
||||
),
|
||||
)
|
||||
assert result13.did_run() is True
|
||||
assert result13.did_crash() is True # Should detect crash due to multiple crash patterns in stacktrace
|
||||
@@ -1265,7 +1399,10 @@ def test_apply_patch_diff_git_apply_failure(challenge_task: ChallengeTask):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
# Simulate git apply failure
|
||||
mock_run.side_effect = subprocess.CalledProcessError(
|
||||
returncode=1, cmd=["patch", "-p1"], output="", stderr="patch does not apply"
|
||||
returncode=1,
|
||||
cmd=["patch", "-p1"],
|
||||
output="",
|
||||
stderr="patch does not apply",
|
||||
)
|
||||
|
||||
with pytest.raises(ChallengeTaskError, match="Error applying diff"):
|
||||
|
||||
+333
-3
@@ -1,10 +1,13 @@
|
||||
import pytest
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from buttercup.common.corpus import InputDir, Corpus
|
||||
|
||||
import pytest
|
||||
|
||||
from buttercup.common import node_local
|
||||
from buttercup.common.corpus import Corpus, InputDir, _get_corpus_storage_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -81,7 +84,7 @@ def test_local_corpus_size_with_mixed_exceptions(temp_dir, mock_node_local):
|
||||
if self.name == "test_file_2" and call_count[self.name] == 1:
|
||||
raise FileNotFoundError(f"Simulated transient error for {self.name}")
|
||||
# test_file_4 will always fail
|
||||
elif self.name == "test_file_4":
|
||||
if self.name == "test_file_4":
|
||||
raise PermissionError(f"Simulated permission error for {self.name}")
|
||||
# Other files work normally
|
||||
return original_lstat(self)
|
||||
@@ -225,3 +228,330 @@ def test_input_dir_copy_corpus_all_files_too_large(temp_dir, mock_node_local):
|
||||
# Should return empty list
|
||||
assert copied_files == []
|
||||
assert input_dir.local_corpus_count() == 0
|
||||
|
||||
|
||||
def test_copy_corpus_only_local(temp_dir):
|
||||
"""Test that copy_corpus copies only to node-local (not remote)."""
|
||||
remote_path = os.path.join(temp_dir, "remote")
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
input_dir = InputDir(temp_dir, "test_corpus")
|
||||
|
||||
src_dir = os.path.join(temp_dir, "src_corpus")
|
||||
os.makedirs(src_dir, exist_ok=True)
|
||||
|
||||
# Create a test file
|
||||
file_path = os.path.join(src_dir, "test_file")
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(b"test content")
|
||||
|
||||
copied_files = input_dir.copy_corpus(src_dir)
|
||||
|
||||
# File should exist locally
|
||||
assert len(copied_files) == 1
|
||||
assert os.path.exists(copied_files[0])
|
||||
|
||||
# Remote file should not exist
|
||||
remote_file = os.path.join(remote_path, os.path.basename(copied_files[0]))
|
||||
assert not os.path.exists(remote_file)
|
||||
|
||||
|
||||
def test_copy_file_only_local(temp_dir):
|
||||
"""Test that copy_file with only_local=True skips remote copy."""
|
||||
remote_path = os.path.join(temp_dir, "remote")
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
input_dir = InputDir(temp_dir, "test_corpus")
|
||||
|
||||
src_dir = os.path.join(temp_dir, "src_corpus")
|
||||
os.makedirs(src_dir, exist_ok=True)
|
||||
|
||||
# Create a test file
|
||||
file_path = os.path.join(src_dir, "test_file")
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(b"test content")
|
||||
|
||||
# Copy file with only_local=True
|
||||
dst = input_dir.copy_file(file_path, only_local=True)
|
||||
|
||||
# File should exist locally
|
||||
assert os.path.exists(dst)
|
||||
|
||||
# Remote file should not exist
|
||||
remote_file = os.path.join(remote_path, os.path.basename(dst))
|
||||
assert not os.path.exists(remote_file)
|
||||
|
||||
|
||||
def test_copy_file_with_remote(temp_dir):
|
||||
"""Test that copy_file with only_local=False copies to both local and remote."""
|
||||
remote_path = os.path.join(temp_dir, "remote")
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
input_dir = InputDir(temp_dir, "test_corpus")
|
||||
|
||||
src_dir = os.path.join(temp_dir, "src_corpus")
|
||||
os.makedirs(src_dir, exist_ok=True)
|
||||
|
||||
# Create a test file
|
||||
file_path = os.path.join(src_dir, "test_file")
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(b"test content")
|
||||
|
||||
# Copy file with only_local=False (explicit)
|
||||
dst = input_dir.copy_file(file_path, only_local=False)
|
||||
|
||||
# File should exist locally
|
||||
assert os.path.exists(dst)
|
||||
|
||||
# Same file should exist in remote
|
||||
remote_file = os.path.join(remote_path, os.path.basename(dst))
|
||||
assert os.path.exists(remote_file)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for tmpfs corpus functionality
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmpfs_temp_dirs():
|
||||
"""Create separate temp directories for node_data, tmpfs, and remote storage."""
|
||||
node_data_dir = tempfile.mkdtemp(prefix="node_data_")
|
||||
tmpfs_dir = tempfile.mkdtemp(prefix="tmpfs_")
|
||||
yield {"node_data": node_data_dir, "tmpfs": tmpfs_dir}
|
||||
shutil.rmtree(node_data_dir, ignore_errors=True)
|
||||
shutil.rmtree(tmpfs_dir, ignore_errors=True)
|
||||
|
||||
|
||||
class TestTmpfsCorpusConfiguration:
|
||||
"""Tests for tmpfs corpus configuration functions."""
|
||||
|
||||
def test_is_corpus_tmpfs_enabled_when_not_set(self):
|
||||
"""Test that tmpfs is disabled when env var is not set."""
|
||||
with patch.object(node_local, "corpus_tmpfs_path", None):
|
||||
assert node_local.is_corpus_tmpfs_enabled() is False
|
||||
|
||||
def test_is_corpus_tmpfs_enabled_when_set(self):
|
||||
"""Test that tmpfs is enabled when env var is set."""
|
||||
with patch.object(node_local, "corpus_tmpfs_path", "/tmp/corpus"):
|
||||
assert node_local.is_corpus_tmpfs_enabled() is True
|
||||
|
||||
def test_get_corpus_tmpfs_path_when_not_set(self):
|
||||
"""Test that get_corpus_tmpfs_path returns None when not configured."""
|
||||
with patch.object(node_local, "corpus_tmpfs_path", None):
|
||||
assert node_local.get_corpus_tmpfs_path() is None
|
||||
|
||||
def test_get_corpus_tmpfs_path_when_set(self):
|
||||
"""Test that get_corpus_tmpfs_path returns the configured path."""
|
||||
with patch.object(node_local, "corpus_tmpfs_path", "/tmp/corpus"):
|
||||
assert node_local.get_corpus_tmpfs_path() == Path("/tmp/corpus")
|
||||
|
||||
|
||||
class TestCorpusTmpfsStorage:
|
||||
"""Tests for corpus storage using tmpfs."""
|
||||
|
||||
def test_corpus_uses_standard_path_when_tmpfs_disabled(self, tmpfs_temp_dirs):
|
||||
"""Test that Corpus uses standard node-local path when tmpfs is disabled."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
remote_path = Path(tmpfs_temp_dirs["node_data"]) / "remote"
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", None):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
corpus = Corpus(node_data, "test_task", "test_harness")
|
||||
|
||||
# Corpus should be stored under node_data
|
||||
assert corpus.path.startswith(node_data)
|
||||
assert "test_task" in corpus.path
|
||||
assert "buttercup_corpus_test_harness" in corpus.path
|
||||
|
||||
def test_corpus_uses_tmpfs_path_when_enabled(self, tmpfs_temp_dirs):
|
||||
"""Test that Corpus uses tmpfs path when tmpfs is enabled."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
tmpfs = tmpfs_temp_dirs["tmpfs"]
|
||||
remote_path = Path("/") / "test_task" / "buttercup_corpus_test_harness"
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", tmpfs):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
corpus = Corpus(node_data, "test_task", "test_harness")
|
||||
|
||||
# Corpus should be stored under tmpfs
|
||||
assert corpus.path.startswith(tmpfs)
|
||||
assert "test_task" in corpus.path
|
||||
assert "buttercup_corpus_test_harness" in corpus.path
|
||||
|
||||
def test_corpus_remote_path_unchanged_with_tmpfs(self, tmpfs_temp_dirs):
|
||||
"""Test that remote path is calculated correctly regardless of tmpfs setting."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
tmpfs = tmpfs_temp_dirs["tmpfs"]
|
||||
|
||||
# The remote path should be calculated based on node_data structure, not tmpfs
|
||||
expected_remote = Path("/") / "test_task" / "buttercup_corpus_test_harness"
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", tmpfs):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
local_path, remote_path = _get_corpus_storage_path(node_data, "test_task/buttercup_corpus_test_harness")
|
||||
|
||||
# Local should use tmpfs
|
||||
assert tmpfs in local_path
|
||||
|
||||
# Remote should be calculated from node_data structure
|
||||
assert remote_path == expected_remote
|
||||
|
||||
def test_corpus_file_operations_with_tmpfs(self, tmpfs_temp_dirs):
|
||||
"""Test that corpus file operations work correctly with tmpfs."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
tmpfs = tmpfs_temp_dirs["tmpfs"]
|
||||
remote_path = Path(node_data) / "remote" / "test_task" / "buttercup_corpus_test_harness"
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", tmpfs):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
corpus = Corpus(node_data, "test_task", "test_harness")
|
||||
|
||||
# Create a source directory with test files
|
||||
src_dir = os.path.join(tmpfs_temp_dirs["node_data"], "src")
|
||||
os.makedirs(src_dir, exist_ok=True)
|
||||
|
||||
# Create test files
|
||||
for i in range(3):
|
||||
file_path = os.path.join(src_dir, f"file_{i}")
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(f"content_{i}".encode())
|
||||
|
||||
# Copy files to corpus (should be on tmpfs)
|
||||
copied = corpus.copy_corpus(src_dir)
|
||||
|
||||
assert len(copied) == 3
|
||||
assert corpus.local_corpus_count() == 3
|
||||
|
||||
# Verify files are on tmpfs
|
||||
for copied_file in copied:
|
||||
assert tmpfs in copied_file
|
||||
assert os.path.exists(copied_file)
|
||||
|
||||
|
||||
class TestGetCorpusStoragePath:
|
||||
"""Tests for the _get_corpus_storage_path function."""
|
||||
|
||||
def test_standard_path_when_tmpfs_disabled(self, tmpfs_temp_dirs):
|
||||
"""Test standard path calculation when tmpfs is disabled."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", None):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
local_path, remote_path = _get_corpus_storage_path(node_data, "task/corpus")
|
||||
|
||||
assert local_path == os.path.join(node_data, "task/corpus")
|
||||
assert remote_path == Path("/task/corpus")
|
||||
|
||||
def test_tmpfs_path_when_enabled(self, tmpfs_temp_dirs):
|
||||
"""Test tmpfs path calculation when enabled."""
|
||||
node_data = tmpfs_temp_dirs["node_data"]
|
||||
tmpfs = tmpfs_temp_dirs["tmpfs"]
|
||||
|
||||
with patch.object(node_local, "corpus_tmpfs_path", tmpfs):
|
||||
with patch.object(node_local, "node_local_path", node_data):
|
||||
local_path, remote_path = _get_corpus_storage_path(node_data, "task/corpus")
|
||||
|
||||
# Local path should use tmpfs
|
||||
assert local_path == os.path.join(tmpfs, "task/corpus")
|
||||
# Remote path should still be calculated from node_data structure
|
||||
assert remote_path == Path("/task/corpus")
|
||||
|
||||
|
||||
class TestCrossFilesystemOperations:
|
||||
"""Tests for cross-filesystem file operations."""
|
||||
|
||||
def test_hash_corpus_with_shutil_move(self, temp_dir, mock_node_local):
|
||||
"""Test that hash_corpus works correctly (uses shutil.move internally)."""
|
||||
input_dir = InputDir(temp_dir, "test_corpus")
|
||||
|
||||
# Create files with non-hash names
|
||||
test_files = ["file_a.txt", "file_b.txt", "file_c.txt"]
|
||||
for name in test_files:
|
||||
file_path = os.path.join(input_dir.path, name)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(f"content of {name}".encode())
|
||||
|
||||
# Hash the corpus
|
||||
hashed = InputDir.hash_corpus(input_dir.path)
|
||||
|
||||
# Should have hashed all 3 files
|
||||
assert len(hashed) == 3
|
||||
|
||||
# All files should now have hash names (64 hex chars)
|
||||
for file in os.listdir(input_dir.path):
|
||||
assert InputDir.has_hashed_name(file)
|
||||
|
||||
def test_hash_corpus_skips_already_hashed(self, temp_dir, mock_node_local):
|
||||
"""Test that hash_corpus skips files that are already hashed."""
|
||||
input_dir = InputDir(temp_dir, "test_corpus")
|
||||
|
||||
# Create a file with a hash name (64 hex chars)
|
||||
hash_name = "a" * 64
|
||||
hash_file_path = os.path.join(input_dir.path, hash_name)
|
||||
with open(hash_file_path, "wb") as f:
|
||||
f.write(b"already hashed content")
|
||||
|
||||
# Create a non-hashed file
|
||||
non_hash_path = os.path.join(input_dir.path, "not_hashed.txt")
|
||||
with open(non_hash_path, "wb") as f:
|
||||
f.write(b"not hashed content")
|
||||
|
||||
# Hash the corpus
|
||||
hashed = InputDir.hash_corpus(input_dir.path)
|
||||
|
||||
# Should only hash the non-hashed file
|
||||
assert len(hashed) == 1
|
||||
assert hash_name not in hashed
|
||||
|
||||
# The pre-existing hash file should still exist with same name
|
||||
assert os.path.exists(hash_file_path)
|
||||
|
||||
|
||||
class TestInputDirOverrides:
|
||||
"""Tests for InputDir with override parameters."""
|
||||
|
||||
def test_input_dir_with_override_local_path(self, temp_dir):
|
||||
"""Test InputDir with override_local_path parameter."""
|
||||
custom_local = os.path.join(temp_dir, "custom_local")
|
||||
remote_path = Path(temp_dir) / "remote"
|
||||
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=remote_path):
|
||||
input_dir = InputDir(
|
||||
temp_dir,
|
||||
"test_corpus",
|
||||
override_local_path=custom_local,
|
||||
)
|
||||
|
||||
assert input_dir.path == custom_local
|
||||
assert os.path.exists(custom_local)
|
||||
|
||||
def test_input_dir_with_override_remote_path(self, temp_dir):
|
||||
"""Test InputDir with override_remote_path parameter."""
|
||||
custom_remote = Path("/custom/remote/path")
|
||||
|
||||
# Need to still patch the default remote_path call
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=Path("/default")):
|
||||
input_dir = InputDir(
|
||||
temp_dir,
|
||||
"test_corpus",
|
||||
override_remote_path=custom_remote,
|
||||
)
|
||||
|
||||
assert input_dir.remote_path == custom_remote
|
||||
|
||||
def test_input_dir_with_both_overrides(self, temp_dir):
|
||||
"""Test InputDir with both override parameters."""
|
||||
custom_local = os.path.join(temp_dir, "custom_local")
|
||||
custom_remote = Path("/custom/remote")
|
||||
|
||||
with patch("buttercup.common.node_local.remote_path", return_value=Path("/default")):
|
||||
input_dir = InputDir(
|
||||
temp_dir,
|
||||
"test_corpus",
|
||||
override_local_path=custom_local,
|
||||
override_remote_path=custom_remote,
|
||||
)
|
||||
|
||||
assert input_dir.path == custom_local
|
||||
assert input_dir.remote_path == custom_remote
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import pytest
|
||||
from redis import Redis
|
||||
from buttercup.common.maps import CoverageMap
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import FunctionCoverage
|
||||
from buttercup.common.maps import CoverageMap
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Tests for cross-filesystem operations in node_local module.
|
||||
|
||||
These tests use real filesystem operations, not mocks, to test the actual
|
||||
behavior of cross-filesystem file operations.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from buttercup.common.node_local import (
|
||||
_copy_and_delete,
|
||||
rename_atomically,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dirs():
|
||||
"""Create temporary directories for testing."""
|
||||
dir1 = tempfile.mkdtemp(prefix="test_dir1_")
|
||||
dir2 = tempfile.mkdtemp(prefix="test_dir2_")
|
||||
yield {"dir1": Path(dir1), "dir2": Path(dir2)}
|
||||
shutil.rmtree(dir1, ignore_errors=True)
|
||||
shutil.rmtree(dir2, ignore_errors=True)
|
||||
|
||||
|
||||
class TestCopyAndDelete:
|
||||
"""Tests for _copy_and_delete function."""
|
||||
|
||||
def test_file(self, temp_dirs):
|
||||
"""Test _copy_and_delete for files."""
|
||||
src = temp_dirs["dir1"] / "test_file.txt"
|
||||
dst = temp_dirs["dir2"] / "copied_file.txt"
|
||||
|
||||
# Create source file
|
||||
src.write_bytes(b"file content")
|
||||
|
||||
# Copy and delete
|
||||
result = _copy_and_delete(src, dst)
|
||||
|
||||
assert result == dst
|
||||
assert dst.exists()
|
||||
assert not src.exists()
|
||||
assert dst.read_bytes() == b"file content"
|
||||
|
||||
def test_directory(self, temp_dirs):
|
||||
"""Test _copy_and_delete for directories."""
|
||||
src = temp_dirs["dir1"] / "test_dir"
|
||||
dst = temp_dirs["dir2"] / "copied_dir"
|
||||
|
||||
# Create source directory with files
|
||||
src.mkdir()
|
||||
(src / "file1.txt").write_bytes(b"content1")
|
||||
(src / "file2.txt").write_bytes(b"content2")
|
||||
|
||||
# Copy and delete
|
||||
result = _copy_and_delete(src, dst)
|
||||
|
||||
assert result == dst
|
||||
assert dst.exists()
|
||||
assert not src.exists()
|
||||
assert (dst / "file1.txt").read_bytes() == b"content1"
|
||||
assert (dst / "file2.txt").read_bytes() == b"content2"
|
||||
|
||||
def test_file_exists(self, temp_dirs):
|
||||
"""Test _copy_and_delete when destination already exists."""
|
||||
src = temp_dirs["dir1"] / "test_file.txt"
|
||||
dst = temp_dirs["dir2"] / "existing_file.txt"
|
||||
|
||||
# Create source and destination files
|
||||
src.write_bytes(b"source content")
|
||||
dst.write_bytes(b"existing content")
|
||||
|
||||
# Copy and delete should skip and return None
|
||||
result = _copy_and_delete(src, dst)
|
||||
|
||||
assert result is None
|
||||
# Destination should keep its original content
|
||||
assert dst.read_bytes() == b"existing content"
|
||||
# Source should be deleted
|
||||
assert not src.exists()
|
||||
|
||||
def test_directory_exists(self, temp_dirs):
|
||||
"""Test _copy_and_delete when destination directory already exists."""
|
||||
src = temp_dirs["dir1"] / "test_dir"
|
||||
dst = temp_dirs["dir2"] / "existing_dir"
|
||||
|
||||
# Create source and destination directories
|
||||
src.mkdir()
|
||||
(src / "new_file.txt").write_bytes(b"new content")
|
||||
|
||||
dst.mkdir()
|
||||
(dst / "existing_file.txt").write_bytes(b"existing content")
|
||||
|
||||
# Copy and delete should skip and return None
|
||||
result = _copy_and_delete(src, dst)
|
||||
|
||||
assert result is None
|
||||
# Destination should keep its original content
|
||||
assert (dst / "existing_file.txt").read_bytes() == b"existing content"
|
||||
# Source should be deleted
|
||||
assert not src.exists()
|
||||
|
||||
|
||||
class TestRenameAtomicallyCrossFilesystem:
|
||||
"""Tests for rename_atomically function with cross-filesystem scenarios."""
|
||||
|
||||
def test_cross_filesystem(self, temp_dirs):
|
||||
"""Test rename_atomically handles cross-filesystem via copy+delete."""
|
||||
src = temp_dirs["dir1"] / "test_file.txt"
|
||||
dst = temp_dirs["dir2"] / "renamed_file.txt"
|
||||
|
||||
# Create source file
|
||||
src.write_bytes(b"test content")
|
||||
|
||||
# Manually patch os.rename at runtime to simulate EXDEV
|
||||
original_rename = os.rename
|
||||
|
||||
def mock_rename(s, d):
|
||||
raise OSError(errno.EXDEV, "Invalid cross-device link")
|
||||
|
||||
os.rename = mock_rename
|
||||
try:
|
||||
# rename_atomically should fall back to copy+delete
|
||||
result = rename_atomically(src, dst)
|
||||
|
||||
assert result == dst
|
||||
assert dst.exists()
|
||||
assert not src.exists()
|
||||
assert dst.read_bytes() == b"test content"
|
||||
finally:
|
||||
os.rename = original_rename
|
||||
|
||||
def test_directory_not_empty(self, temp_dirs):
|
||||
"""Test rename_atomically when directory exists (errno ENOTEMPTY)."""
|
||||
src = temp_dirs["dir1"] / "test_file.txt"
|
||||
dst = temp_dirs["dir2"] / "renamed_file.txt"
|
||||
|
||||
# Create source file
|
||||
src.write_bytes(b"test content")
|
||||
|
||||
# Manually patch os.rename at runtime to simulate ENOTEMPTY
|
||||
original_rename = os.rename
|
||||
|
||||
def mock_rename(s, d):
|
||||
raise OSError(errno.ENOTEMPTY, "Directory not empty")
|
||||
|
||||
os.rename = mock_rename
|
||||
try:
|
||||
# rename_atomically should return None
|
||||
result = rename_atomically(src, dst)
|
||||
|
||||
assert result is None
|
||||
finally:
|
||||
os.rename = original_rename
|
||||
|
||||
def test_cross_filesystem_directory(self, temp_dirs):
|
||||
"""Test rename_atomically handles cross-filesystem for directories."""
|
||||
src = temp_dirs["dir1"] / "test_dir"
|
||||
dst = temp_dirs["dir2"] / "renamed_dir"
|
||||
|
||||
# Create source directory with files
|
||||
src.mkdir()
|
||||
(src / "file1.txt").write_bytes(b"content1")
|
||||
(src / "subdir").mkdir()
|
||||
(src / "subdir" / "file2.txt").write_bytes(b"content2")
|
||||
|
||||
# Manually patch os.rename at runtime to simulate EXDEV
|
||||
original_rename = os.rename
|
||||
|
||||
def mock_rename(s, d):
|
||||
raise OSError(errno.EXDEV, "Invalid cross-device link")
|
||||
|
||||
os.rename = mock_rename
|
||||
try:
|
||||
# rename_atomically should fall back to copy+delete
|
||||
result = rename_atomically(src, dst)
|
||||
|
||||
assert result == dst
|
||||
assert dst.exists()
|
||||
assert not src.exists()
|
||||
assert (dst / "file1.txt").read_bytes() == b"content1"
|
||||
assert (dst / "subdir" / "file2.txt").read_bytes() == b"content2"
|
||||
finally:
|
||||
os.rename = original_rename
|
||||
@@ -3,7 +3,7 @@ import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from buttercup.common.clusterfuzz_utils import get_fuzz_targets, EXTRA_BUILD_DIR
|
||||
from buttercup.common.clusterfuzz_utils import EXTRA_BUILD_DIR, get_fuzz_targets
|
||||
|
||||
|
||||
class TestGetFuzzTargets(unittest.TestCase):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import pytest
|
||||
from redis import Redis
|
||||
from buttercup.common.maps import RedisMap
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import WeightedHarness
|
||||
from buttercup.common.maps import RedisMap
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from buttercup.common.reproduce_multiple import ReproduceMultiple
|
||||
|
||||
from buttercup.common.challenge_task import CommandResult, ReproduceResult
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput
|
||||
from buttercup.common.challenge_task import ReproduceResult, CommandResult
|
||||
from buttercup.common.reproduce_multiple import ReproduceMultiple
|
||||
|
||||
# ruff: noqa: E501, W291
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import patch, mock_open, MagicMock
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from buttercup.common.node_local import (
|
||||
_get_root_path,
|
||||
TmpDir,
|
||||
temp_dir,
|
||||
rename_atomically,
|
||||
remote_path as remote_path_func,
|
||||
remote_archive_path as remote_archive_path_func,
|
||||
scratch_path,
|
||||
scratch_dir,
|
||||
local_scratch_file,
|
||||
remote_scratch_file,
|
||||
_get_root_path,
|
||||
dir_to_remote_archive,
|
||||
get_corpus_tmpfs_path,
|
||||
is_corpus_tmpfs_enabled,
|
||||
local_scratch_file,
|
||||
lopen,
|
||||
remote_scratch_file,
|
||||
rename_atomically,
|
||||
scratch_dir,
|
||||
scratch_path,
|
||||
temp_dir,
|
||||
)
|
||||
from buttercup.common.node_local import (
|
||||
remote_archive_path as remote_archive_path_func,
|
||||
)
|
||||
from buttercup.common.node_local import (
|
||||
remote_path as remote_path_func,
|
||||
)
|
||||
|
||||
|
||||
# Use this root path for all tests
|
||||
TEST_ROOT_PATH = Path("/test/node/data/dir")
|
||||
@@ -217,13 +223,15 @@ class TestNodeLocal:
|
||||
mock_scratch_context.__enter__.return_value = mock_scratch_file
|
||||
|
||||
with patch(
|
||||
"buttercup.common.node_local.local_scratch_file", return_value=mock_scratch_context
|
||||
"buttercup.common.node_local.local_scratch_file",
|
||||
return_value=mock_scratch_context,
|
||||
) as mock_local_scratch:
|
||||
# Mock copyfileobj to avoid actual copying
|
||||
with patch("shutil.copyfileobj") as mock_copy:
|
||||
# Mock rename_atomically
|
||||
with patch(
|
||||
"buttercup.common.node_local.rename_atomically", return_value=local_path
|
||||
"buttercup.common.node_local.rename_atomically",
|
||||
return_value=local_path,
|
||||
) as mock_rename:
|
||||
# Mock path operations
|
||||
with patch.object(Path, "mkdir"):
|
||||
@@ -265,7 +273,8 @@ class TestNodeLocal:
|
||||
|
||||
# Mock the archive path
|
||||
with patch(
|
||||
"buttercup.common.node_local.remote_archive_path", return_value=remote_archive_path_val
|
||||
"buttercup.common.node_local.remote_archive_path",
|
||||
return_value=remote_archive_path_val,
|
||||
) as mock_rpath:
|
||||
# Mock file operations
|
||||
with patch("builtins.open", mock_open(read_data=b"test data")) as mocked_open:
|
||||
@@ -276,7 +285,8 @@ class TestNodeLocal:
|
||||
mock_scratch_context.__enter__.return_value = mock_scratch_file
|
||||
|
||||
with patch(
|
||||
"buttercup.common.node_local.local_scratch_file", return_value=mock_scratch_context
|
||||
"buttercup.common.node_local.local_scratch_file",
|
||||
return_value=mock_scratch_context,
|
||||
) as mock_local_scratch:
|
||||
# Mock copyfileobj to avoid actual copying
|
||||
with patch("shutil.copyfileobj") as mock_copy:
|
||||
@@ -294,11 +304,13 @@ class TestNodeLocal:
|
||||
mock_tmp_dir_context.__enter__.return_value = mock_tmp_dir
|
||||
|
||||
with patch(
|
||||
"buttercup.common.node_local.scratch_dir", return_value=mock_tmp_dir_context
|
||||
"buttercup.common.node_local.scratch_dir",
|
||||
return_value=mock_tmp_dir_context,
|
||||
) as mock_scratch_dir:
|
||||
# Mock rename_atomically
|
||||
with patch(
|
||||
"buttercup.common.node_local.rename_atomically", return_value=local_path
|
||||
"buttercup.common.node_local.rename_atomically",
|
||||
return_value=local_path,
|
||||
) as mock_rename:
|
||||
# Mock path.exists
|
||||
with patch.object(Path, "exists", return_value=False):
|
||||
@@ -491,3 +503,32 @@ class TestNodeLocal:
|
||||
|
||||
# Check the return value
|
||||
assert result == mock_file
|
||||
|
||||
|
||||
class TestTmpfsCorpusConfiguration:
|
||||
"""Tests for tmpfs corpus configuration functions."""
|
||||
|
||||
@patch("buttercup.common.node_local.corpus_tmpfs_path", None)
|
||||
def test_is_corpus_tmpfs_enabled_false(self):
|
||||
"""Test is_corpus_tmpfs_enabled when not configured."""
|
||||
assert is_corpus_tmpfs_enabled() is False
|
||||
|
||||
@patch("buttercup.common.node_local.corpus_tmpfs_path", "/tmp/corpus")
|
||||
def test_is_corpus_tmpfs_enabled_true(self):
|
||||
"""Test is_corpus_tmpfs_enabled when configured."""
|
||||
assert is_corpus_tmpfs_enabled() is True
|
||||
|
||||
@patch("buttercup.common.node_local.corpus_tmpfs_path", None)
|
||||
def test_get_corpus_tmpfs_path_not_set(self):
|
||||
"""Test get_corpus_tmpfs_path when not configured."""
|
||||
assert get_corpus_tmpfs_path() is None
|
||||
|
||||
@patch("buttercup.common.node_local.corpus_tmpfs_path", "/tmp/corpus")
|
||||
def test_get_corpus_tmpfs_path_set(self):
|
||||
"""Test get_corpus_tmpfs_path when configured."""
|
||||
result = get_corpus_tmpfs_path()
|
||||
assert result == Path("/tmp/corpus")
|
||||
|
||||
|
||||
# Cross-filesystem operation tests are in test_cross_filesystem.py
|
||||
# to avoid interference with the autouse mock_env_settings fixture.
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import pytest
|
||||
import time
|
||||
from redis import Redis
|
||||
|
||||
import pytest
|
||||
from google.protobuf.struct_pb2 import Struct
|
||||
from redis import Redis
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildOutput, BuildRequest
|
||||
from buttercup.common.queues import (
|
||||
ReliableQueue,
|
||||
RQItem,
|
||||
BUILD_OUTPUT_TASK_TIMEOUT_MS,
|
||||
BUILD_TASK_TIMEOUT_MS,
|
||||
GroupNames,
|
||||
QueueFactory,
|
||||
QueueNames,
|
||||
GroupNames,
|
||||
BUILD_TASK_TIMEOUT_MS,
|
||||
BUILD_OUTPUT_TASK_TIMEOUT_MS,
|
||||
ReliableQueue,
|
||||
RQItem,
|
||||
)
|
||||
from buttercup.common.datastructures.msg_pb2 import BuildRequest, BuildOutput
|
||||
|
||||
GROUP_NAME = "test_group"
|
||||
QUEUE_NAME = "test_queue"
|
||||
@@ -210,7 +212,7 @@ def test_queue_factory(redis_client):
|
||||
sanitizer="test_sanitizer",
|
||||
task_dir="test_task_dir",
|
||||
task_id="test_task_id",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
item = queue.pop()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from redis import Redis
|
||||
from buttercup.common.sarif_store import SARIFStore, SARIFBroadcastDetail
|
||||
|
||||
from buttercup.common.sarif_store import SARIFBroadcastDetail, SARIFStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from redis import Redis
|
||||
from buttercup.common.sets import RedisSet, PoVReproduceStatus
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import POVReproduceRequest, POVReproduceResponse
|
||||
from unittest.mock import MagicMock
|
||||
from buttercup.common.sets import PoVReproduceStatus, RedisSet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from buttercup.common.task_registry import TaskRegistry, CANCELLED_TASKS_SET, SUCCEEDED_TASKS_SET, ERRORED_TASKS_SET
|
||||
from buttercup.common.datastructures.msg_pb2 import Task, SourceDetail
|
||||
import time
|
||||
from typing import Set
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from redis import Redis
|
||||
from unittest.mock import patch
|
||||
|
||||
from buttercup.common.datastructures.msg_pb2 import SourceDetail, Task
|
||||
from buttercup.common.task_registry import CANCELLED_TASKS_SET, ERRORED_TASKS_SET, SUCCEEDED_TASKS_SET, TaskRegistry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -166,7 +166,10 @@ def test_iter_tasks_with_different_types(task_registry, redis_client):
|
||||
# Create and add two different tasks
|
||||
full_task = Task(task_id="full123", task_type=Task.TaskType.TASK_TYPE_FULL, message_id="msg_full", cancelled=False)
|
||||
delta_task = Task(
|
||||
task_id="delta456", task_type=Task.TaskType.TASK_TYPE_DELTA, message_id="msg_delta", cancelled=True
|
||||
task_id="delta456",
|
||||
task_type=Task.TaskType.TASK_TYPE_DELTA,
|
||||
message_id="msg_delta",
|
||||
cancelled=True,
|
||||
)
|
||||
|
||||
# Setup Redis mock
|
||||
@@ -319,7 +322,7 @@ def test_is_expired(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "expired-task":
|
||||
return expired_task.SerializeToString()
|
||||
elif key == "live-task":
|
||||
if key == "live-task":
|
||||
return live_task.SerializeToString()
|
||||
return None
|
||||
|
||||
@@ -432,7 +435,7 @@ def test_get_cancelled_task_ids(task_registry, redis_client):
|
||||
def test_should_stop_processing_with_cancelled_ids(task_registry, redis_client):
|
||||
"""Test that should_stop_processing handles cancelled_ids correctly."""
|
||||
# Create a set of cancelled IDs
|
||||
cancelled_ids: Set[str] = {"cancelled-task-1", "cancelled-task-2"}
|
||||
cancelled_ids: set[str] = {"cancelled-task-1", "cancelled-task-2"}
|
||||
|
||||
# Create tasks to test with
|
||||
current_time = int(time.time())
|
||||
@@ -444,9 +447,9 @@ def test_should_stop_processing_with_cancelled_ids(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "cancelled-task-1":
|
||||
return cancelled_task1.SerializeToString()
|
||||
elif key == "cancelled-task-2":
|
||||
if key == "cancelled-task-2":
|
||||
return cancelled_task2.SerializeToString()
|
||||
elif key == "active-task":
|
||||
if key == "active-task":
|
||||
return active_task.SerializeToString()
|
||||
return None
|
||||
|
||||
@@ -479,7 +482,7 @@ def test_should_stop_processing_no_cancelled_ids(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "active-task":
|
||||
return active_task.SerializeToString()
|
||||
elif key == "cancelled-task":
|
||||
if key == "cancelled-task":
|
||||
return cancelled_task.SerializeToString()
|
||||
return None
|
||||
|
||||
@@ -517,7 +520,7 @@ def test_should_stop_processing_expired_task(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "expired-task":
|
||||
return expired_task.SerializeToString()
|
||||
elif key == "active-task":
|
||||
if key == "active-task":
|
||||
return active_task.SerializeToString()
|
||||
return None
|
||||
|
||||
@@ -659,9 +662,9 @@ def test_is_expired_with_delta(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "just-expired":
|
||||
return just_expired_task.SerializeToString()
|
||||
elif key == "soon-expired":
|
||||
if key == "soon-expired":
|
||||
return soon_expired_task.SerializeToString()
|
||||
elif key == "future-task":
|
||||
if key == "future-task":
|
||||
return future_task.SerializeToString()
|
||||
return None
|
||||
|
||||
@@ -732,7 +735,7 @@ def test_is_expired_with_delta_edge_cases(task_registry, redis_client):
|
||||
def mock_hget(hash_name, key):
|
||||
if key == "one-second-expired":
|
||||
return one_second_expired.SerializeToString()
|
||||
elif key == "one-second-future":
|
||||
if key == "one-second-future":
|
||||
return one_second_future.SerializeToString()
|
||||
return None
|
||||
|
||||
|
||||
Generated
+879
-1487
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,8 @@ services:
|
||||
# - shared-tmp:/tmp
|
||||
|
||||
scantron:
|
||||
profiles:
|
||||
- competition-server
|
||||
image: ghcr.io/tob-challenges/example-crs-architecture/competition-test-api:v1.4-rc1
|
||||
platform: linux/amd64
|
||||
privileged: true
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
version: "3"
|
||||
x-common: &common
|
||||
profiles:
|
||||
- signoz
|
||||
networks:
|
||||
- signoz-net
|
||||
restart: on-failure
|
||||
|
||||
Generated
+5
@@ -5,6 +5,7 @@ provider "registry.terraform.io/azure/azapi" {
|
||||
version = "2.2.0"
|
||||
constraints = "2.2.0"
|
||||
hashes = [
|
||||
"h1:Us5LvK2ju2qo3MQlXVtDDKCt5SMFRDIHUL8ubVdCEUg=",
|
||||
"h1:yckm1jqUMUGSeS57a3uR0gG/V7scwvjpkRVXnQIUAo4=",
|
||||
"zh:062be5d8272cac297a88c2057449f449ea6906c4121ba3dfdeb5cecb3ff91178",
|
||||
"zh:1fd9abec3ffcbf8d0244408334e9bfc8f49ada50978cd73ee0ed5f8560987267",
|
||||
@@ -25,6 +26,7 @@ provider "registry.terraform.io/hashicorp/azurerm" {
|
||||
version = "4.17.0"
|
||||
constraints = "4.17.0"
|
||||
hashes = [
|
||||
"h1:VgnUh7PiRa/76P+0NFk8vmrmfLnPT6+tOZ/AP6h4TeQ=",
|
||||
"h1:gpFgaBSkRTxhavgPAuqQcElHJqmRJ1RpQGr1K0dvVW8=",
|
||||
"zh:163b81a3bf29c8f161a1c100a48164b1bd1af434cd564b44596cb71a6c33f03d",
|
||||
"zh:2996b107d3c05a9db14458b32b6f22f8cde0adb96263196d82d3dc302907a257",
|
||||
@@ -46,6 +48,7 @@ provider "registry.terraform.io/hashicorp/helm" {
|
||||
constraints = "2.17.0"
|
||||
hashes = [
|
||||
"h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=",
|
||||
"h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=",
|
||||
"zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4",
|
||||
"zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7",
|
||||
"zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3",
|
||||
@@ -66,6 +69,7 @@ provider "registry.terraform.io/hashicorp/random" {
|
||||
constraints = "3.6.3"
|
||||
hashes = [
|
||||
"h1:Fnaec9vA8sZ8BXVlN3Xn9Jz3zghSETIKg7ch8oXhxno=",
|
||||
"h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=",
|
||||
"zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451",
|
||||
"zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8",
|
||||
"zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe",
|
||||
@@ -86,6 +90,7 @@ provider "registry.terraform.io/hashicorp/time" {
|
||||
constraints = "0.12.1"
|
||||
hashes = [
|
||||
"h1:6BhxSYBJdBBKyuqatOGkuPKVenfx6UmLdiI13Pb3his=",
|
||||
"h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=",
|
||||
"zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2",
|
||||
"zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea",
|
||||
"zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511",
|
||||
|
||||
@@ -26,6 +26,131 @@ The standard `azure` networking profile is used.
|
||||
- Access credentials to the competition Tailscale tailnet.
|
||||
- Minikube installed, if you want to test the full CRS locally (linux/amd64 system recommended).
|
||||
|
||||
## Local Minikube Deployment
|
||||
|
||||
This section covers deploying the CRS locally using minikube on macOS or Linux.
|
||||
|
||||
### Local Prerequisites
|
||||
|
||||
- **Docker Desktop** (macOS/Windows), **Colima** (macOS/Linux), or Docker Engine (Linux)
|
||||
- **docker-buildx**: Required for building images (`brew install docker-buildx` on macOS)
|
||||
- **minikube**: `brew install minikube` (macOS) or see [minikube install docs](https://minikube.sigs.k8s.io/docs/start/)
|
||||
- **helm**: `brew install helm` (macOS)
|
||||
- **kubectl**: `brew install kubectl` (macOS)
|
||||
|
||||
### Docker Resource Configuration
|
||||
|
||||
The Docker runtime must have enough resources allocated for the full CRS deployment.
|
||||
|
||||
**Docker Desktop (macOS/Windows):**
|
||||
1. Open Docker Desktop > Settings > Resources
|
||||
2. Allocate at least:
|
||||
- CPUs: 4-6 (depending on your machine)
|
||||
- Memory: 8-12 GB (10GB recommended for full deployment)
|
||||
- Disk: 80+ GB
|
||||
3. Click "Apply & Restart"
|
||||
|
||||
**Colima (macOS/Linux):**
|
||||
```bash
|
||||
colima start --cpu 6 --memory 10 --disk 80
|
||||
```
|
||||
|
||||
After starting Colima, ensure the buildx plugin is linked:
|
||||
```bash
|
||||
brew install docker-buildx
|
||||
```
|
||||
|
||||
`docker-buildx` is a Docker plugin. For Docker to find the plugin, add "cliPluginsExtraDirs" to ~/.docker/config.json:
|
||||
```json
|
||||
"cliPluginsExtraDirs": [
|
||||
"/opt/homebrew/lib/docker/cli-plugins"
|
||||
]
|
||||
```
|
||||
|
||||
**Important**: Minikube cannot use more resources than the Docker runtime allocates. If `env.template` specifies `MINIKUBE_CPU=6` and `MINIKUBE_MEMORY_GB=10`, but Docker only has 4 CPUs and 8GB RAM, minikube will fail to start or pods will remain Pending.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
cd deployment
|
||||
cp env.template env
|
||||
|
||||
# Edit env to configure:
|
||||
# - MINIKUBE_CPU/MINIKUBE_MEMORY_GB (match your Docker Desktop resources)
|
||||
# - OPENAI_API_KEY and/or ANTHROPIC_API_KEY
|
||||
# - Comment out GHCR_AUTH if building locally
|
||||
|
||||
make up
|
||||
```
|
||||
|
||||
### Common Errors and Solutions
|
||||
|
||||
#### "Exiting due to RSRC_INSUFFICIENT_CORES" or memory errors
|
||||
|
||||
**Cause**: Minikube is requesting more resources than Docker Desktop has available.
|
||||
|
||||
**Solution**: Either increase Docker Desktop resources or reduce minikube resources in `env`:
|
||||
```bash
|
||||
export MINIKUBE_CPU=4
|
||||
export MINIKUBE_MEMORY_GB=7
|
||||
```
|
||||
|
||||
#### Pods stuck in Pending state
|
||||
|
||||
**Cause**: Kubernetes cannot schedule pods due to insufficient CPU or memory.
|
||||
|
||||
**Solution**: This is expected with limited resources. Check which pods are pending:
|
||||
```bash
|
||||
kubectl get pods -n crs | grep Pending
|
||||
kubectl describe pod <pod-name> -n crs | grep -A5 Events
|
||||
```
|
||||
|
||||
For local development, core services (redis, task-server, scheduler) should run. Fuzzer bots and multiple replicas may remain Pending.
|
||||
|
||||
#### Base64 decode errors with GHCR_AUTH
|
||||
|
||||
**Cause**: `GHCR_AUTH` is set to the placeholder value `<your-ghcr-base64-auth>`.
|
||||
|
||||
**Solution**: Either set a valid base64-encoded GitHub token or comment out the line:
|
||||
```bash
|
||||
# export GHCR_AUTH="<your-ghcr-base64-auth>"
|
||||
```
|
||||
|
||||
For local builds, GHCR authentication is not required.
|
||||
|
||||
### Resource Expectations
|
||||
|
||||
With 8GB RAM allocated to Docker Desktop:
|
||||
|
||||
| Resource Setting | Expected Result |
|
||||
|-----------------|-----------------|
|
||||
| MINIKUBE_MEMORY_GB=10 | Minikube fails to start |
|
||||
| MINIKUBE_MEMORY_GB=7 | Minikube starts, some pods Pending |
|
||||
| MINIKUBE_MEMORY_GB=6 | More pods Pending, core services run |
|
||||
|
||||
### macOS ARM64 (Apple Silicon) Notes
|
||||
|
||||
- Minikube uses QEMU emulation for amd64 images on ARM64 Macs
|
||||
- Some components may run slower due to emulation overhead
|
||||
- The `gcr.io/oss-fuzz-base/base-runner` image is amd64-only
|
||||
- For best performance, use a linux/amd64 machine or cloud VM
|
||||
|
||||
### Verifying the Deployment
|
||||
|
||||
```bash
|
||||
# Check all pods
|
||||
kubectl get pods -n crs
|
||||
|
||||
# Check core services are running
|
||||
kubectl get pods -n crs | grep -E "(redis|task-server|scheduler)"
|
||||
|
||||
# View logs for a specific service
|
||||
kubectl logs -n crs -l app=scheduler --tail=50
|
||||
|
||||
# Port forward to access the API locally
|
||||
kubectl port-forward -n crs service/buttercup-competition-api 31323:1323
|
||||
```
|
||||
|
||||
### Azure
|
||||
|
||||
#### Login to Azure
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- Microsoft.Compute
|
||||
- Microsoft.Storage
|
||||
- Microsoft.Network
|
||||
- Microsoft.ContainerService
|
||||
|
||||
2. Verify and adjust Azure quotas:
|
||||
- Total Regional vCPUs: ~2000
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
RUN_DIR="run_data_${TIMESTAMP}"
|
||||
mkdir -p "${RUN_DIR}"
|
||||
cd "${RUN_DIR}"
|
||||
cd "${RUN_DIR}" || exit 1
|
||||
|
||||
echo "Collecting all data post-run"
|
||||
# Get cluster info
|
||||
@@ -21,7 +21,7 @@ echo -e "\nPods in crs namespace:"
|
||||
kubectl get pods -n crs
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "Is this the correct cluster to collect run data from? (y/N) " confirm
|
||||
read -r -p "Is this the correct cluster to collect run data from? (y/N) " confirm
|
||||
if [[ $confirm != [yY] ]]; then
|
||||
echo "Aborting data collection"
|
||||
exit 1
|
||||
|
||||
@@ -112,7 +112,7 @@ export -f process_pod
|
||||
running_jobs=0
|
||||
for pod in $pods; do
|
||||
# Wait if we've reached the maximum number of parallel jobs
|
||||
while [ $running_jobs -ge $MAX_PARALLEL ]; do
|
||||
while [ "$running_jobs" -ge "$MAX_PARALLEL" ]; do
|
||||
# Wait for any background job to complete
|
||||
wait -n
|
||||
running_jobs=$((running_jobs - 1))
|
||||
|
||||
@@ -28,6 +28,7 @@ else
|
||||
fi
|
||||
if [ "$TAILSCALE_ENABLED" = "true" ]; then
|
||||
envsubst <k8s/base/tailscale-operator/operator.template >k8s/base/tailscale-operator/operator.yaml
|
||||
envsubst <k8s/base/tailscale-connections/proxies.template >k8s/base/tailscale-connections/proxies.yaml
|
||||
fi
|
||||
if [ "$(echo "$LANGFUSE_ENABLED" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||||
LANGFUSE_ENABLED="true"
|
||||
@@ -37,6 +38,7 @@ fi
|
||||
|
||||
BUTTERCUP_NAMESPACE=${BUTTERCUP_NAMESPACE:-crs}
|
||||
DEPLOY_CLUSTER=${DEPLOY_CLUSTER:-true}
|
||||
DEPLOY_SIGNOZ=${DEPLOY_SIGNOZ:-false}
|
||||
CLUSTER_TYPE=${CLUSTER_TYPE:-minikube}
|
||||
|
||||
if [ "$DEPLOY_CLUSTER" = "true" ] && [ "$CLUSTER_TYPE" = "aks" ]; then
|
||||
@@ -87,12 +89,32 @@ up() {
|
||||
;;
|
||||
*)
|
||||
echo -e "${BLU}Deploying minikube cluster${NC}"
|
||||
minikube status | grep -q "kubelet: Running" || minikube start --force --extra-config=kubeadm.skip-phases=preflight --cpus=8 --memory=32g --disk-size=80g --driver=docker --kubernetes-version=stable
|
||||
# Minikube defaults
|
||||
MINIKUBE_CPU="${MINIKUBE_CPU:-6}"
|
||||
MINIKUBE_MEMORY_GB="${MINIKUBE_MEMORY_GB:-10}"
|
||||
MINIKUBE_DISK_GB="${MINIKUBE_DISK_GB:-80}"
|
||||
|
||||
echo -e "${BLU}Using Minikube configuration: CPU=${MINIKUBE_CPU}, Memory=${MINIKUBE_MEMORY_GB}GB, Disk=${MINIKUBE_DISK_GB}GB${NC}"
|
||||
minikube status | grep -q "kubelet: Running" || minikube start \
|
||||
--force \
|
||||
--extra-config=kubeadm.skip-phases=preflight \
|
||||
--cpus="${MINIKUBE_CPU}" \
|
||||
--memory="${MINIKUBE_MEMORY_GB}g" \
|
||||
--disk-size="${MINIKUBE_DISK_GB}g" \
|
||||
--driver=docker \
|
||||
--kubernetes-version=stable
|
||||
echo -e "${GRN}Minikube cluster status:${NC}"
|
||||
minikube status
|
||||
|
||||
# Resize /dev/shm inside minikube for corpus tmpfs storage.
|
||||
# Minikube docker driver defaults to 64MB, which is too small for fuzzing corpus data
|
||||
if [ -n "${MINIKUBE_SHM_SIZE_GB:-}" ]; then
|
||||
echo -e "${BLU}Resizing minikube /dev/shm to ${MINIKUBE_SHM_SIZE_GB}G${NC}"
|
||||
docker exec minikube mount -o remount,size="${MINIKUBE_SHM_SIZE_GB}G" /dev/shm
|
||||
fi
|
||||
|
||||
echo -e "${BLU}Building local docker images${NC}"
|
||||
eval $(minikube docker-env)
|
||||
eval "$(minikube docker-env --shell bash)"
|
||||
|
||||
# Authenticate with GitHub Container Registry for Docker builds
|
||||
if [ -n "$GHCR_AUTH" ]; then
|
||||
@@ -114,15 +136,25 @@ up() {
|
||||
fi
|
||||
|
||||
if [ -n "$FUZZER_BASE_IMAGE" ]; then
|
||||
# Check for aarch64 and append :manifest-arm64v8 to base image if needed
|
||||
if { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } && [ "$FUZZER_BASE_IMAGE" = "gcr.io/oss-fuzz-base/base-runner" ]; then
|
||||
FUZZER_BASE_IMAGE="${FUZZER_BASE_IMAGE}:manifest-arm64v8"
|
||||
fi
|
||||
|
||||
FUZZER_BUILD_ARGS="--build-arg BASE_IMAGE=$FUZZER_BASE_IMAGE"
|
||||
else
|
||||
FUZZER_BUILD_ARGS=""
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # Intentionally unquoted for word splitting
|
||||
docker build $ORCHESTRATOR_BUILD_ARGS -f "$SCRIPT_DIR"/../orchestrator/Dockerfile -t localhost/orchestrator:latest "$SCRIPT_DIR"/..
|
||||
docker build $FUZZER_BUILD_ARGS -f "$SCRIPT_DIR"/../fuzzer/dockerfiles/runner_image.Dockerfile -t localhost/fuzzer:latest "$SCRIPT_DIR"/..
|
||||
# shellcheck disable=SC2086 # Intentionally unquoted for word splitting
|
||||
docker build $FUZZER_BUILD_ARGS -f "$SCRIPT_DIR"/../fuzzer/Dockerfile -t localhost/fuzzer:latest "$SCRIPT_DIR"/..
|
||||
# shellcheck disable=SC2086 # Intentionally unquoted for word splitting
|
||||
docker build $SEED_GEN_BUILD_ARGS -f "$SCRIPT_DIR"/../seed-gen/Dockerfile -t localhost/seed-gen:latest "$SCRIPT_DIR"/..
|
||||
# shellcheck disable=SC2086 # Intentionally unquoted for word splitting
|
||||
docker build $PATCHER_BUILD_ARGS -f "$SCRIPT_DIR"/../patcher/Dockerfile -t localhost/patcher:latest "$SCRIPT_DIR"/..
|
||||
# shellcheck disable=SC2086 # Intentionally unquoted for word splitting
|
||||
docker build $PROGRAM_MODEL_BUILD_ARGS -f "$SCRIPT_DIR"/../program-model/Dockerfile -t localhost/program-model:latest "$SCRIPT_DIR"/..
|
||||
;;
|
||||
esac
|
||||
@@ -141,7 +173,7 @@ up() {
|
||||
--from-literal=scantron_github_pat="$SCANTRON_GITHUB_PAT" || echo -e "${GRN}ghcr secret already exists${NC}"
|
||||
|
||||
echo -e "${BLU}Creating CRS_INSTANCE_ID${NC}"
|
||||
CRS_INSTANCE_ID=$(echo $RANDOM | md5sum | head -c 20)
|
||||
CRS_INSTANCE_ID=$(echo "$RANDOM" | md5sum | head -c 20)
|
||||
kubectl create configmap crs-instance-id \
|
||||
--namespace "$BUTTERCUP_NAMESPACE" \
|
||||
--from-literal=crs-instance-id="$CRS_INSTANCE_ID" || echo -e "${GRN}crs-instance-id configmap already exists${NC}"
|
||||
@@ -202,7 +234,7 @@ fi
|
||||
if [ "$TAILSCALE_ENABLED" = "true" ]; then
|
||||
kubectl apply -k k8s/base/tailscale-connections/
|
||||
echo -e "${BLU}Waiting for ingress hostname DNS registration${NC}"
|
||||
timeout 5m bash -c "until kubectl get ingress -n "$BUTTERCUP_NAMESPACE" buttercup-task-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | grep -q '.'; do sleep 1; done" || echo -e "${BLU}Error: Ingress hostname failed to be to set within 5 minutes${NC}"
|
||||
timeout 5m bash -c 'until kubectl get ingress -n '"$BUTTERCUP_NAMESPACE"' buttercup-task-server -o jsonpath='"'"'{.status.loadBalancer.ingress[0].hostname}'"'"' | grep -q "."; do sleep 1; done' || echo -e "${BLU}Error: Ingress hostname failed to be to set within 5 minutes${NC}"
|
||||
INGRESS_HOSTNAME=$(kubectl get ingress -n "$BUTTERCUP_NAMESPACE" buttercup-task-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
|
||||
echo -e "${GRN}Your ingress DNS hostname is $INGRESS_HOSTNAME${NC}"
|
||||
fi
|
||||
@@ -247,13 +279,20 @@ down-k8s() {
|
||||
esac
|
||||
fi
|
||||
echo -e "${BLU}Deleting Kubernetes resource${NC}"
|
||||
set -x
|
||||
kubectl delete -k k8s/base/tailscale-connections/
|
||||
helm uninstall --wait --namespace "$BUTTERCUP_NAMESPACE" buttercup
|
||||
# Remove finalizers from clickhouse installation as stated in https://signoz.io/docs/operate/kubernetes/#uninstall-signoz
|
||||
# Without this, the namespace would not be deleted
|
||||
kubectl -n "$BUTTERCUP_NAMESPACE" patch clickhouseinstallations.clickhouse.altinity.com/buttercup-clickhouse -p '{"metadata":{"finalizers":[]}}' --type=merge
|
||||
kubectl delete -k k8s/base/tailscale-coredns/
|
||||
kubectl delete -k k8s/base/tailscale-dns/
|
||||
kubectl delete -k k8s/base/tailscale-operator/
|
||||
kubectl delete secret ghcr --namespace "$BUTTERCUP_NAMESPACE"
|
||||
kubectl delete namespace "$BUTTERCUP_NAMESPACE"
|
||||
set +x
|
||||
echo -e "${GRN}Cleanup complete.${NC}"
|
||||
echo -e "${BLU}Note: If you plan to redeploy and access SigNoz, clear your browser cookies for http://localhost:33301 to avoid login issues.${NC}"
|
||||
set -e
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -1,6 +1,7 @@
|
||||
# Reference a values file for kubernetes deployment. This should be a template
|
||||
# file, filled by crs-architecture.sh with the correct variables.
|
||||
# See values-upstream-minikube.template, values-minikube.template, values-aks.template, values-prod.template
|
||||
# For a large machine, see values-upstream-minikube-16cpu-128gb.template
|
||||
export BUTTERCUP_K8S_VALUES_TEMPLATE="k8s/values-upstream-minikube.template"
|
||||
|
||||
# Namespace used to install the whole CRS in.
|
||||
@@ -22,12 +23,14 @@ export CLUSTER_TYPE=minikube # or "aks"
|
||||
# export TF_VAR_ARM_SUBSCRIPTION_ID="<your-sub-id>"
|
||||
# export TF_VAR_usr_node_count=50
|
||||
# export TF_VAR_resource_group_name_prefix="<resource-prefix>"
|
||||
# export TF_VAR_resource_group_name="<existing-resource-group-name>"
|
||||
|
||||
# TailScale variables, necessary only for production/staging deployments
|
||||
export TAILSCALE_ENABLED=false # or true
|
||||
# export TS_CLIENT_ID="<your-tailscale-oauth-client-id>"
|
||||
# export TS_CLIENT_SECRET="<your-tailscale-oauth-client-secret>"
|
||||
# export TS_OP_TAG="<your-tailscale-operator-tag>"
|
||||
# export TAILSCALE_DOMAIN="tail123456.ts.net"
|
||||
|
||||
# AIxCC - Competition API settings, used to properly connect to the right endpoint and
|
||||
# with the right authentication. If COMPETITION_API_ENABLED is true, a test API
|
||||
@@ -55,14 +58,18 @@ export CRS_KEY_TOKEN_HASH='$argon2id$v=19$m=65536,t=3,p=4$Dg1v6NPGTyXPoOPF4ozD5A
|
||||
# GitHub Container Registry auth, base64 encoded
|
||||
# echo "USERNAME:$GHCR_PAT" | base64 --wrap=0
|
||||
# The PAT should have at least package:read permissions
|
||||
export GHCR_AUTH="<your-ghcr-base64-auth>"
|
||||
# NOTE: Comment out for local builds - images will be built locally without GHCR access.
|
||||
# Only needed when pulling pre-built images from GHCR.
|
||||
# export GHCR_AUTH="<your-ghcr-base64-auth>"
|
||||
|
||||
# LiteLLM/LLMs settings
|
||||
export LITELLM_MASTER_KEY="d5179c62ae1c7366e3ee09775d0993d5" # Generate with: openssl rand -hex 16
|
||||
export LITELLM_MAX_BUDGET="100"
|
||||
export AZURE_API_BASE="<disabled>"
|
||||
export AZURE_API_KEY="<disabled>"
|
||||
export OPENAI_API_KEY="<your-openai-api-key>"
|
||||
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
||||
# export GEMINI_API_KEY="<your-gemini-api-key>"
|
||||
|
||||
# LangFuse settings, instructing LLM-applications to log their LLM traces
|
||||
# export LANGFUSE_ENABLED=true
|
||||
@@ -71,8 +78,11 @@ export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
||||
# export LANGFUSE_SECRET_KEY="<your-langfuse-secret-key>"
|
||||
|
||||
# OpenTelemetry endpoint settings (e.g. SigNoz)
|
||||
export DEPLOY_SIGNOZ=false
|
||||
# Your otel endpoint
|
||||
# export OTEL_ENDPOINT="<your-otel-endpoint>"
|
||||
# export OTEL_TOKEN="<your-otel-http-token>"
|
||||
# Your otel token, including basic or bearer
|
||||
# export OTEL_TOKEN="dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||
# export OTEL_PROTOCOL=grpc
|
||||
|
||||
# Docker Hub credentials for doing logged requests while getting Container
|
||||
@@ -82,3 +92,16 @@ export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
||||
|
||||
# Docker build arguments, useful for local deployment
|
||||
export FUZZER_BASE_IMAGE="gcr.io/oss-fuzz-base/base-runner"
|
||||
|
||||
# Minikube cluster size
|
||||
# NOTE: Docker Desktop typically limits resources. Check Docker Desktop > Settings > Resources.
|
||||
# For macOS Docker Desktop with 8GB RAM allocated to Docker:
|
||||
# - Use MINIKUBE_CPU=4 and MINIKUBE_MEMORY_GB=7
|
||||
# - Some pods may remain Pending due to resource constraints (this is expected)
|
||||
# For larger systems or dedicated Linux machines with 16GB+ RAM:
|
||||
# - Use MINIKUBE_CPU=6 and MINIKUBE_MEMORY_GB=10
|
||||
export MINIKUBE_CPU=6
|
||||
export MINIKUBE_MEMORY_GB=10
|
||||
export MINIKUBE_DISK_GB=80
|
||||
# Increase /dev/shm size if corpus tmpfs is enabled
|
||||
# export MINIKUBE_SHM_SIZE_GB=60
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
charts/dind-daemon-*.tgz
|
||||
charts/pov-reproducer-*.tgz
|
||||
charts/registry-cache-*.tgz
|
||||
charts/scratch-cleaner-*.tgz
|
||||
charts/ui-*.tgz
|
||||
charts/build-bot-*.tgz
|
||||
charts/competition-api-*.tgz
|
||||
charts/coverage-bot-*.tgz
|
||||
|
||||
@@ -2,9 +2,12 @@ dependencies:
|
||||
- name: redis
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 17.17.1
|
||||
- name: dind-daemon
|
||||
repository: file://charts/dind-daemon
|
||||
version: 0.1.0
|
||||
- name: litellm-helm
|
||||
repository: oci://ghcr.io/berriai
|
||||
version: 0.1.615
|
||||
version: 0.1.783
|
||||
- name: task-server
|
||||
repository: file://charts/task-server
|
||||
version: 0.0.1
|
||||
@@ -38,5 +41,20 @@ dependencies:
|
||||
- name: competition-api
|
||||
repository: file://charts/competition-api
|
||||
version: 0.0.1
|
||||
digest: sha256:8ac9c249917d32604f6d59d65bc74e7a7a7eb46b7c71fbb982bd7b0dd038395a
|
||||
generated: "2025-03-26T13:45:05.979619483Z"
|
||||
- name: registry-cache
|
||||
repository: file://charts/registry-cache
|
||||
version: 0.1.0
|
||||
- name: pov-reproducer
|
||||
repository: file://charts/pov-reproducer
|
||||
version: 0.1.0
|
||||
- name: scratch-cleaner
|
||||
repository: file://charts/scratch-cleaner
|
||||
version: 0.0.1
|
||||
- name: ui
|
||||
repository: file://charts/ui
|
||||
version: 0.1.0
|
||||
- name: signoz
|
||||
repository: https://charts.signoz.io
|
||||
version: 0.71.0
|
||||
digest: sha256:d406395eef32321d26f3dee82bd5ab91e80f476dc8f271bac7edbcd04790cc34
|
||||
generated: "2025-09-29T12:14:43.37270009Z"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies:
|
||||
version: "*"
|
||||
repository: "file://charts/dind-daemon"
|
||||
- name: litellm-helm
|
||||
version: "0.1.615"
|
||||
version: "0.1.783"
|
||||
repository: "oci://ghcr.io/berriai"
|
||||
- name: task-server
|
||||
version: "0.0.1"
|
||||
@@ -56,5 +56,9 @@ dependencies:
|
||||
version: "0.0.1"
|
||||
repository: "file://charts/scratch-cleaner"
|
||||
- name: ui
|
||||
version: "0.0.1"
|
||||
version: "0.1.0"
|
||||
repository: "file://charts/ui"
|
||||
- name: signoz
|
||||
version: "0.71.0"
|
||||
repository: "https://charts.signoz.io"
|
||||
condition: signoz.enabled
|
||||
|
||||
@@ -8,7 +8,7 @@ This guide explains how to set up and run the Buttercup system on Kubernetes.
|
||||
- kubectl configured to communicate with your cluster
|
||||
- Helm v3
|
||||
- Access to container registries (ghcr.io)
|
||||
- Access to required API keys (OpenAI, Azure, Anthropic)
|
||||
- Access to required API keys (OpenAI, Azure, Anthropic, Gemini)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
@@ -43,6 +43,8 @@ This is controlled via the `global.environment` setting in the values.yaml or va
|
||||
apiKey: "your-openai-api-key"
|
||||
anthropic:
|
||||
apiKey: "your-anthropic-api-key"
|
||||
gemini:
|
||||
apiKey: "your-google-gemini-key"
|
||||
|
||||
crs:
|
||||
api_key_id: 515cc8a0-3019-4c9f-8c1c-72d0b54ae561
|
||||
@@ -77,6 +79,7 @@ This secret contains API keys for LLM providers:
|
||||
- **AZURE_API_KEY**: Azure OpenAI API key
|
||||
- **OPENAI_API_KEY**: OpenAI API key
|
||||
- **ANTHROPIC_API_KEY**: Anthropic API key
|
||||
- **GEMINI_API_KEY**: Google Gemini API key
|
||||
|
||||
These are populated from the `values-override.yaml` file.
|
||||
|
||||
@@ -197,6 +200,7 @@ The system uses LiteLLM to proxy all LLM requests. The configuration includes mo
|
||||
- Azure OpenAI
|
||||
- OpenAI
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
|
||||
To modify the available models, edit the `litellm-helm.proxy_config.model_list` section in values.yaml.
|
||||
|
||||
|
||||
+3
-2
@@ -5,7 +5,7 @@ metadata:
|
||||
name: competition-echo-egress
|
||||
namespace: tailscale
|
||||
annotations:
|
||||
tailscale.com/tailnet-fqdn: "echo.tail7e9b4c.ts.net" #Basic http echo test server
|
||||
tailscale.com/tailnet-fqdn: "echo.${TAILSCALE_DOMAIN}" #Basic http echo test server
|
||||
spec:
|
||||
externalName: placeholder
|
||||
type: ExternalName
|
||||
@@ -16,7 +16,8 @@ metadata:
|
||||
name: competition-api-1-egress
|
||||
namespace: tailscale
|
||||
annotations:
|
||||
tailscale.com/tailnet-fqdn: "api.tail7e9b4c.ts.net" #Competition API FQDN
|
||||
tailscale.com/tailnet-fqdn: "api.${TAILSCALE_DOMAIN}" #Competition API FQDN
|
||||
spec:
|
||||
externalName: placeholder
|
||||
type: ExternalName
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
namespace: kube-system
|
||||
data:
|
||||
tailscale.server: |
|
||||
tail7e9b4c.ts.net:53 {
|
||||
${TAILSCALE_DOMAIN}:53 {
|
||||
forward . ${TS_DNS_IP}
|
||||
cache 30
|
||||
}
|
||||
|
||||
@@ -8,15 +8,6 @@ resources:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
||||
dind:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
replicaCount: 4
|
||||
timer: 5000
|
||||
logLevel: "DEBUG"
|
||||
|
||||
@@ -13,15 +13,5 @@ resources:
|
||||
cpu: 4000m
|
||||
memory: 8Gi
|
||||
|
||||
dind:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 4000m
|
||||
memory: 8Gi
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
@@ -30,16 +30,17 @@ spec:
|
||||
{{- include "buttercup.standardVolumeMounts" (dict "usesTasksStorage" true) | nindent 8 }}
|
||||
{{- include "buttercup.nodeLocalVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.dockerSocketVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.corpusTmpfsVolumeMount" . | nindent 8 }}
|
||||
env:
|
||||
- name: BUTTERCUP_FUZZER_SAMPLE_SIZE
|
||||
value: "{{ .Values.global.coverageBot.sampleSize }}"
|
||||
{{- include "buttercup.commonEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.nodeData" . | nindent 8 }}
|
||||
{{- include "buttercup.env.dockerSocket" . | nindent 8 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 8 }}
|
||||
{{- include "buttercup.env.corpusTmpfs" . | nindent 8 }}
|
||||
volumes:
|
||||
{{- include "buttercup.volumes.scratch" . | nindent 6 }}
|
||||
{{- include "buttercup.volumes.tasks" . | nindent 6 }}
|
||||
{{- include "buttercup.dockerSocketVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.nodeLocalVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.corpusTmpfsVolume" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
- "ghcr.io"
|
||||
containers:
|
||||
- name: dind
|
||||
image: "docker:24.0.6-dind"
|
||||
image: "docker:29.0.0-dind"
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
@@ -72,4 +72,4 @@ spec:
|
||||
secret:
|
||||
secretName: registry-cache-tls
|
||||
{{- include "buttercup.dockerSocketVolume" . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -30,17 +30,18 @@ spec:
|
||||
periodSeconds: {{ .Values.healthCheck.periodSeconds | default 300 }}
|
||||
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds | default 10 }}
|
||||
failureThreshold: {{ .Values.healthCheck.failureThreshold | default 2 }}
|
||||
env:
|
||||
env:
|
||||
{{- include "buttercup.commonEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.nodeData" . | nindent 8 }}
|
||||
{{- include "buttercup.fuzzerBotEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 8 }}
|
||||
{{- include "buttercup.env.corpusTmpfs" . | nindent 8 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 10 }}
|
||||
volumeMounts:
|
||||
{{- include "buttercup.standardVolumeMounts" (dict "usesTasksStorage" false) | nindent 8 }}
|
||||
{{- include "buttercup.nodeLocalVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.corpusTmpfsVolumeMount" . | nindent 8 }}
|
||||
volumes:
|
||||
{{- include "buttercup.volumes.scratch" . | nindent 6 }}
|
||||
{{- include "buttercup.nodeLocalVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.corpusTmpfsVolume" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -21,16 +21,19 @@ spec:
|
||||
- name: merger-bot
|
||||
image: "{{ .Values.global.fuzzerImage.repository }}:{{ .Values.global.fuzzerImage.tag }}"
|
||||
imagePullPolicy: {{ .Values.global.fuzzerImage.pullPolicy }}
|
||||
command: ["buttercup-corpus-merger", "--crs_scratch_dir", "{{ include "buttercup.nodeLocalCrsScratchPath" . }}", "--redis_url", "{{ include "buttercup.core.redisUrl" . }}", "--timer", "{{ .Values.timer }}", "--timeout", "{{ .Values.timeout }}", "--crash_dir_count_limit", "{{ include "buttercup.core.crashDirCountLimit" . }}", "--max_local_files", "{{ .Values.max_local_files }}"]
|
||||
env:
|
||||
command: ["buttercup-corpus-merger", "--crs_scratch_dir", "{{ include "buttercup.nodeLocalCrsScratchPath" . }}", "--redis_url", "{{ include "buttercup.core.redisUrl" . }}", "--timer", "{{ .Values.timer }}", "--timeout", "{{ .Values.timeout }}", "--crash_dir_count_limit", "{{ include "buttercup.core.crashDirCountLimit" . }}", "--max_local_files", "{{ .Values.max_local_files }}", "--runner_path", "/app/fuzzer_runner/runner.sh"]
|
||||
env:
|
||||
{{- include "buttercup.env.nodeData" . | nindent 8 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 8 }}
|
||||
{{- include "buttercup.env.corpusTmpfs" . | nindent 8 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 10 }}
|
||||
volumeMounts:
|
||||
{{- include "buttercup.standardVolumeMounts" (dict "usesTasksStorage" false) | nindent 8 }}
|
||||
{{- include "buttercup.nodeLocalVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.corpusTmpfsVolumeMount" . | nindent 8 }}
|
||||
volumes:
|
||||
{{- include "buttercup.volumes.scratch" . | nindent 6 }}
|
||||
{{- include "buttercup.nodeLocalVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.corpusTmpfsVolume" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -38,6 +38,7 @@ spec:
|
||||
{{- include "buttercup.patcherEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.dockerSocket" . | nindent 8 }}
|
||||
{{- include "buttercup.env.langfuse" . | nindent 8 }}
|
||||
{{- include "buttercup.env.llm" (merge (dict "secretName" "litellm-api-user" "secretKey" "API_KEY") .) | nindent 8 }}
|
||||
- name: LOG_LEVEL
|
||||
value: "{{ .Values.logLevel }}"
|
||||
volumes:
|
||||
@@ -45,4 +46,5 @@ spec:
|
||||
{{- include "buttercup.volumes.tasks" . | nindent 6 }}
|
||||
{{- include "buttercup.dockerSocketVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.nodeLocalVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.apiKeySecretVolume" (dict "secretName" "litellm-api-user") | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -8,14 +8,5 @@ resources:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
||||
dind:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
logLevel: debug
|
||||
sleepTime: 5
|
||||
|
||||
@@ -56,5 +56,5 @@ resources:
|
||||
cpu: 2000m
|
||||
memory: 2048Mi
|
||||
requests:
|
||||
cpu: 1000m
|
||||
memory: 1024Mi
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
|
||||
@@ -39,13 +39,18 @@ spec:
|
||||
{{- include "buttercup.standardVolumeMounts" (dict "usesTasksStorage" false) | nindent 8 }}
|
||||
{{- include "buttercup.nodeLocalVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.dockerSocketVolumeMount" . | nindent 8 }}
|
||||
{{- include "buttercup.corpusTmpfsVolumeMount" . | nindent 8 }}
|
||||
env:
|
||||
{{- include "buttercup.commonEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.dockerSocket" . | nindent 8 }}
|
||||
{{- include "buttercup.env.langfuse" . | nindent 8 }}
|
||||
{{- include "buttercup.seedGenEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.llm" (merge (dict "secretName" "litellm-api-user" "secretKey" "API_KEY") .) | nindent 8 }}
|
||||
{{- include "buttercup.env.corpusTmpfs" . | nindent 8 }}
|
||||
volumes:
|
||||
{{- include "buttercup.volumes.scratch" . | nindent 6 }}
|
||||
{{- include "buttercup.dockerSocketVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.nodeLocalVolume" . | nindent 6 }}
|
||||
{{- include "buttercup.apiKeySecretVolume" (dict "secretName" "litellm-api-user") | nindent 6 }}
|
||||
{{- include "buttercup.corpusTmpfsVolume" . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -9,15 +9,6 @@ resources:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
dind:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
# Health check configuration
|
||||
healthCheck:
|
||||
# Maximum time (in seconds) the health file can be stale before failing
|
||||
|
||||
Binary file not shown.
@@ -40,8 +40,6 @@ spec:
|
||||
env:
|
||||
{{- include "buttercup.commonEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.dockerSocket" . | nindent 8 }}
|
||||
{{- include "buttercup.env.nodeData" . | nindent 8 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 8 }}
|
||||
volumes:
|
||||
{{- include "buttercup.volumes.scratch" . | nindent 6 }}
|
||||
{{- include "buttercup.volumes.tasks" . | nindent 6 }}
|
||||
|
||||
@@ -16,6 +16,8 @@ spec:
|
||||
app: ui
|
||||
spec:
|
||||
{{- include "buttercup.imagePullSecrets" . | nindent 6 }}
|
||||
initContainers:
|
||||
{{- include "buttercup.waitForDocker" . | nindent 8 }}
|
||||
containers:
|
||||
- name: ui
|
||||
image: "{{ .Values.global.orchestratorImage.repository }}:{{ .Values.global.orchestratorImage.tag }}"
|
||||
@@ -27,11 +29,14 @@ spec:
|
||||
env:
|
||||
{{- include "buttercup.uiEnv" . | nindent 8 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 8 }}
|
||||
{{- include "buttercup.env.dockerSocket" . | nindent 8 }}
|
||||
volumeMounts:
|
||||
- name: data-volume
|
||||
mountPath: /data
|
||||
mountPath: /data/data-volume
|
||||
- name: run-data-volume
|
||||
mountPath: /tmp/buttercup-run-data
|
||||
{{- include "buttercup.volumeMounts.ui_db" . | nindent 10 }}
|
||||
{{- include "buttercup.dockerSocketVolumeMount" . | nindent 10 }}
|
||||
command: ["buttercup-ui"]
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
@@ -59,4 +64,6 @@ spec:
|
||||
- name: run-data-volume
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- include "buttercup.volumes.ui_db" . | nindent 8 }}
|
||||
{{- include "buttercup.dockerSocketVolume" . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-ui
|
||||
annotations:
|
||||
tailscale.com/hostname: "{{ default (printf "%s-ui" .Release.Name) }}"
|
||||
tailscale.com/experimental-forward-cluster-traffic-via-ingress: "true"
|
||||
spec:
|
||||
ingressClassName: tailscale
|
||||
tls:
|
||||
- hosts:
|
||||
- "{{ default (printf "%s-ui" .Release.Name) }}"
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-ui
|
||||
port:
|
||||
number: {{ .Values.service.port }}
|
||||
@@ -5,6 +5,10 @@ Define constants for directories that are used across multiple services
|
||||
/crs_scratch
|
||||
{{- end -}}
|
||||
|
||||
{{- define "buttercup.dirs.ui_db_storage" -}}
|
||||
/ui_db_storage
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define imagePullSecrets for pod specs - using ghcr-auth to match Terraform
|
||||
*/}}
|
||||
@@ -28,12 +32,28 @@ Define the Redis init container template that can be included in multiple deploy
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define the LiteLLM health check init container template
|
||||
Define the LiteLLM health check init container template with secret passed via volume
|
||||
*/}}
|
||||
{{- define "buttercup.waitForLiteLLM" -}}
|
||||
- name: wait-for-litellm
|
||||
image: curlimages/curl:8.6.0
|
||||
command: ['sh', '-c', 'until curl --silent -f http://{{ .Release.Name }}-litellm:4000/health/readiness; do echo waiting for litellm; sleep 2; done;']
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
for i in $(seq 1 60); do
|
||||
if curl --silent -f -H "Authorization: Bearer $(cat /etc/secrets/API_KEY)" -X POST http://{{ .Release.Name }}-litellm:4000/key/health; then
|
||||
exit 0
|
||||
fi
|
||||
echo "waiting for litellm and key to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
echo "litellm or key not ready after 120s"
|
||||
exit 1
|
||||
volumeMounts:
|
||||
- name: api-key-secret
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
@@ -187,3 +207,43 @@ Define a wait-for-docker init container that checks if the Docker socket is avai
|
||||
volumeMounts:
|
||||
{{- include "buttercup.dockerSocketVolumeMount" . | nindent 2 }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define api-key-secret volume with configurable secret name
|
||||
Usage: {{- include "buttercup.apiKeySecretVolume" (dict "secretName" "litellm-api-user") | nindent 8 }}
|
||||
*/}}
|
||||
{{- define "buttercup.apiKeySecretVolume" -}}
|
||||
- name: api-key-secret
|
||||
secret:
|
||||
secretName: {{ .secretName }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Corpus Tmpfs Volume Mount Helper
|
||||
Conditionally adds the volume mount for tmpfs corpus storage if enabled globally.
|
||||
Expects the root context '.' to be passed.
|
||||
Usage: {{- include "buttercup.corpusTmpfsVolumeMount" . | nindent 10 }}
|
||||
*/}}
|
||||
{{- define "buttercup.corpusTmpfsVolumeMount" -}}
|
||||
{{- if .Values.global.volumes.corpusTmpfs.enabled }}
|
||||
- name: corpus-tmpfs
|
||||
mountPath: {{ .Values.global.volumes.corpusTmpfs.mountPath }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Corpus Tmpfs Volume Definition Helper
|
||||
Conditionally adds the volume definition for tmpfs corpus storage if enabled globally.
|
||||
Uses hostPath to mount a node-level tmpfs (shared across all pods on the node).
|
||||
PREREQUISITE: The hostPath must be a tmpfs mount on the node (e.g., /dev/shm or custom tmpfs).
|
||||
Expects the root context '.' to be passed.
|
||||
Usage: {{- include "buttercup.corpusTmpfsVolume" . | nindent 8 }}
|
||||
*/}}
|
||||
{{- define "buttercup.corpusTmpfsVolume" -}}
|
||||
{{- if .Values.global.volumes.corpusTmpfs.enabled }}
|
||||
- name: corpus-tmpfs
|
||||
hostPath:
|
||||
path: {{ .Values.global.volumes.corpusTmpfs.hostPath }}
|
||||
type: {{ .Values.global.volumes.corpusTmpfs.type }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -14,6 +14,12 @@ Define reusable volume specifications for our persistent volumes
|
||||
claimName: {{ .Release.Name }}-tasks-storage
|
||||
{{- end -}}
|
||||
|
||||
{{- define "buttercup.volumes.ui_db" -}}
|
||||
- name: ui-db-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Release.Name }}-ui-db
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define reusable volume mount specifications for our persistent volumes
|
||||
*/}}
|
||||
@@ -26,4 +32,9 @@ Define reusable volume mount specifications for our persistent volumes
|
||||
{{- define "buttercup.volumeMounts.tasks" -}}
|
||||
- name: tasks-storage
|
||||
mountPath: {{ include "buttercup.dirs.tasks_storage" . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "buttercup.volumeMounts.ui_db" -}}
|
||||
- name: ui-db-storage
|
||||
mountPath: {{ include "buttercup.dirs.ui_db_storage" . }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -61,6 +61,13 @@ Define common variables by categories
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "buttercup.env.corpusTmpfs" }}
|
||||
{{- if .Values.global.volumes.corpusTmpfs.enabled }}
|
||||
- name: CORPUS_TMPFS_PATH
|
||||
value: "{{ .Values.global.volumes.corpusTmpfs.mountPath }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "buttercup.env.redis" }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "buttercup.core.redisUrl" . }}
|
||||
@@ -78,19 +85,28 @@ Define Docker Host environment variable for Unix socket
|
||||
- name: BUTTERCUP_LITELLM_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-litellm-api-secrets
|
||||
key: BUTTERCUP_LITELLM_KEY
|
||||
name: {{ .secretName }}
|
||||
key: {{ .secretKey }}
|
||||
- name: BUTTERCUP_LITELLM_HOSTNAME
|
||||
value: {{ include "buttercup.core.litellmEndpoint" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "buttercup.env.telemetry" }}
|
||||
{{- if .Values.global.signoz.deployed }}
|
||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
value: "http://{{ .Release.Name }}-signoz-otel-collector:4317"
|
||||
- name: OTEL_EXPORTER_OTLP_HEADERS
|
||||
value: "Authorization=Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||
- name: OTEL_EXPORTER_OTLP_PROTOCOL
|
||||
value: "grpc"
|
||||
{{- else }}
|
||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
value: "{{ .Values.global.otel.endpoint }}"
|
||||
- name: OTEL_EXPORTER_OTLP_HEADERS
|
||||
value: "Authorization=Basic {{ .Values.global.otel.token }}"
|
||||
value: "Authorization={{ .Values.global.otel.token }}"
|
||||
- name: OTEL_EXPORTER_OTLP_PROTOCOL
|
||||
value: "{{ .Values.global.otel.protocol | default "grpc" }}"
|
||||
{{- end }}
|
||||
- name: CRS_INSTANCE_ID
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -135,7 +151,6 @@ Define persistent log directory environment variable
|
||||
Define common environment for all services
|
||||
*/}}
|
||||
{{- define "buttercup.commonEnv" }}
|
||||
{{- include "buttercup.env.llm" . | nindent 0 }}
|
||||
{{- include "buttercup.env.telemetry" . | nindent 0 }}
|
||||
{{- include "buttercup.env.timeouts" . | nindent 0 }}
|
||||
{{- include "buttercup.env.dirs" . | nindent 0 }}
|
||||
@@ -328,6 +343,8 @@ Service-specific environment variables that utilize the standardized variables
|
||||
value: "{{ include "buttercup.core.logMaxLineLength" . }}"
|
||||
- name: BUTTERCUP_FUZZER_MAX_POV_SIZE
|
||||
value: {{ int .Values.global.maxPovSize | quote }}
|
||||
- name: BUTTERCUP_FUZZER_RUNNER_PATH
|
||||
value: "/app/fuzzer_runner/runner.sh"
|
||||
{{- end }}
|
||||
|
||||
{{- define "buttercup.povReproducerEnv" }}
|
||||
@@ -368,6 +385,8 @@ Service-specific environment variables that utilize the standardized variables
|
||||
value: "/tmp/buttercup-run-data"
|
||||
- name: BUTTERCUP_UI_EXTERNAL_HOST
|
||||
value: {{ .Values.service.external_host | default (printf "%s-ui.%s.svc.cluster.local" .Release.Name .Release.Namespace) | quote }}
|
||||
- name: BUTTERCUP_UI_DATABASE_URL
|
||||
value: "sqlite:///{{ include "buttercup.dirs.ui_db_storage" . }}/buttercup_ui.db"
|
||||
- name: BUTTERCUP_UI_HOST
|
||||
value: {{ .Values.service.host | default "0.0.0.0" | quote }}
|
||||
- name: BUTTERCUP_UI_PORT
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user