diff --git a/.github/actions/build-package/action.yml b/.github/actions/build-package/action.yml new file mode 100644 index 00000000000..2c89a9a493d --- /dev/null +++ b/.github/actions/build-package/action.yml @@ -0,0 +1,36 @@ +name: Build and package (Bun -> single zip) +description: Build with Bun (Turbo) and package a single distributable archive +outputs: + archive_path: + description: Absolute path to the archive + value: ${{ steps.pkg.outputs.archive_path }} +runs: + using: composite + steps: + - name: Setup Bun (from .bun-version) + uses: ./.github/actions/setup-bun + + - name: Build (Turbo) + shell: bash + run: | + if ! OPENCODE_CHANNEL=latest bunx turbo run build --env-mode=loose; then + echo "Turbo build failed; continuing with source packaging for security scan." + fi + + - name: Ensure zip is available + shell: bash + run: sudo apt-get update -y && sudo apt-get install -y zip + + - name: Package single file + id: pkg + shell: bash + run: | + set -e + mkdir -p bundle + if [ -d dist ]; then SRC=dist; elif [ -d build ]; then SRC=build; else SRC=.; fi + if [ "$SRC" = "." ]; then + zip -r bundle/opencode.zip . -x '.git/*' '.github/*' 'node_modules/*' + else + (cd "$SRC" && zip -r ../bundle/opencode.zip .) + fi + echo "archive_path=$(pwd)/bundle/opencode.zip" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/clam-av.yml b/.github/workflows/clam-av.yml new file mode 100644 index 00000000000..a98562c68b0 --- /dev/null +++ b/.github/workflows/clam-av.yml @@ -0,0 +1,82 @@ +name: av-clamav +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + actions: read + +jobs: + clamav: + runs-on: ubuntu-latest + steps: + # Checkout the right ref + - name: Checkout (release tag) + if: github.event_name == 'release' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + - name: Checkout (PR/default) + if: github.event_name != 'release' + uses: actions/checkout@v4 + + # Single source-of-truth build -> one file + - name: Build and package + id: build + uses: ./.github/actions/build-package + + # Install fresh ClamAV DB + - name: Install & update ClamAV DB + run: | + set -e + sudo apt-get update + sudo apt-get install -y clamav clamav-freshclam unzip + sudo systemctl stop clamav-freshclam || true + sudo mkdir -p /var/lib/clamav + sudo chown -R clamav:clamav /var/lib/clamav + sudo freshclam --verbose + ls -lh /var/lib/clamav + + # Scan extracted bundle so counts reflect actual files + - name: Verify ClamAV detects EICAR signature + run: | + set -euo pipefail + printf 'X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > eicar.com + status=0 + clamscan eicar.com > eicar.log || status=$? + cat eicar.log + if [ "$status" -ne 1 ]; then + echo "ClamAV failed to report the EICAR signature" >&2 + exit 1 + fi + grep -q 'eicar.com: Eicar-Test-Signature FOUND' eicar.log + grep -q 'Infected files: 1' eicar.log + rm -f eicar.com eicar.log + + - name: Extract bundle and scan + run: | + set -euo pipefail + rm -rf scan && mkdir -p scan + unzip -q bundle/opencode.zip -d scan + echo "File count in payload: $(find scan -type f | wc -l)" + clamscan -ri --scan-archive=yes scan | tee clamav.log + if grep -qE 'Infected files: [1-9][0-9]*' clamav.log; then + findings=$(grep 'FOUND' clamav.log | grep -v 'Eicar-Test-Signature' || true) + if [ -n "${findings}" ]; then + echo "Unexpected detections found:" >&2 + echo "${findings}" >&2 + exit 1 + fi + echo 'Only EICAR detections observed; continuing.' + fi + + - name: Upload scan results + if: ${{ env.ACTIONS_RUNTIME_TOKEN != '' }} + uses: actions/upload-artifact@v4 + with: + name: clamav-scan-results + path: | + clamav.log + bundle/opencode.zip diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml new file mode 100644 index 00000000000..cb7056d9db5 --- /dev/null +++ b/.github/workflows/dependency-security.yml @@ -0,0 +1,74 @@ +name: dependency-security +on: + pull_request: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + bun-audit: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + + - name: Run Bun audit (prod, high+) + run: bun audit --prod --audit-level=high + + trivy-release: + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Checkout (release tag) + if: github.event_name == 'release' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + - name: Checkout (default) + if: github.event_name != 'release' + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scan + uses: aquasecurity/trivy-action@0.34.1 + with: + version: v0.69.1 + scan-type: fs + scan-ref: . + scanners: vuln + vuln-type: library + severity: HIGH,CRITICAL + ignore-unfixed: true + format: sarif + output: trivy-results.sarif + exit-code: "1" + + - name: Run Trivy misconfig and secret scan + uses: aquasecurity/trivy-action@0.34.1 + with: + version: v0.69.1 + scan-type: fs + scan-ref: . + scanners: misconfig,secret + severity: HIGH,CRITICAL + skip-dirs: node_modules,packages/opencode/dist,tree-sitter-scheme + format: sarif + output: trivy-policy-results.sarif + exit-code: "0" + + - name: Upload Trivy report + if: ${{ always() && env.ACTIONS_RUNTIME_TOKEN != '' }} + uses: actions/upload-artifact@v4 + with: + name: trivy-release-report + path: | + trivy-results.sarif + trivy-policy-results.sarif