diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 00000000..d6f8a88c --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,52 @@ +# ClusterFuzzLite builder image for edge-ai fuzz harnesses. +# +# Single-Dockerfile, single-base-image pattern (issue #459): the CFLite GitHub +# Action does NOT forward its `language:` input as a Docker build-arg, so a +# `LANGUAGE`-parameterized base image cannot be selected at build time. Instead +# we build on top of `base-builder-rust` (which contains the OSS-Fuzz tooling +# plus a working python3) and dispatch to per-language `build_.sh` +# scripts via the shared `build.sh`. The OSS-Fuzz `compile` step uses the +# runtime `FUZZING_LANGUAGE` env var (set by CFLite from `language:`) to know +# how to wrap each harness. +# +# IMPORTANT: do NOT pin a base-image tag (e.g. :ubuntu-24-04). The +# ClusterFuzzLite GitHub Action runner is Ubuntu 20.04 (glibc 2.31). The +# 24.04-tagged base produces binaries linked against glibc >= 2.32 which fail +# bad_build_check on the runner ("GLIBC_2.32 not found"). The default tag +# tracks the runner's glibc. +FROM gcr.io/oss-fuzz-base/base-builder-rust + +ENV DEBIAN_FRONTEND=noninteractive + +# Node.js (and npm) are required by build_js.sh to run `npm ci` and `npx jazzer` +# for JavaScript fuzz harnesses. The base-builder-rust image does not include +# Node. Install Node 20.x from the official prebuilt linux-x64 tarball on +# nodejs.org instead of apt: the CFLite build container cannot reliably reach +# Ubuntu's archive/security mirrors (port 80 to archive.ubuntu.com and +# security.ubuntu.com regularly times out from the runner), but HTTPS to +# nodejs.org works. curl + ca-certificates are already present in the base +# image. +ARG NODE_VERSION=20.18.1 +# hadolint ignore=DL3003 +RUN curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \ + && tar -xJf "node-v${NODE_VERSION}-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ + && rm "node-v${NODE_VERSION}-linux-x64.tar.xz" \ + && node --version \ + && npm --version + +# atheris must be present in the image before the OSS-Fuzz `compile` step runs +# (compile imports the Python fuzz targets to wrap them, which fails if atheris +# is not yet installed). Install unconditionally: the ClusterFuzzLite GitHub +# Action does not forward its `language:` input as a Docker build-arg, so the +# `LANGUAGE` ARG above is always the default at image-build time and a +# conditional install would never fire for python harnesses. +# hadolint ignore=DL3013 +RUN python3 -m pip install --no-cache-dir atheris pyinstaller + +COPY . $SRC/edge-ai +COPY .clusterfuzzlite/build.sh $SRC/build.sh +COPY .clusterfuzzlite/build_rust.sh $SRC/build_rust.sh +COPY .clusterfuzzlite/build_python.sh $SRC/build_python.sh +COPY .clusterfuzzlite/build_js.sh $SRC/build_js.sh + +WORKDIR $SRC/edge-ai diff --git a/.clusterfuzzlite/README.md b/.clusterfuzzlite/README.md new file mode 100644 index 00000000..ca62e199 --- /dev/null +++ b/.clusterfuzzlite/README.md @@ -0,0 +1,72 @@ +# ClusterFuzzLite Builder Containers + +This directory configures the [ClusterFuzzLite][cflite] (CFLite) builder +containers and per-language fuzz harness build scripts for `edge-ai`. + +## Architecture + +CFLite expects a single `Dockerfile` and a single `build.sh` at the project +root (here, `.clusterfuzzlite/`). The `build_fuzzers` GitHub Action does not +expose a `dockerfile-path` input, so all three languages share one Dockerfile +parameterized by an `ARG LANGUAGE` build-arg that the action forwards from its +`language:` workflow input. + +* `Dockerfile` — selects `gcr.io/oss-fuzz-base/base-builder-${LANGUAGE}` at + build time. Default `LANGUAGE=rust` preserves the historical behavior. +* `build.sh` — top-level dispatcher. Inspects the `LANGUAGE` env var (also set + by the action) and execs the matching `build_.sh`. +* `build_rust.sh` / `build_python.sh` / `build_js.sh` — per-language harness + builders. + +The base image tag is intentionally unpinned: the CFLite Action runner is +Ubuntu 20.04 (glibc 2.31), and pinning a newer tag (e.g. `:ubuntu-24-04`) +produces binaries linked against glibc >= 2.32 that fail `bad_build_check` +on the runner. + +## Language toolchains + +| Language | Engine | Build pattern | +|--------------|------------|------------------------------------------------------| +| `rust` | cargo-fuzz | `cargo +nightly fuzz build` per harness, copy to OUT | +| `python` | Atheris | `pyinstaller --onefile` + ASAN-aware bash wrapper | +| `javascript` | Jazzer.js | `npm ci` in the harness service + `npx jazzer` shim | + +## Adding a Python harness + +1. Place the harness at `tests/fuzz/fuzz_.py` inside the target service. +2. Append an entry to the `HARNESSES` array in [`build_python.sh`](./build_python.sh) + in the form `::`. +3. Ensure the service ships a `requirements.txt` so transitive deps are + installed before `pyinstaller` runs. +4. If a new top-level component number is introduced, add a matching + `fuzz-py-` flag to [`codecov.yml`](../codecov.yml). + +## Adding a JavaScript harness + +1. Place the harness at `tests/fuzz/fuzz_.mjs` inside the target service. +2. Append an entry to the `HARNESSES` array in [`build_js.sh`](./build_js.sh). +3. Ensure the service has a committed `package-lock.json` so `npm ci` succeeds + reproducibly. +4. Declare `@jazzer.js/core` in the service's `devDependencies` for local + repro and editor IntelliSense (CFLite preinstalls it globally in the base + image, but the manifest entry keeps repos buildable outside CFLite). + +## Lint waivers + +* `Dockerfile` carries a `# hadolint ignore=DL3006` directive on the + `FROM gcr.io/oss-fuzz-base/base-builder-${LANGUAGE}` line. The base image + tag is intentionally unpinned (see Architecture above); pinning to a + specific Ubuntu release breaks `bad_build_check` on the CFLite runner. + +## Known limitations + +* The `506-ros2-connector` harness fuzzes the in-process message registry + only (paho-mqtt + pure-Python typed accessors). Fuzzing the full ROS 2 + bridge is out of scope because `rclpy` is not installable from PyPI; that + work would require a derived base image. +* Atheris 3.0.0 pins Python 3.11. Upgrading the CFLite base image to Ubuntu + 24.04 (issue [#454][i454]) cannot proceed without an Atheris 3.12 wheel or + a replacement Python fuzzing engine. + +[cflite]: https://google.github.io/clusterfuzzlite/ +[i454]: https://github.com/microsoft/edge-ai/issues/454 diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 00000000..34789c5d --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# ClusterFuzzLite top-level build dispatcher. Selects the per-language +# builder based on the OSS-Fuzz canonical `FUZZING_LANGUAGE` env var, which +# is set by the CFLite action's `language:` input on the inner `compile` +# container. `LANGUAGE` is accepted as a fallback for local repro convenience +# and defaults to rust to preserve historical behavior when neither is set. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +case "${FUZZING_LANGUAGE:-${LANGUAGE:-rust}}" in + rust) bash "${SCRIPT_DIR}/build_rust.sh" ;; + python) bash "${SCRIPT_DIR}/build_python.sh" ;; + javascript) bash "${SCRIPT_DIR}/build_js.sh" ;; + *) + echo "build.sh: unsupported FUZZING_LANGUAGE='${FUZZING_LANGUAGE:-${LANGUAGE:-}}' (expected rust|python|javascript)" >&2 + exit 1 + ;; +esac diff --git a/.clusterfuzzlite/build_js.sh b/.clusterfuzzlite/build_js.sh new file mode 100644 index 00000000..88126a74 --- /dev/null +++ b/.clusterfuzzlite/build_js.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Build JavaScript Jazzer.js fuzz harnesses. The base-builder-javascript image +# preinstalls @jazzer.js/core globally; we additionally install the harness +# service's npm dependencies so any local imports resolve at runtime. +set -euo pipefail + +: "${OUT:?OUT must be set by ClusterFuzzLite}" +: "${SRC:?SRC must be set by ClusterFuzzLite}" + +# Format: ":" +HARNESSES=( + "fuzz_processAlerts_513:$SRC/edge-ai/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_processAlerts.mjs" + "fuzz_smoke_513:$SRC/edge-ai/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_smoke.mjs" +) + +if [[ ${#HARNESSES[@]} -eq 0 ]]; then + echo "build_js.sh: no JavaScript fuzz harnesses configured" + exit 0 +fi + +# Install npm dependencies for harness service(s) so local module resolution +# works inside the wrapper at fuzz time. +pushd "${SRC}/edge-ai/src/500-application/513-tiered-notification-service" >/dev/null +npm install --no-audit --no-fund +popd >/dev/null + +for entry in "${HARNESSES[@]}"; do + harness_name="${entry%%:*}" + harness_path="${entry#*:}" + out_path="${OUT}/${harness_name}" + svc_dir="$(dirname "$(dirname "$(dirname "${harness_path}")")")" + + # Run the harness in place so its relative imports (e.g. ../../src/...) + # resolve against the original service tree instead of a flattened OUT dir. + cat >"${out_path}" <::" +HARNESSES=( + "fuzz_models_505:src/500-application/505-akri-rest-http-connector/services/sensor-simulator:tests/fuzz/fuzz_models.py" + "fuzz_message_registry_506:src/500-application/506-ros2-connector/services/ros2-connector:tests/fuzz/fuzz_message_registry.py" + "fuzz_process_event_509:src/500-application/509-sse-connector/services/connector-test-client:tests/fuzz/fuzz_process_event.py" + "fuzz_soap_parser_510:src/500-application/510-onvif-connector/services/onvif-camera-simulator:tests/fuzz/fuzz_soap_parser.py" + "fuzz_smoke_505:src/500-application/505-akri-rest-http-connector/services/sensor-simulator:tests/fuzz/fuzz_smoke.py" + "fuzz_smoke_509:src/500-application/509-sse-connector/services/connector-test-client:tests/fuzz/fuzz_smoke.py" + "fuzz_smoke_510:src/500-application/510-onvif-connector/services/onvif-camera-simulator:tests/fuzz/fuzz_smoke.py" +) + +if [[ ${#HARNESSES[@]} -eq 0 ]]; then + echo "build_python.sh: no Python fuzz harnesses configured" + exit 0 +fi + +# atheris and pyinstaller are pre-installed in the Dockerfile for the python +# language path so they are available before the OSS-Fuzz `compile` wrapper +# runs. Local-repro fallbacks below cover environments using a stock +# base-builder-python image. +if ! command -v pyinstaller >/dev/null 2>&1; then + python3 -m pip install --no-cache-dir pyinstaller +fi + +if ! python3 -c "import atheris" >/dev/null 2>&1; then + python3 -m pip install --no-cache-dir atheris +fi + +for entry in "${HARNESSES[@]}"; do + IFS=':' read -r harness_name svc_dir harness_rel <<<"${entry}" + svc_path="${SRC}/edge-ai/${svc_dir}" + + pushd "${svc_path}" >/dev/null + if [[ -f requirements.txt ]]; then + pip3 install --no-cache-dir -r requirements.txt + fi + # Only collect src.message_types submodules for services that actually ship + # that package (currently 506-ros2-connector). PyInstaller aborts when asked + # to collect a non-existent package. + extra_args=() + if [[ -d "${svc_path}/src/message_types" ]]; then + extra_args+=(--collect-all src.message_types --add-data "${svc_path}/src/message_types:src/message_types") + fi + pyinstaller \ + --distpath "${OUT}" \ + --onefile \ + --name "${harness_name}.pkg" \ + --paths "${svc_path}" \ + "${extra_args[@]}" \ + "${svc_path}/${harness_rel}" + popd >/dev/null + + # The `LLVMFuzzerTestOneInput` marker below is required: OSS-Fuzz's + # bad_build_check greps each target file for that string to recognize it as + # a libFuzzer-compatible fuzz target. Without it, the build is rejected with + # "No fuzz targets found in out dir." + cat >"${OUT}/${harness_name}" </dev/null + cargo "+${RUST_TOOLCHAIN}" fuzz build -O + popd >/dev/null + + target_dir="${crate_dir}/fuzz/target/x86_64-unknown-linux-gnu/release" + if [[ ! -d "${target_dir}" ]]; then + echo "build_rust.sh: expected build output ${target_dir} not found" >&2 + exit 1 + fi + + for binary in "${target_dir}"/*; do + [[ -f "${binary}" && -x "${binary}" ]] || continue + harness_name="$(basename "${binary}")" + cp "${binary}" "${OUT}/${crate_name}_${harness_name}" + + corpus_dir="${fuzz_dir}/corpus/${harness_name}" + if [[ -d "${corpus_dir}" ]]; then + (cd "${corpus_dir}" && zip -qr "${OUT}/${crate_name}_${harness_name}_seed_corpus.zip" .) + fi + done +done diff --git a/.cspell.json b/.cspell.json index 1a47ffe0..2e900967 100644 --- a/.cspell.json +++ b/.cspell.json @@ -33,5 +33,5 @@ } ], "dictionaries": ["k8s", "docker", "rust", "data-science", "aws", "terraform", "azure-services", "iot-operations", "microsoft-sample-companies", "industry-acronyms", "project-specific", "general-technical"], - "words": ["Acks", "analyzability", "behaviour", "cdylib", "Chronograf", "edgeai", "GHCP", "Kapacitor", "Kata", "Katas", "learning", "msazure", "myorg", "myorga", "myorgb", "SARIF", "Segoe", "shuf", "westeurope"] + "words": ["Acks", "analyzability", "ASAN", "Atheris", "atheris", "behaviour", "cdylib", "cflite", "Chronograf", "clusterfuzzlite", "distpath", "edgeai", "EUSAGE", "GHCP", "Kapacitor", "Kata", "Katas", "learning", "libfuzzer", "msazure", "myorg", "myorga", "myorgb", "onefile", "preinstalls", "pyinstaller", "SARIF", "Segoe", "shuf", "symbolizer", "westeurope"] } diff --git a/.github/workflows/fuzz-pr.yml b/.github/workflows/fuzz-pr.yml new file mode 100644 index 00000000..3a42daa1 --- /dev/null +++ b/.github/workflows/fuzz-pr.yml @@ -0,0 +1,145 @@ +# Fuzz Workflow (Reusable) +# Purpose: +# Runs ClusterFuzzLite fuzzers on changed Rust/Python/JS targets. +# Invoked from pr-validation.yml and main.yml as a reusable workflow. +# Wave 2 soft-fail: continue-on-error keeps checks green while fuzzing is bedded in. +# Uploads SARIF to GHAS and per-component coverage to Codecov. +--- +name: Fuzz + +on: # yamllint disable-line rule:truthy + workflow_call: + +permissions: + contents: read + +jobs: + detect-changes: + name: Detect fuzz target changes + permissions: + contents: read + actions: read + uses: ./.github/workflows/matrix-folder-check.yml + with: + includeFuzzTargets: true + displayName: 'Check for fuzz target changes' + + fuzz-rust: + name: Fuzz Rust + needs: [detect-changes] + if: needs.detect-changes.outputs.changesInFuzzRust == 'true' && fromJson(needs.detect-changes.outputs.changedFuzzRustFolders).folderName[0] != null + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + security-events: write + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-changes.outputs.changedFuzzRustFolders) }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Build fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + language: rust + sanitizer: address + - name: Run fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + fuzz-seconds: 300 + mode: code-change + sanitizer: address + output-sarif: true + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + use_oidc: true + flags: fuzz-${{ matrix.folderName }} + fail_ci_if_error: false + + fuzz-python: + name: Fuzz Python + needs: [detect-changes] + if: needs.detect-changes.outputs.changesInFuzzPython == 'true' && fromJson(needs.detect-changes.outputs.changedFuzzPythonFolders).folderName[0] != null + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + security-events: write + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-changes.outputs.changedFuzzPythonFolders) }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Build fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + language: python + sanitizer: address + - name: Run fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + fuzz-seconds: 300 + mode: code-change + sanitizer: address + output-sarif: true + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + use_oidc: true + flags: fuzz-py-${{ matrix.folderName }} + fail_ci_if_error: false + + fuzz-js: + name: Fuzz JavaScript + needs: [detect-changes] + if: needs.detect-changes.outputs.changesInFuzzJs == 'true' && fromJson(needs.detect-changes.outputs.changedFuzzJsFolders).folderName[0] != null + runs-on: ubuntu-latest + continue-on-error: true + permissions: + contents: read + security-events: write + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.detect-changes.outputs.changedFuzzJsFolders) }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Build fuzzers + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + language: javascript + sanitizer: coverage + - name: Run fuzzers + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1 + with: + fuzz-seconds: 300 + mode: code-change + sanitizer: coverage + output-sarif: true + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + use_oidc: true + flags: fuzz-js-${{ matrix.folderName }} + fail_ci_if_error: false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5fc560a0..3edf4598 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -173,6 +173,17 @@ jobs: break-build: false secrets: inherit + # ClusterFuzzLite fuzzing for changed Rust/Python/JS fuzz targets (main) + fuzz-main: + name: Fuzz + permissions: + contents: read + security-events: write + id-token: write + actions: read + uses: ./.github/workflows/fuzz-pr.yml + secrets: inherit + # Call docs-check-terraform workflow docs-check-terraform-main: name: Terraform Documentation Check diff --git a/.github/workflows/matrix-folder-check.yml b/.github/workflows/matrix-folder-check.yml index b258c6dc..3683a9bd 100644 --- a/.github/workflows/matrix-folder-check.yml +++ b/.github/workflows/matrix-folder-check.yml @@ -98,6 +98,11 @@ on: # yamllint disable-line rule:truthy required: false default: false type: boolean + includeFuzzTargets: + description: 'Whether to detect changes in fuzz targets (Rust/Python/JS)' + required: false + default: false + type: boolean outputs: # Backward compatibility outputs with legacy names changesInRpEnablementShell: @@ -124,6 +129,24 @@ on: # yamllint disable-line rule:truthy changedApplicationFolders: description: 'JSON matrix of Application folders that have changed' value: ${{ jobs.map-outputs.outputs.changedApplicationFolders }} + changesInFuzzRust: + description: 'Whether any Rust fuzz target sources have changed' + value: ${{ jobs.map-outputs.outputs.changesInFuzzRust }} + changedFuzzRustFolders: + description: 'JSON matrix of Rust fuzz folders that have changed' + value: ${{ jobs.map-outputs.outputs.changedFuzzRustFolders }} + changesInFuzzPython: + description: 'Whether any Python fuzz target sources have changed' + value: ${{ jobs.map-outputs.outputs.changesInFuzzPython }} + changedFuzzPythonFolders: + description: 'JSON matrix of Python fuzz folders that have changed' + value: ${{ jobs.map-outputs.outputs.changedFuzzPythonFolders }} + changesInFuzzJs: + description: 'Whether any JavaScript fuzz target sources have changed' + value: ${{ jobs.map-outputs.outputs.changesInFuzzJs }} + changedFuzzJsFolders: + description: 'JSON matrix of JavaScript fuzz folders that have changed' + value: ${{ jobs.map-outputs.outputs.changedFuzzJsFolders }} changesInRust: description: 'Whether any Rust-relevant files have changed (gates rust-tests)' value: ${{ jobs.map-outputs.outputs.changesInRust }} @@ -144,6 +167,12 @@ jobs: changedBicepFolders: ${{ steps.detect.outputs.changedBicepFolders }} changesInApplications: ${{ steps.detect.outputs.changesInApplications }} changedApplicationFolders: ${{ steps.detect.outputs.changedApplicationFolders }} + changesInFuzzRust: ${{ steps.detect.outputs.changesInFuzzRust }} + changedFuzzRustFolders: ${{ steps.detect.outputs.changedFuzzRustFolders }} + changesInFuzzPython: ${{ steps.detect.outputs.changesInFuzzPython }} + changedFuzzPythonFolders: ${{ steps.detect.outputs.changedFuzzPythonFolders }} + changesInFuzzJs: ${{ steps.detect.outputs.changesInFuzzJs }} + changedFuzzJsFolders: ${{ steps.detect.outputs.changedFuzzJsFolders }} changesInRust: ${{ steps.detect.outputs.changesInRust }} steps: - name: Checkout repository @@ -155,18 +184,23 @@ jobs: id: detect shell: pwsh run: | - # Build parameters for the PowerShell script - $scriptArgs = @() + # Build parameters for the PowerShell script (hashtable splat for named-parameter binding) + $baseRef = if ($env:GITHUB_BASE_REF) { $env:GITHUB_BASE_REF } else { 'main' } + $scriptArgs = @{ BaseBranch = "origin/$baseRef" } if ("${{ inputs.includeIaCFolders }}" -eq 'true') { - $scriptArgs += '-IncludeIaCFolders' + $scriptArgs['IncludeAllIaC'] = $true } if ("${{ inputs.includeApplications }}" -eq 'true') { - $scriptArgs += '-IncludeApplications' + $scriptArgs['IncludeAllApplications'] = $true } - Write-Host "Running folder change detection with parameters: $($scriptArgs -join ' ')" + if ("${{ inputs.includeFuzzTargets }}" -eq 'true') { + $scriptArgs['IncludeFuzzTargets'] = $true + } + + Write-Host "Running folder change detection with parameters: $($scriptArgs | ConvertTo-Json -Compress)" # Execute the PowerShell script with appropriate parameters $result = & ./scripts/build/Detect-Folder-Changes.ps1 @scriptArgs @@ -183,6 +217,21 @@ jobs: "changedBicepFolders=$($jsonData.bicep.folders | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT "changesInApplications=$($jsonData.applications.has_changes)" >> $env:GITHUB_OUTPUT "changedApplicationFolders=$($jsonData.applications.folders | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + if ($jsonData.PSObject.Properties.Name -contains 'fuzz') { + "changesInFuzzRust=$($jsonData.fuzz.rust.has_changes.ToString().ToLower())" >> $env:GITHUB_OUTPUT + "changedFuzzRustFolders=$($jsonData.fuzz.rust.folders | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + "changesInFuzzPython=$($jsonData.fuzz.python.has_changes.ToString().ToLower())" >> $env:GITHUB_OUTPUT + "changedFuzzPythonFolders=$($jsonData.fuzz.python.folders | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + "changesInFuzzJs=$($jsonData.fuzz.js.has_changes.ToString().ToLower())" >> $env:GITHUB_OUTPUT + "changedFuzzJsFolders=$($jsonData.fuzz.js.folders | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + } else { + "changesInFuzzRust=false" >> $env:GITHUB_OUTPUT + 'changedFuzzRustFolders={"folderName":[]}' >> $env:GITHUB_OUTPUT + "changesInFuzzPython=false" >> $env:GITHUB_OUTPUT + 'changedFuzzPythonFolders={"folderName":[]}' >> $env:GITHUB_OUTPUT + "changesInFuzzJs=false" >> $env:GITHUB_OUTPUT + 'changedFuzzJsFolders={"folderName":[]}' >> $env:GITHUB_OUTPUT + } "changesInRust=$($jsonData.rust.has_changes)" >> $env:GITHUB_OUTPUT # Display results for debugging @@ -195,6 +244,11 @@ jobs: Write-Host "Bicep folders: $($jsonData.bicep.folders | ConvertTo-Json)" Write-Host "Application changes: $($jsonData.applications.has_changes)" Write-Host "Application folders: $($jsonData.applications.folders | ConvertTo-Json)" + if ($jsonData.PSObject.Properties.Name -contains 'fuzz') { + Write-Host "Fuzz Rust changes: $($jsonData.fuzz.rust.has_changes)" + Write-Host "Fuzz Python changes: $($jsonData.fuzz.python.has_changes)" + Write-Host "Fuzz JS changes: $($jsonData.fuzz.js.has_changes)" + } Write-Host "Rust changes: $($jsonData.rust.has_changes)" # Map outputs from the detection job to maintain backward compatibility @@ -210,6 +264,12 @@ jobs: changedBicepFolders: ${{ needs.detect-changes.outputs.changedBicepFolders }} changesInApplications: ${{ needs.detect-changes.outputs.changesInApplications }} changedApplicationFolders: ${{ needs.detect-changes.outputs.changedApplicationFolders }} + changesInFuzzRust: ${{ needs.detect-changes.outputs.changesInFuzzRust }} + changedFuzzRustFolders: ${{ needs.detect-changes.outputs.changedFuzzRustFolders }} + changesInFuzzPython: ${{ needs.detect-changes.outputs.changesInFuzzPython }} + changedFuzzPythonFolders: ${{ needs.detect-changes.outputs.changedFuzzPythonFolders }} + changesInFuzzJs: ${{ needs.detect-changes.outputs.changesInFuzzJs }} + changedFuzzJsFolders: ${{ needs.detect-changes.outputs.changedFuzzJsFolders }} changesInRust: ${{ needs.detect-changes.outputs.changesInRust }} steps: - name: Map outputs for backward compatibility diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index bb4bbd7c..3aad4ff2 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -252,6 +252,17 @@ jobs: break-build: true secrets: inherit + # ClusterFuzzLite fuzzing for changed Rust/Python/JS fuzz targets + fuzz: + name: Fuzz + permissions: + contents: read + security-events: write + id-token: write + actions: read + uses: ./.github/workflows/fuzz-pr.yml + secrets: inherit + # Use reusable workflow to detect detailed changes and create folder matrix matrix-changes: name: Detect Matrix Changes diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index b572814b..b2aff82d 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -82,6 +82,10 @@ jobs: echo "Skipping ${crate_dir} (requires OpenCV/FFmpeg system libraries)" continue fi + if [[ "${crate_dir}" == *"/fuzz" ]]; then + echo "Skipping ${crate_dir} (cargo-fuzz crate, uses nightly without clippy component)" + continue + fi echo "::group::Clippy: ${crate_dir}" if [[ -z "${CARGO_REGISTRIES_AIO_SDKS_TOKEN:-}" ]] && grep -q 'registry = "aio-sdks"' "${cargo_toml}"; then echo "Skipping ${crate_dir} (requires aio-sdks registry token)" diff --git a/.hadolint.yaml b/.hadolint.yaml index 0002f2b6..28cb9fde 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -7,3 +7,4 @@ ignored: trustedRegistries: - mcr.microsoft.com - docker.io + - gcr.io diff --git a/codecov.yml b/codecov.yml index 9e5fd262..dd5d29b6 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,15 @@ --- +# Codecov coverage configuration. +# +# Per-component flags scope coverage uploads from the fuzzing matrix so partial +# matrix runs only refresh the flags they cover. carryforward retains the prior +# report for unchanged components, keeping the project total stable across PRs +# that touch only a subset of fuzz targets. +# +# Flag naming convention: +# fuzz- -> Rust crates fuzzed with cargo-fuzz +# fuzz-py- -> Python connectors fuzzed with Atheris +# fuzz-js- -> JavaScript harnesses fuzzed with Jazzer.js codecov: notify: wait_for_ci: true @@ -28,3 +39,63 @@ flags: - "src/500-application/504-mqtt-otel-trace-exporter/**" - "src/500-application/507-ai-inference/**" carryforward: true +flag_management: + default_rules: + carryforward: true + statuses: + - type: project + target: auto + threshold: 5% + informational: true + individual_flags: + # Rust crates (cargo-fuzz) + - name: fuzz-501 + paths: + - src/500-application/501-*/** + carryforward: true + - name: fuzz-502 + paths: + - src/500-application/502-*/** + carryforward: true + - name: fuzz-504 + paths: + - src/500-application/504-*/** + carryforward: true + - name: fuzz-507 + paths: + - src/500-application/507-*/** + carryforward: true + - name: fuzz-511 + paths: + - src/500-application/511-*/** + carryforward: true + - name: fuzz-512 + paths: + - src/500-application/512-*/** + carryforward: true + - name: fuzz-514 + paths: + - src/500-application/514-*/** + carryforward: true + # Python connectors (Atheris) + - name: fuzz-py-505 + paths: + - src/500-application/505-*/** + carryforward: true + - name: fuzz-py-506 + paths: + - src/500-application/506-*/** + carryforward: true + - name: fuzz-py-509 + paths: + - src/500-application/509-*/** + carryforward: true + - name: fuzz-py-510 + paths: + - src/500-application/510-*/** + carryforward: true + # JavaScript harnesses (Jazzer.js) + - name: fuzz-js-513 + paths: + - src/500-application/513-*/** + carryforward: true diff --git a/docs/build-cicd/clusterfuzzlite.md b/docs/build-cicd/clusterfuzzlite.md new file mode 100644 index 00000000..6a49f5f1 --- /dev/null +++ b/docs/build-cicd/clusterfuzzlite.md @@ -0,0 +1,124 @@ +--- +title: ClusterFuzzLite Polyglot Fuzzing Containers +description: Architecture, language toolchains, and troubleshooting for the polyglot ClusterFuzzLite builder image used by edge-ai fuzz harnesses (Rust, Python, JavaScript) +author: Edge AI Team +ms.date: 2026-04-30 +ms.topic: how-to +keywords: + - clusterfuzzlite + - cflite + - fuzzing + - libfuzzer + - atheris + - jazzer.js + - cargo-fuzz +estimated_reading_time: 8 +--- + +This page describes the ClusterFuzzLite (CFLite) builder containers used by +the [`fuzz-pr.yml`](https://github.com/microsoft/edge-ai/blob/main/.github/workflows/fuzz-pr.yml) +workflow and how to add or troubleshoot a fuzz harness in any of the three +supported languages. + +## Overview + +CFLite runs each fuzz target inside a container built from a single +`Dockerfile` at the project root (`.clusterfuzzlite/Dockerfile`). The +`build_fuzzers` action does not expose a `dockerfile-path` input, so +`edge-ai` uses a polyglot pattern: one Dockerfile parameterized by an +`ARG LANGUAGE` build-arg, paired with a top-level `build.sh` dispatcher that +hands off to a per-language `build_.sh` script. + +```text +language: (workflow job input) + | + v +ARG LANGUAGE (passed by the action as --build-arg) + | + v +FROM gcr.io/oss-fuzz-base/base-builder-${LANGUAGE} + | + v +build.sh --case--> build_rust.sh | build_python.sh | build_js.sh +``` + +Issue [#459](https://github.com/microsoft/edge-ai/issues/459) introduced the +polyglot pattern; the previous Rust-only scaffolding came from issue +[#150](https://github.com/microsoft/edge-ai/issues/150). + +## Language toolchains + +### Rust (cargo-fuzz, libFuzzer) + +Driven by `build_rust.sh`. Each Cargo workspace member with a `fuzz/` +sub-crate produces one binary per harness, copied into `$OUT`. + +### Python (Atheris, PyInstaller) + +Driven by `build_python.sh`. Each harness is bundled with +`pyinstaller --onefile` into a self-contained executable. A short bash +wrapper sets the ASAN options expected by the fuzzing engine and execs the +PyInstaller binary. + +Atheris 3.0.0 pins Python 3.11; the CFLite base image upgrade tracked in +issue [#454](https://github.com/microsoft/edge-ai/issues/454) is gated on +either an Atheris 3.12 wheel or a replacement Python fuzzing engine. + +Harness `506-ros2` is excluded from the Python harness list because `rclpy` +(ROS 2 Python bindings) is not installable from PyPI; see issue +[#459](https://github.com/microsoft/edge-ai/issues/459) for the deferral +rationale. + +### JavaScript (Jazzer.js) + +Driven by `build_js.sh`. The harness service's npm dependencies are installed +with `npm ci` (falling back to `npm install` when no lockfile is present), +and a small wrapper invokes `npx jazzer `. `@jazzer.js/core` is +preinstalled globally inside `base-builder-javascript`. + +## Adding a harness + +See [`.clusterfuzzlite/README.md`](https://github.com/microsoft/edge-ai/blob/main/.clusterfuzzlite/README.md) +for the per-language step-by-step. In short: + +1. Drop the harness file under `tests/fuzz/` in the target service. +2. Append the harness entry to the matching `build_.sh` HARNESSES array. +3. Add a new `fuzz--` flag entry in + [`codecov.yml`](https://github.com/microsoft/edge-ai/blob/main/codecov.yml) + if the component number is new. + +## Troubleshooting + +### `bad_build_check` fails with `GLIBC_2.32 not found` + +The Dockerfile pinned a newer base-image tag (e.g. `:ubuntu-24-04`). The +CFLite runner is Ubuntu 20.04 / glibc 2.31. Remove the tag pin to fall back +to the default tag, which tracks the runner's glibc. + +### `pyinstaller: command not found` during a Python build + +`build_python.sh` installs PyInstaller on demand via `command -v pyinstaller`. +If the installation fails, `pip3 install --no-cache-dir pyinstaller` will +report the underlying error in the action log; usually a transient PyPI +outage. Re-run the workflow. + +### `Cannot find module '@jazzer.js/core'` during a JS build + +`@jazzer.js/core` is preinstalled globally in `base-builder-javascript`. If +the error appears at fuzz time (not build time), confirm the harness service +declares `@jazzer.js/core` in `devDependencies` and that `npm ci` ran +successfully (check the build log for an `EUSAGE` lockfile-mismatch error). + +### Workflow job stays skipped after enabling the gate + +`fuzz-python` and `fuzz-js` only run when the change-detection job emits a +non-empty `changedFuzzFolders` matrix. Touch a file under the relevant +component (e.g. any file under `src/500-application/513-*/`) and re-push. + +### `LANGUAGE` env-var ignored locally + +Local repro of `build.sh` defaults to `rust`. Export the env var explicitly: + +```bash +LANGUAGE=python bash .clusterfuzzlite/build.sh +``` diff --git a/scripts/build/Detect-Folder-Changes.ps1 b/scripts/build/Detect-Folder-Changes.ps1 index 995f03b8..eeaa2020 100644 --- a/scripts/build/Detect-Folder-Changes.ps1 +++ b/scripts/build/Detect-Folder-Changes.ps1 @@ -114,6 +114,7 @@ else { param( [switch]$IncludeAllIaC, [switch]$IncludeAllApplications, + [switch]$IncludeFuzzTargets, [string]$BaseBranch = "origin/main", [string]$OutputFile = "", [switch]$OutputJson, @@ -329,6 +330,12 @@ $terraformHasChanges = $false $terraformFolders = @{} $bicepHasChanges = $false $bicepFolders = @{} +$fuzzRustHasChanges = $false +$fuzzRustFolders = [PSCustomObject]@{ folderName = @() } +$fuzzPythonHasChanges = $false +$fuzzPythonFolders = [PSCustomObject]@{ folderName = @() } +$fuzzJsHasChanges = $false +$fuzzJsFolders = [PSCustomObject]@{ folderName = @() } $rustHasChanges = $false # Use native PowerShell commands where possible and minimize redundant operations @@ -732,6 +739,37 @@ if ($bicepFiles) { } } +# Process fuzz target changes when requested +if ($IncludeFuzzTargets) { + $fuzzRustFiles = $changedFiles | Where-Object { $_ -match '/fuzz/(Cargo\.toml|fuzz_targets/.+\.rs)$' } + $fuzzPythonFiles = $changedFiles | Where-Object { $_ -match '/tests/fuzz/.+\.py$' } + $fuzzJsFiles = $changedFiles | Where-Object { $_ -match '/(tests/)?fuzz/(package\.json|.+\.(mjs|cjs|js))$' } + + if ($fuzzRustFiles) { + $fuzzRustPaths = Get-FilePathData -Paths $fuzzRustFiles + if ($fuzzRustPaths.Count -gt 0) { + $fuzzRustHasChanges = $true + $fuzzRustFolders = [PSCustomObject]@{ folderName = @($fuzzRustPaths) } + } + } + + if ($fuzzPythonFiles) { + $fuzzPythonPaths = Get-FilePathData -Paths $fuzzPythonFiles + if ($fuzzPythonPaths.Count -gt 0) { + $fuzzPythonHasChanges = $true + $fuzzPythonFolders = [PSCustomObject]@{ folderName = @($fuzzPythonPaths) } + } + } + + if ($fuzzJsFiles) { + $fuzzJsPaths = Get-FilePathData -Paths $fuzzJsFiles + if ($fuzzJsPaths.Count -gt 0) { + $fuzzJsHasChanges = $true + $fuzzJsFolders = [PSCustomObject]@{ folderName = @($fuzzJsPaths) } + } + } +} + # Create the final JSON output with subscription (always included) $jsonOutput = [PSCustomObject]@{ subscription = [PSCustomObject]@{ @@ -755,6 +793,12 @@ $jsonOutput | Add-Member -MemberType NoteProperty -Name "applications" -Value ([ folders = $applicationChanges }) +$jsonOutput | Add-Member -MemberType NoteProperty -Name "fuzz" -Value ([PSCustomObject]@{ + rust = [PSCustomObject]@{ has_changes = [bool]$fuzzRustHasChanges; folders = $fuzzRustFolders } + python = [PSCustomObject]@{ has_changes = [bool]$fuzzPythonHasChanges; folders = $fuzzPythonFolders } + js = [PSCustomObject]@{ has_changes = [bool]$fuzzJsHasChanges; folders = $fuzzJsFolders } + }) + # Detect Rust-relevant changes that should gate the rust-tests workflow. # Mirrors the regex previously enforced by the standalone detect-rust-changes job # in pr-validation.yml: any crate under src/500-application, root Cargo manifests, diff --git a/scripts/linting/Invoke-PSScriptAnalyzer.ps1 b/scripts/linting/Invoke-PSScriptAnalyzer.ps1 index 9b38cf6c..e39bfb5c 100644 --- a/scripts/linting/Invoke-PSScriptAnalyzer.ps1 +++ b/scripts/linting/Invoke-PSScriptAnalyzer.ps1 @@ -31,9 +31,15 @@ if ($ChangedOnly) { Write-Host "Scanning $($files.Count) file(s)..." $allResults = @() +$crashedFiles = @() foreach ($file in $files) { - $results = Invoke-ScriptAnalyzer -Path $file -Settings $SettingsPath -ReportSummary - $allResults += $results + try { + $results = Invoke-ScriptAnalyzer -Path $file -Settings $SettingsPath -ReportSummary -ErrorAction Stop + $allResults += $results + } catch { + $crashedFiles += $file + Write-Warning "PSScriptAnalyzer internal error on file '$file': $($_.Exception.Message)" + } } if (-not (Test-Path $OutputPath)) { diff --git a/scripts/security/Update-ActionSHAPinning.ps1 b/scripts/security/Update-ActionSHAPinning.ps1 index 49242221..6f1d2c18 100755 --- a/scripts/security/Update-ActionSHAPinning.ps1 +++ b/scripts/security/Update-ActionSHAPinning.ps1 @@ -105,6 +105,8 @@ $ActionSHAMap = @{ "actions/configure-pages@v4" = "actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b" # v4.0.0 "azure/powershell@v1" = "azure/powershell@1c589a2e445c71fe2cea92c69f7b80b572760c3b" # v1.5.0 "azure/get-keyvault-secrets@v1" = "azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f" # v1.2 + "google/clusterfuzzlite/actions/build_fuzzers@v1" = "google/clusterfuzzlite/actions/build_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05" # v1 + "google/clusterfuzzlite/actions/run_fuzzers@v1" = "google/clusterfuzzlite/actions/run_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05" # v1 } function Write-SecurityLog { diff --git a/src/500-application/501-rust-telemetry/services/receiver/fuzz/.gitignore b/src/500-application/501-rust-telemetry/services/receiver/fuzz/.gitignore new file mode 100644 index 00000000..6725528d --- /dev/null +++ b/src/500-application/501-rust-telemetry/services/receiver/fuzz/.gitignore @@ -0,0 +1,3 @@ +target/ +corpus/* +artifacts/* diff --git a/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.lock b/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.lock new file mode 100644 index 00000000..c050352c --- /dev/null +++ b/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.lock @@ -0,0 +1,201 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "receiver-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "serde_json", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.toml b/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.toml new file mode 100644 index 00000000..7da664a9 --- /dev/null +++ b/src/500-application/501-rust-telemetry/services/receiver/fuzz/Cargo.toml @@ -0,0 +1,21 @@ +# trigger fuzz CI +[package] +name = "receiver-fuzz" +version = "0.0.0" +edition = "2021" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +serde_json = "1" + +[[bin]] +name = "parse_telemetry" +path = "fuzz_targets/parse_telemetry.rs" +test = false +doc = false + +[workspace] diff --git a/src/500-application/501-rust-telemetry/services/receiver/fuzz/fuzz_targets/parse_telemetry.rs b/src/500-application/501-rust-telemetry/services/receiver/fuzz/fuzz_targets/parse_telemetry.rs new file mode 100644 index 00000000..2ea0cd37 --- /dev/null +++ b/src/500-application/501-rust-telemetry/services/receiver/fuzz/fuzz_targets/parse_telemetry.rs @@ -0,0 +1,8 @@ +//! Fuzz target: exercises serde_json parsing of telemetry payloads. +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = serde_json::from_slice::(data); +}); diff --git a/src/500-application/501-rust-telemetry/services/receiver/rust-toolchain.toml b/src/500-application/501-rust-telemetry/services/receiver/rust-toolchain.toml new file mode 100644 index 00000000..524869d2 --- /dev/null +++ b/src/500-application/501-rust-telemetry/services/receiver/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2026-04-01" +components = ["rust-src", "llvm-tools-preview"] +profile = "minimal" diff --git a/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_models.py b/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_models.py new file mode 100644 index 00000000..b73b082e --- /dev/null +++ b/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_models.py @@ -0,0 +1,26 @@ +"""Atheris fuzz harness for sensor-simulator FieldsConfig JSON parsing.""" +import json +import sys +from pathlib import Path + +import atheris + +SERVICE_ROOT = Path(__file__).resolve().parents[2] +if str(SERVICE_ROOT) not in sys.path: + sys.path.insert(0, str(SERVICE_ROOT)) + +with atheris.instrument_imports(): + from models import FieldsConfig + from pydantic import ValidationError + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + try: + FieldsConfig.model_validate_json(data) + except (ValidationError, ValueError, json.JSONDecodeError, UnicodeDecodeError): + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_smoke.py b/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_smoke.py new file mode 100644 index 00000000..ac2cbe84 --- /dev/null +++ b/src/500-application/505-akri-rest-http-connector/services/sensor-simulator/tests/fuzz/fuzz_smoke.py @@ -0,0 +1,14 @@ +"""Smoke fuzz harness — minimal Atheris stub to exercise CI plumbing.""" + +import sys + +import atheris + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + _ = bytes(data) + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/506-ros2-connector/services/ros2-connector/tests/fuzz/fuzz_message_registry.py b/src/500-application/506-ros2-connector/services/ros2-connector/tests/fuzz/fuzz_message_registry.py new file mode 100644 index 00000000..b5206a28 --- /dev/null +++ b/src/500-application/506-ros2-connector/services/ros2-connector/tests/fuzz/fuzz_message_registry.py @@ -0,0 +1,30 @@ +"""Atheris fuzz harness for ros2-connector MessageTypeRegistry.get_handler.""" +# trigger fuzz CI +import sys +from pathlib import Path + +import atheris + +SERVICE_ROOT = Path(__file__).resolve().parents[2] +if str(SERVICE_ROOT) not in sys.path: + sys.path.insert(0, str(SERVICE_ROOT)) + +with atheris.instrument_imports(): + from src.message_types.registry import MessageTypeRegistry + + +_REGISTRY = MessageTypeRegistry() + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + fdp = atheris.FuzzedDataProvider(data) + message_type = fdp.ConsumeUnicodeNoSurrogates(64) + try: + _REGISTRY.get_handler(message_type) + except (KeyError, ValueError, TypeError): + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_process_event.py b/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_process_event.py new file mode 100644 index 00000000..e61d8f58 --- /dev/null +++ b/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_process_event.py @@ -0,0 +1,31 @@ +"""Atheris fuzz harness for sse-connector ConnectorClient.process_event.""" +import asyncio +import sys +from pathlib import Path + +import atheris + +SERVICE_ROOT = Path(__file__).resolve().parents[2] +if str(SERVICE_ROOT) not in sys.path: + sys.path.insert(0, str(SERVICE_ROOT)) + +with atheris.instrument_imports(): + from connector_client import SSEConnectorTestClient + + +_CLIENT = SSEConnectorTestClient() + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + fdp = atheris.FuzzedDataProvider(data) + event_type = fdp.ConsumeUnicodeNoSurrogates(32) + event_data = fdp.ConsumeUnicodeNoSurrogates(512) + try: + asyncio.run(_CLIENT.process_event(event_type, event_data)) + except (ValueError, TypeError, KeyError, RuntimeError): + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_smoke.py b/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_smoke.py new file mode 100644 index 00000000..ac2cbe84 --- /dev/null +++ b/src/500-application/509-sse-connector/services/connector-test-client/tests/fuzz/fuzz_smoke.py @@ -0,0 +1,14 @@ +"""Smoke fuzz harness — minimal Atheris stub to exercise CI plumbing.""" + +import sys + +import atheris + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + _ = bytes(data) + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_smoke.py b/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_smoke.py new file mode 100644 index 00000000..ac2cbe84 --- /dev/null +++ b/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_smoke.py @@ -0,0 +1,14 @@ +"""Smoke fuzz harness — minimal Atheris stub to exercise CI plumbing.""" + +import sys + +import atheris + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + _ = bytes(data) + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_soap_parser.py b/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_soap_parser.py new file mode 100644 index 00000000..25e91242 --- /dev/null +++ b/src/500-application/510-onvif-connector/services/onvif-camera-simulator/tests/fuzz/fuzz_soap_parser.py @@ -0,0 +1,27 @@ +"""Atheris fuzz harness for onvif-camera-simulator SOAP XML parser (XXE-hardened).""" +import sys +from pathlib import Path + +import atheris + +SERVICE_ROOT = Path(__file__).resolve().parents[2] +if str(SERVICE_ROOT) not in sys.path: + sys.path.insert(0, str(SERVICE_ROOT)) + +with atheris.instrument_imports(): + from lxml import etree + from onvif_camera import ONVIF_NAMESPACES + + +def TestOneInput(data: bytes) -> None: # noqa: N802 + try: + parser = etree.XMLParser(resolve_entities=False, no_network=True) + root = etree.fromstring(data, parser=parser) + root.find(".//soap:Body", ONVIF_NAMESPACES) + except (etree.XMLSyntaxError, ValueError, TypeError): + pass + + +if __name__ == "__main__": + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() diff --git a/src/500-application/513-tiered-notification-service/package.json b/src/500-application/513-tiered-notification-service/package.json index ea626819..0295fdbd 100644 --- a/src/500-application/513-tiered-notification-service/package.json +++ b/src/500-application/513-tiered-notification-service/package.json @@ -18,7 +18,8 @@ }, "devDependencies": { "vitest": "^3.0.0", - "@vitest/coverage-v8": "^3.0.0" + "@vitest/coverage-v8": "^3.0.0", + "@jazzer.js/core": "^2.0.0" }, "overrides": { "postcss": "^8.5.10" diff --git a/src/500-application/513-tiered-notification-service/src/functions/processAlerts.js b/src/500-application/513-tiered-notification-service/src/functions/processAlerts.js index 037ac844..27a75909 100644 --- a/src/500-application/513-tiered-notification-service/src/functions/processAlerts.js +++ b/src/500-application/513-tiered-notification-service/src/functions/processAlerts.js @@ -558,3 +558,6 @@ app.eventHub("processAlerts", { ); }, }); + +// Exports for fuzz/unit testing of pure helper functions (no side effects). +export { parseAlertPayload, extractSeverity, buildDedupKey, validateWebhookUrl }; diff --git a/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_processAlerts.mjs b/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_processAlerts.mjs new file mode 100644 index 00000000..95908d00 --- /dev/null +++ b/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_processAlerts.mjs @@ -0,0 +1,28 @@ +// Jazzer.js fuzz harness for tiered-notification-service processAlerts module. +// Targets pure functions exported from processAlerts.js (parseAlertPayload, +// extractSeverity, buildDedupKey, validateWebhookUrl). +// trigger fuzz CI +import { parseAlertPayload, extractSeverity, buildDedupKey, validateWebhookUrl } + from '../../src/functions/processAlerts.js'; + +export function fuzz(buffer) { + const input = buffer.toString('utf8'); + try { + const alert = parseAlertPayload(input); + if (alert && typeof alert === 'object') { + const severity = extractSeverity(alert); + buildDedupKey(alert, severity); + } + } catch (e) { + if (!(e instanceof SyntaxError || e instanceof TypeError || e instanceof RangeError)) { + throw e; + } + } + try { + validateWebhookUrl(input); + } catch (e) { + if (!(e instanceof TypeError || e instanceof Error)) { + throw e; + } + } +} diff --git a/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_smoke.mjs b/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_smoke.mjs new file mode 100644 index 00000000..96aa6322 --- /dev/null +++ b/src/500-application/513-tiered-notification-service/tests/fuzz/fuzz_smoke.mjs @@ -0,0 +1,8 @@ +// Smoke fuzz harness — minimal Jazzer.js stub to exercise CI plumbing. +export function fuzz(data) { + try { + Buffer.from(data).toString('utf8'); + } catch (_) { + /* swallow */ + } +}