capa 0.5.2

File capability extractor.
Documentation
name: Release

on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to build artifacts for (e.g. v0.3.21). Must already exist."
        required: true

permissions:
  contents: write   # for creating GitHub releases + uploading assets

env:
  CARGO_TERM_COLOR: always

jobs:
  verify:
    name: verify build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo fmt --all -- --check
      - run: cargo clippy --all-targets --all-features -- -D warnings
      - run: cargo test --all-features

  package-source:
    name: package source + cargo .crate
    needs: verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: Determine tag
        id: tag
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
          fi

      # `cargo package` builds the exact .crate file that would be uploaded
      # to crates.io if/when we publish manually. Shipping it lets downstream
      # consumers verify what's on crates.io matches the tagged source.
      - name: Build cargo package
        run: cargo package --allow-dirty --no-verify

      - name: Stage source artifacts
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          mkdir -p release

          cp target/package/capa-*.crate "release/capa-${tag}.crate"

          src_name="capa-${tag}-src"
          git archive --format=tar.gz --prefix="${src_name}/" \
            -o "release/${src_name}.tar.gz" HEAD

          (cd release && \
            for f in *.crate *.tar.gz; do
              sha256sum "$f" > "$f.sha256"
            done)

          ls -la release

      - name: Upload source artifacts
        uses: actions/upload-artifact@v4
        with:
          name: capa-source-artifacts
          path: |
            release/*.crate
            release/*.tar.gz
            release/*.sha256
          if-no-files-found: error

  # 0.4.3: separate `flirt-sigs.tar.gz` artifact shipped alongside the
  # CLI binaries. The `flirt-sigs/` directory in the repo is ~70 MB
  # raw (~25-35 MB compressed) — too big for crates.io, but a
  # one-time download for CLI users. Single tarball is platform-
  # independent; users extract once and point `--signatures` at the
  # extracted path.
  package-flirt-sigs:
    name: package flirt-sigs tarball
    needs: verify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Determine tag
        id: tag
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
          fi

      - name: Build flirt-sigs tarball
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          mkdir -p release
          if [ ! -d flirt-sigs ]; then
            echo "flirt-sigs/ directory missing from checkout — refusing to ship empty bundle"
            exit 1
          fi
          # Tar from the parent so the archive extracts to a clean
          # `flirt-sigs/` directory at the user's chosen path.
          tar -czf "release/flirt-sigs-${tag}.tar.gz" flirt-sigs/
          (cd release && sha256sum "flirt-sigs-${tag}.tar.gz" > "flirt-sigs-${tag}.tar.gz.sha256")
          ls -lh release

      - name: Upload flirt-sigs artifact
        uses: actions/upload-artifact@v4
        with:
          name: flirt-sigs
          path: |
            release/flirt-sigs-*.tar.gz
            release/flirt-sigs-*.sha256
          if-no-files-found: error

  build-cli:
    name: build capa_cli — ${{ matrix.target }}
    needs: verify
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            cross: false
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            cross: true
          - target: x86_64-apple-darwin
            os: macos-latest
            cross: false
          - target: aarch64-apple-darwin
            os: macos-latest
            cross: false
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            cross: false
          - target: aarch64-pc-windows-msvc
            os: windows-latest
            cross: false
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust + target
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      # Linux aarch64 needs a cross linker / sysroot. The simplest way that
      # doesn't pull in `cross` (and Docker-in-Docker on CI) is the apt
      # gcc-aarch64-linux-gnu toolchain + a cargo config that uses it as
      # the linker for the target.
      - name: Install aarch64-linux cross toolchain
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          mkdir -p .cargo
          cat >> .cargo/config.toml <<'EOF'
          [target.aarch64-unknown-linux-gnu]
          linker = "aarch64-linux-gnu-gcc"
          EOF

      - name: Determine tag
        id: tag
        shell: bash
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
          fi

      - name: Build capa_cli (release)
        run: cargo build --release --example capa_cli --target ${{ matrix.target }} --all-features

      - name: Stage binary
        shell: bash
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          target="${{ matrix.target }}"
          mkdir -p release
          # The example binary lives at target/<triple>/release/examples/<name>(.exe).
          if [[ "$target" == *windows* ]]; then
            src="target/${target}/release/examples/capa_cli.exe"
            dst="release/capa_cli-${tag}-${target}.exe"
          else
            src="target/${target}/release/examples/capa_cli"
            dst="release/capa_cli-${tag}-${target}"
          fi
          cp "$src" "$dst"
          # SHA-256 — `sha256sum` exists on Linux/macOS, `certutil` on Windows.
          if command -v sha256sum >/dev/null 2>&1; then
            (cd release && sha256sum "$(basename "$dst")" > "$(basename "$dst").sha256")
          else
            (cd release && certutil -hashfile "$(basename "$dst")" SHA256 | \
              grep -v ':' | head -1 | tr -d ' \r' > "$(basename "$dst").sha256")
          fi
          ls -la release

      - name: Upload binary
        uses: actions/upload-artifact@v4
        with:
          name: capa_cli-${{ matrix.target }}
          path: |
            release/capa_cli-*
            release/capa_cli-*.sha256
          if-no-files-found: error

  github-release:
    name: create GitHub release
    needs: [package-source, package-flirt-sigs, build-cli]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Determine tag
        id: tag
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
          fi

      - name: Generate release notes
        id: notes
        run: |
          tag="${{ steps.tag.outputs.tag }}"
          previous=$(git describe --tags --abbrev=0 "${tag}^" 2>/dev/null || echo "")
          {
            echo "notes<<EOF"
            if [ -n "$previous" ]; then
              echo "## Changes since $previous"
              echo
              git log --pretty=format:"- %s (%h)" "${previous}..${tag}"
            else
              echo "## Initial release"
              echo
              git log --pretty=format:"- %s (%h)" "${tag}"
            fi
            echo
            echo
            echo "## Source"
            echo
            echo "- \`capa-${tag}.crate\` — the cargo package archive (same bytes as crates.io)"
            echo "- \`capa-${tag}-src.tar.gz\` — plain git source tarball"
            echo
            echo "## CLI binaries"
            echo
            echo "Pre-built \`capa_cli\` binaries for each supported platform:"
            echo
            echo "| Platform                  | Binary                                             |"
            echo "|---------------------------|----------------------------------------------------|"
            echo "| Linux x86_64              | \`capa_cli-${tag}-x86_64-unknown-linux-gnu\`        |"
            echo "| Linux aarch64             | \`capa_cli-${tag}-aarch64-unknown-linux-gnu\`       |"
            echo "| macOS Intel (x86_64)      | \`capa_cli-${tag}-x86_64-apple-darwin\`             |"
            echo "| macOS Apple Silicon       | \`capa_cli-${tag}-aarch64-apple-darwin\`            |"
            echo "| Windows x86_64            | \`capa_cli-${tag}-x86_64-pc-windows-msvc.exe\`      |"
            echo "| Windows aarch64           | \`capa_cli-${tag}-aarch64-pc-windows-msvc.exe\`     |"
            echo
            echo "Every artifact ships with a matching \`*.sha256\` checksum."
            echo
            echo "## FLIRT signatures (optional, for library-function recognition)"
            echo
            echo "- \`flirt-sigs-${tag}.tar.gz\` — Mandiant FLARE + Maktm FLIRTDB \`.sig\` corpora (~25-35 MB compressed, ~70 MB extracted)"
            echo
            echo "Extract anywhere and pass \`--signatures /path/to/flirt-sigs\` to \`capa_cli\`. Optional — analysis works without it, but stripped MSVC binaries produce cleaner output when FLIRT is wired up. Signature content remains the work of Mandiant FLARE and Michael Kiros (Maktm); see \`flirt-sigs/README.md\` for licensing and credits."
            echo
            echo "Publishing to crates.io is performed manually with \`cargo publish\` and is not part of this workflow."
            echo EOF
          } >> "$GITHUB_OUTPUT"

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Flatten artifact tree
        run: |
          mkdir -p release
          # Each artifacts/<name>/ folder contains files we want at release/ top-level.
          find artifacts -type f \( -name 'capa-*' -o -name 'capa_cli-*' -o -name 'flirt-sigs-*' \) -exec cp {} release/ \;
          ls -la release

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.tag.outputs.tag }}
          name: ${{ steps.tag.outputs.tag }}
          body: ${{ steps.notes.outputs.notes }}
          files: release/*
          draft: false
          prerelease: ${{ contains(steps.tag.outputs.tag, '-') }}