diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..b7566dc Binary files /dev/null and b/.DS_Store differ diff --git a/.github/security/sca-fossa/README.md b/.github/security/sca-fossa/README.md new file mode 100644 index 0000000..8d4dd0e --- /dev/null +++ b/.github/security/sca-fossa/README.md @@ -0,0 +1,293 @@ +# SCA FOSSA Action + +Run FOSSA scans from GitHub Actions with support for: + +- `fossa analyze` +- optional `fossa test` +- optional diff-based test gating for pull requests +- optional attribution report generation +- optional debug mode +- optional high/critical vulnerability gating through the FOSSA issues API + +## Prerequisites + +- A FOSSA API key stored as a GitHub secret such as `FOSSA_API_KEY` +- A Linux runner with network access to your FOSSA endpoint +- For the optional vulnerability gate, a FOSSA project scope convention that can be resolved from `scope-id-prefix`, `github-org`, and `repo-name` + +## Usage + +Use a pinned release tag or commit SHA when consuming this action from another repository. + +### Basic scan + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} +``` + +If `working-directory` is omitted, the action scans from the repository root (`.`). + +### Scan and test + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + run-tests: "true" +``` + +### Scan a specific folder in a repository + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + working-directory: apps/web + run-tests: "true" +``` + +Use `working-directory` only when you want to scope the scan to a subdirectory. If you do not pass it, the entire repository is scanned. + +### Pull request diff gate + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + run-tests: ${{ github.event_name == 'pull_request' }} + test-diff-revision: ${{ github.event.pull_request.base.sha }} +``` + +### Generate an attribution report + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - id: fossa + uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + generate-report: html + + - run: echo '${{ steps.fossa.outputs.report }}' > report.html +``` + +### Enable the vulnerability API gate + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + run-tests: "true" + fail-on-vulnerabilities: "true" + scope-id-prefix: custom+28356/github.com +``` + +## Inputs + +- `api-key`: Required FOSSA API key. +- `run-tests`: Optional. Runs `fossa test` after analysis when set to `"true"`. Default: `"false"`. +- `generate-report`: Optional. Runs `fossa report attribution --format ` and exposes the report content as an output. +- `test-diff-revision`: Optional. Passed to `fossa test --diff`. Effective only when `run-tests` is enabled. +- `branch`: Optional. Branch passed to FOSSA CLI. Defaults to the current GitHub ref. +- `project`: Optional. Project name passed to FOSSA CLI. +- `endpoint`: Optional. FOSSA endpoint URL. Default: `https://app.fossa.com`. +- `debug`: Optional. Runs FOSSA commands in debug mode when set to `"true"`. Default: `"false"`. +- `working-directory`: Optional. Directory to scan. Default: `.`. If omitted, the action scans the repository root. +- `fail-on-vulnerabilities`: Optional. Enables the extra FOSSA issues API gate for active `high` and `critical` vulnerabilities. Default: `"false"`. +- `repo-name`: Optional. Repository name used by the vulnerability API gate. Defaults to the current repository name. +- `github-org`: Optional. Repository owner used by the vulnerability API gate. Defaults to the current repository owner. +- `scope-id-prefix`: Optional. FOSSA scope prefix used by the vulnerability API gate. Required for that gate to run. + +## Outputs + +- `revision`: Revision extracted from `fossa analyze` output. Falls back to `GITHUB_SHA` if the revision cannot be parsed. +- `active-count`: Active high/critical vulnerability count returned by the FOSSA issues API when the vulnerability gate is enabled. +- `report`: Generated attribution report content when `generate-report` is used. + +## Admin Integration Guide + +For most repositories, use a single workflow and declare scan targets with a matrix instead of creating one workflow per path. + +### Recommended model + +- Keep one GitHub workflow for FOSSA scanning in the product repository. +- Define one matrix entry per independently scanable component. +- Pass both `working-directory` and `project` for each component. +- Use a stable project naming convention such as `owner/repo/path` so each component is tracked as a separate FOSSA project. +- Run all components on `main`, and optionally allow manual selection with `workflow_dispatch`. +- If the repository should be scanned as a single unit, do not pass `working-directory`; the action will scan the entire repository. + +### Example for a monorepo + +```yaml +jobs: + fossa-scan: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + app: + - name: nextjs + path: apps/nextjs + - name: go + path: apps/go + - name: api + path: services/api + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + working-directory: ${{ matrix.app.path }} + project: ${{ format('{0}/{1}', github.repository, matrix.app.path) }} +``` + +### Example with manual target selection and path-based PR or push scans + +```yaml +name: FOSSA Scan + +on: + workflow_dispatch: + inputs: + target: + description: App to scan + required: false + default: all + type: choice + options: + - all + - nextjs + - go + pull_request: + paths: + - 'apps/nextjs/**' + - 'apps/go/**' + - '.github/workflows/fossa-scan.yml' + push: + branches: + - main + paths: + - 'apps/nextjs/**' + - 'apps/go/**' + - '.github/workflows/fossa-scan.yml' + +jobs: + detect-changes: + if: github.event_name != 'workflow_dispatch' + runs-on: ubuntu-latest + outputs: + nextjs: ${{ steps.filter.outputs.nextjs }} + go: ${{ steps.filter.outputs.go }} + steps: + - id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + nextjs: + - 'apps/nextjs/**' + - '.github/workflows/fossa-scan.yml' + go: + - 'apps/go/**' + - '.github/workflows/fossa-scan.yml' + + fossa-scan: + if: | + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.nextjs == 'true' || + needs.detect-changes.outputs.go == 'true' + needs: + - detect-changes + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + app: + - name: nextjs + path: apps/nextjs + - name: go + path: apps/go + steps: + - uses: actions/checkout@v4 + + - uses: wso2/engineering-governance/.github/security/sca-fossa@main + if: | + (github.event_name == 'workflow_dispatch' && ( + github.event.inputs.target == 'all' || + github.event.inputs.target == matrix.app.name + )) || + (github.event_name != 'workflow_dispatch' && ( + (matrix.app.name == 'nextjs' && needs.detect-changes.outputs.nextjs == 'true') || + (matrix.app.name == 'go' && needs.detect-changes.outputs.go == 'true') + )) + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + working-directory: ${{ matrix.app.path }} + project: ${{ format('{0}/{1}', github.repository, matrix.app.path) }} + run-tests: ${{ github.event_name == 'pull_request' }} + test-diff-revision: ${{ github.event.pull_request.base.sha }} +``` + +### When to use separate workflows + +Use separate workflows only when components need materially different: + +- triggers +- permissions +- ownership +- policy or release cadence + +If those differences do not exist, one workflow with a matrix is the recommended setup. + +### Operational guidance + +- If `working-directory` is not set, the scan runs from the repository root. +- `working-directory` controls what folder is scanned. +- `project` controls which FOSSA project receives the scan. +- Use both together when the repository contains multiple components. +- Use `workflow_dispatch` inputs for manual target selection. +- Use path filtering plus a change-detection job for automatic PR and push target selection. +- Keep the workflow model centralized even when scans are selective. + +## Notes + +- The vulnerability gate is a WSO2-specific extension on top of the standard FOSSA analyze/test flow. +- The action currently installs a repo-managed FOSSA CLI version: `v3.17.10`. Update the action itself when there is a new latest stable version is available. diff --git a/.github/security/sca-fossa/action.yaml b/.github/security/sca-fossa/action.yaml new file mode 100644 index 0000000..1c2fea6 --- /dev/null +++ b/.github/security/sca-fossa/action.yaml @@ -0,0 +1,284 @@ +name: FOSSA SCA Scan +description: Run FOSSA scans in GitHub Actions with optional test, diff, report, and vulnerability-gate support. + +inputs: + api-key: + description: FOSSA API key. + required: true + run-tests: + description: Whether to run `fossa test` after analysis. + required: false + default: "false" + generate-report: + description: Report format to generate with `fossa report attribution --format`. + required: false + default: "" + test-diff-revision: + description: Revision to pass to `fossa test --diff`. + required: false + default: "" + branch: + description: Branch name reported to FOSSA. Defaults to the current GitHub ref. + required: false + default: "" + project: + description: Project name passed to FOSSA CLI. + required: false + default: "" + endpoint: + description: FOSSA endpoint URL. Defaults to `https://app.fossa.com`. + required: false + default: "https://app.fossa.com" + debug: + description: Whether to run FOSSA commands in debug mode. + required: false + default: "false" + working-directory: + description: Directory that contains the project to analyze. + required: false + default: "." + fail-on-vulnerabilities: + description: Whether to query the FOSSA issues API and fail on active high or critical vulnerabilities. + required: false + default: "false" + repo-name: + description: Repository name used when querying FOSSA vulnerability status. Defaults to the current repository name. + required: false + default: "" + github-org: + description: GitHub organization or owner used when querying FOSSA vulnerability status. Defaults to the current repository owner. + required: false + default: "" + scope-id-prefix: + description: FOSSA project scope prefix used for the vulnerability status lookup. + required: false + default: "" + +outputs: + revision: + description: Revision detected by FOSSA analyze. + value: ${{ steps.scan.outputs.revision }} + active-count: + description: Active high and critical vulnerability count returned by the FOSSA API. + value: ${{ steps.scan.outputs.active-count }} + report: + description: Generated attribution report content, if requested. + value: ${{ steps.scan.outputs.report }} + +runs: + using: composite + steps: + - name: Run FOSSA SCA scan + id: scan + shell: bash + env: + INPUT_API_KEY: ${{ inputs.api-key }} + INPUT_RUN_TESTS: ${{ inputs.run-tests }} + INPUT_GENERATE_REPORT: ${{ inputs.generate-report }} + INPUT_TEST_DIFF_REVISION: ${{ inputs.test-diff-revision }} + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_PROJECT: ${{ inputs.project }} + INPUT_ENDPOINT: ${{ inputs.endpoint }} + INPUT_DEBUG: ${{ inputs.debug }} + INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }} + INPUT_FAIL_ON_VULNERABILITIES: ${{ inputs.fail-on-vulnerabilities }} + INPUT_REPO_NAME: ${{ inputs.repo-name }} + INPUT_GITHUB_ORG: ${{ inputs.github-org }} + INPUT_SCOPE_ID_PREFIX: ${{ inputs.scope-id-prefix }} + run: | + set -euo pipefail + + run_tests="${INPUT_RUN_TESTS:-false}" + generate_report="${INPUT_GENERATE_REPORT:-}" + test_diff_revision="${INPUT_TEST_DIFF_REVISION:-}" + branch="${INPUT_BRANCH:-}" + project="${INPUT_PROJECT:-}" + endpoint="${INPUT_ENDPOINT:-https://app.fossa.com}" + debug="${INPUT_DEBUG:-false}" + working_directory="${INPUT_WORKING_DIRECTORY:-.}" + fail_on_vulnerabilities="${INPUT_FAIL_ON_VULNERABILITIES:-false}" + repo_name="${INPUT_REPO_NAME:-}" + github_org="${INPUT_GITHUB_ORG:-}" + scope_id_prefix="${INPUT_SCOPE_ID_PREFIX:-}" + fossa_cli_version="v3.17.10" + + if [[ -z "${branch}" ]]; then + branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}" + fi + + if [[ -z "${repo_name}" && -n "${GITHUB_REPOSITORY:-}" ]]; then + repo_name="${GITHUB_REPOSITORY##*/}" + fi + + if [[ -z "${github_org}" && -n "${GITHUB_REPOSITORY_OWNER:-}" ]]; then + github_org="${GITHUB_REPOSITORY_OWNER}" + fi + + if [[ "${endpoint}" =~ ^https?:// ]]; then + endpoint_url="${endpoint}" + else + endpoint_url="https://${endpoint}" + fi + + echo "Installing fossa-cli..." + echo "Using fossa-cli ${fossa_cli_version}" + curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install-latest.sh | bash -s -- "${fossa_cli_version}" + + analyze_args=( + analyze + --fossa-api-key "${INPUT_API_KEY}" + --unpack-archives + --endpoint "${endpoint}" + ) + + if [[ -n "${branch}" ]]; then + analyze_args+=(-b "${branch}") + fi + + if [[ -n "${project}" ]]; then + analyze_args+=(--project "${project}") + fi + + if [[ "${debug}" == "true" ]]; then + analyze_args+=(--debug) + fi + + echo "Running FOSSA scan..." + cd "${GITHUB_WORKSPACE}/${working_directory}" + + set +e + analyze_output="$( + fossa "${analyze_args[@]}" 2>&1 + )" + analyze_status=$? + set -e + echo "${analyze_output}" + + if (( analyze_status != 0 )); then + echo "fossa analyze failed with exit code ${analyze_status}." + exit "${analyze_status}" + fi + + revision="$( + printf '%s\n' "${analyze_output}" | + sed -n 's/.*Using revision: `\([a-f0-9]\+\)`.*/\1/p' | + tail -n 1 + )" + + if [[ -z "${revision}" ]]; then + revision="${GITHUB_SHA:-}" + echo "Could not extract revision from FOSSA output. Falling back to GITHUB_SHA: ${revision}" + fi + + echo "revision=${revision}" >> "${GITHUB_OUTPUT}" + echo "active-count=" >> "${GITHUB_OUTPUT}" + + if [[ "${run_tests}" != "true" && -n "${test_diff_revision}" ]]; then + echo "Ignoring test-diff-revision because run-tests is not enabled." + fi + + if [[ "${run_tests}" == "true" ]]; then + test_args=( + test + --fossa-api-key "${INPUT_API_KEY}" + --endpoint "${endpoint}" + ) + + if [[ -n "${test_diff_revision}" ]]; then + test_args+=(--diff "${test_diff_revision}") + fi + + if [[ "${debug}" == "true" ]]; then + test_args+=(--debug) + fi + + set +e + test_output="$( + fossa "${test_args[@]}" 2>&1 + )" + test_status=$? + set -e + echo "${test_output}" + + if (( test_status != 0 )); then + echo "fossa test failed with exit code ${test_status}." + exit "${test_status}" + fi + fi + + if [[ -n "${generate_report}" ]]; then + report_args=( + report + attribution + --format "${generate_report}" + --fossa-api-key "${INPUT_API_KEY}" + --endpoint "${endpoint}" + ) + + if [[ "${debug}" == "true" ]]; then + report_args+=(--debug) + fi + + set +e + report_output="$( + fossa "${report_args[@]}" 2>&1 + )" + report_status=$? + set -e + + if (( report_status != 0 )); then + echo "${report_output}" + echo "fossa report attribution failed with exit code ${report_status}." + exit "${report_status}" + fi + + echo "report<> "${GITHUB_OUTPUT}" + printf '%s\n' "${report_output}" >> "${GITHUB_OUTPUT}" + echo "EOF" >> "${GITHUB_OUTPUT}" + fi + + if [[ "${fail_on_vulnerabilities}" != "true" ]]; then + echo "Skipping vulnerability status check." + exit 0 + fi + + if [[ -z "${repo_name}" || -z "${github_org}" || -z "${scope_id_prefix}" ]]; then + echo "Skipping vulnerability status check because repo-name, github-org, or scope-id-prefix is not set." + exit 0 + fi + + echo "Checking for high and critical vulnerabilities..." + response="$( + curl --fail --silent --show-error --get \ + --data "category=vulnerability" \ + --data "scope[type]=project" \ + --data-urlencode "scope[id]=${scope_id_prefix}/${github_org}/${repo_name}" \ + --data "scope[revision]=${revision}" \ + --data "filter[severity][0]=critical" \ + --data "filter[severity][1]=high" \ + -H "Authorization: Bearer ${INPUT_API_KEY}" \ + "${endpoint_url}/api/v2/issues/statuses" + )" + + if [[ -z "${response}" || "${response}" == "null" ]]; then + echo "FOSSA API returned an empty response." + exit 1 + fi + + active_count="$( + python3 -c 'import json,sys; print(json.load(sys.stdin).get("issues", {}).get("active", ""))' <<< "${response}" + )" + echo "active-count=${active_count}" >> "${GITHUB_OUTPUT}" + + if ! [[ "${active_count}" =~ ^[0-9]+$ ]]; then + echo "Could not read active vulnerability count from the FOSSA API response." + echo "${response}" + exit 1 + fi + + if (( active_count > 0 )); then + echo "High or critical vulnerabilities found in the project." + exit 1 + fi + + echo "No active high or critical vulnerabilities found in the project." diff --git a/.gitignore b/.gitignore index 524f096..2070218 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* +.DS_Store diff --git a/README.md b/README.md index ea863d7..6bae70b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # engineering-governance The central hub for WSO2 engineering standards, secure development practices and reusable assets to govern engineering practices + +## Reusable GitHub Actions + +- [`.github/security/sca-fossa`](./.github/security/sca-fossa/README.md): Reusable FOSSA SCA scan action for downstream GitHub repositories.