From 57e33125614acb6b79726bc5c8cb36859b4a02a7 Mon Sep 17 00:00:00 2001 From: Behn Hayhoe <7383025+BehnH@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:20:21 +0100 Subject: [PATCH 1/2] Update gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 485dee6..e92fb65 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .idea +.vscode + +**/.DS_Store + From f6e91c8fb945e53ca1c98a8aa70f99e28ef0621f Mon Sep 17 00:00:00 2001 From: Behn Hayhoe <7383025+BehnH@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:22:20 +0100 Subject: [PATCH 2/2] Add dependency-scan action --- actions/dependency-scan/README.md | 38 ++++++ .../dependency-scan/evaluate-policy/README.md | 28 +++++ .../evaluate-policy/action.yml | 117 ++++++++++++++++++ .../evaluate-policy/license_policy.rego | 21 ++++ .../dependency-scan/generate-sbom/README.md | 27 ++++ .../dependency-scan/generate-sbom/action.yml | 68 ++++++++++ 6 files changed, 299 insertions(+) create mode 100644 actions/dependency-scan/README.md create mode 100644 actions/dependency-scan/evaluate-policy/README.md create mode 100644 actions/dependency-scan/evaluate-policy/action.yml create mode 100644 actions/dependency-scan/evaluate-policy/license_policy.rego create mode 100644 actions/dependency-scan/generate-sbom/README.md create mode 100644 actions/dependency-scan/generate-sbom/action.yml diff --git a/actions/dependency-scan/README.md b/actions/dependency-scan/README.md new file mode 100644 index 0000000..5c04d00 --- /dev/null +++ b/actions/dependency-scan/README.md @@ -0,0 +1,38 @@ +# dependency-scan + +This set of actions is used for license validation to prevent GPL licenses from being shipped in our product. + +There are four sub-actions in this action. + +## [generate-sbom](./generate-sbom) + +This action generates a Software Bill of Materials (SBOM) for a project. It is uploaded as an artifact to the +GitHub Action workspace. + +## [evaluate-policy](./evaluate-policy) + +This action evaluates the SBOM against the policy file. A summary is posted to GitHub Actions. + +# Example workflows + +## Serial generation of BOMs + +```yaml +name: Workflow +on: pull_request + +jobs: + dependency-scan: + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - name: Generate SBOM + uses: launchdarkly/gh-actions/actions/dependency-scan/generate-sbom@main + with: + types: 'go,nodejs' + - name: Evaluate SBOM Policy + uses: launchdarkly/gh-actions/actions/dependency-scan/evaluate-policy@main +``` diff --git a/actions/dependency-scan/evaluate-policy/README.md b/actions/dependency-scan/evaluate-policy/README.md new file mode 100644 index 0000000..80e276f --- /dev/null +++ b/actions/dependency-scan/evaluate-policy/README.md @@ -0,0 +1,28 @@ + +## Description + +Evaluate the OPA license policy against the BOM + + + +## Inputs + +| name | description | required | default | +| --- | --- | --- | --- | +| `policy-file` |

Filename of the license policy.

| `false` | `license_policy.rego` | +| `bom-file` |

File name for the Bill of Materials file

| `false` | `bom.json` | +| `opa-query` |

OPA query to use. If anything is returned, the build will fail.

| `false` | `data.launchdarkly.violation[x]` | +| `opa-version` |

The Open Policy Agent version to use.

| `false` | `latest` | +| `cyclonedx-version` |

The CycloneDX version to use.

| `false` | `latest` | +| `artifacts-pattern` |

Download one or more artifacts and merge them, prior to validation.

| `false` | `""` | + + + + + + + +## Runs + +This action is a `composite` action. + diff --git a/actions/dependency-scan/evaluate-policy/action.yml b/actions/dependency-scan/evaluate-policy/action.yml new file mode 100644 index 0000000..9e4e8d3 --- /dev/null +++ b/actions/dependency-scan/evaluate-policy/action.yml @@ -0,0 +1,117 @@ +name: Evaluate Policy +description: Evaluate the OPA license policy against the BOM + +inputs: + policy-file: + description: Filename of the license policy. + required: false + default: license_policy.rego + bom-file: + description: File name for the Bill of Materials file + required: false + default: bom.json + opa-query: + description: OPA query to use. If anything is returned, the build will fail. + required: false + default: data.launchdarkly.violation[x] + opa-version: + description: The Open Policy Agent version to use. + required: false + default: latest + cyclonedx-version: + description: The CycloneDX version to use. + required: false + default: latest + artifacts-pattern: + description: Download one or more artifacts and merge them, prior to validation. + required: false + +runs: + using: composite + steps: + - name: Open Policy Agent CLI version + id: versions + shell: bash + run: | + if [[ ${{ inputs.opa-version }} == latest ]]; then + echo "opa=$(curl -s https://api.github.com/repos/open-policy-agent/opa/releases/latest | jq -r .tag_name)" >> $GITHUB_OUTPUT + else + echo "opa=${{ inputs.opa-version }}" >> $GITHUB_OUTPUT + fi + + if [[ "${{ inputs.artifacts-pattern }}" != "" && ${{ inputs.cyclonedx-version }} == latest ]]; then + echo "cyclonedx=$(curl -s https://api.github.com/repos/CycloneDX/cyclonedx-cli/releases/latest | jq -r .tag_name)" >> $GITHUB_OUTPUT + else + echo "cyclonedx=${{ inputs.cyclonedx-version }}" >> $GITHUB_OUTPUT + fi + + - name: Cache Open Policy Agent + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + id: cache-opa + with: + path: /usr/local/bin/opa + key: opa-${{ steps.versions.outputs.opa }} + + - name: Install Open Policy Agent + if: steps.cache-opa.outputs.cache-hit != 'true' + shell: bash + run: | + curl -L https://openpolicyagent.org/downloads/${{ steps.versions.outputs.opa }}/opa_linux_amd64 -o /usr/local/bin/opa + chmod +x /usr/local/bin/opa + + - name: Cache CycloneDX CLI tool + if: inputs.artifacts-pattern != '' + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + id: cyclonedx-opa + with: + path: /usr/local/bin/cyclonedx + key: cyclonedx-${{ steps.versions.outputs.cyclonedx }} + + - name: Install CycloneDX CLI tool + if: inputs.artifacts-pattern != '' && steps.cyclonedx-opa.outputs.cache-hit != 'true' + shell: bash + run: | + curl -L https://github.com/CycloneDX/cyclonedx-cli/releases/download/${{ steps.versions.outputs.cyclonedx }}/cyclonedx-linux-x64 -o /usr/local/bin/cyclonedx + chmod +x /usr/local/bin/cyclonedx + + - name: Get all BOMs + if: inputs.artifacts-pattern != '' + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + path: . + pattern: ${{ inputs.artifacts-pattern }} + + - name: Merge BOMs + if: inputs.artifacts-pattern != '' + run: cyclonedx merge --input-files ${{ inputs.artifacts-pattern }}/*.json --output-file ${{ inputs.bom-file }} + shell: bash + + - name: Store Bill of Materials + if: inputs.artifacts-pattern != '' + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + with: + name: merged-bom + path: ${{ inputs.bom-file }} + retention-days: 90 + + - name: Evaluate Policy + run: | + json_data="$(opa eval --fail-defined -i ${{ inputs.bom-file }} -d $GITHUB_ACTION_PATH/${{ inputs.policy-file }} '${{ inputs.opa-query }}' --format json)" || { + rc=$? + { + echo "Status: :x: failed" + echo "" + jq <<<"$json_data" -r ' + [.result[].expressions[].value] | # Filter down to what we want + (.[0] | to_entries | map(.key)), # First element keys as header row + ["---", "---"], # Markdown table header identifier + (.[] | to_entries | map(.value)) | # Finally get the real values + join(" | ") # join by "|" (to mark columns) + ' + } >> $GITHUB_STEP_SUMMARY + printf "%s\n" "$json_data" + exit $rc + } + + echo "Status: :white_check_mark: success" >> $GITHUB_STEP_SUMMARY + shell: bash diff --git a/actions/dependency-scan/evaluate-policy/license_policy.rego b/actions/dependency-scan/evaluate-policy/license_policy.rego new file mode 100644 index 0000000..084b43f --- /dev/null +++ b/actions/dependency-scan/evaluate-policy/license_policy.rego @@ -0,0 +1,21 @@ +package launchdarkly + +default allow = false # unless otherwise defined, allow is false + +allow = true { # allow is true if... + count(violation) == 0 # there are zero violations. +} + +# The golang cyclonedx tool puts licenses under 'evidence' +violation[component["bom-ref"]] = {"dependency": component["bom-ref"], "license": license} { + component := input.components[_] + license := component.evidence.licenses[_].license.id + contains(license, "GPL") # should catch GPL, LGPL, AGPL, etc +} + +# The Node cyclonedx tool puts licenses directly under the component +violation[component["bom-ref"]] = {"dependency": component["bom-ref"], "license": license} { + component := input.components[_] + license := component.licenses[_].license.id + contains(license, "GPL") # should catch GPL, LGPL, AGPL, etc +} diff --git a/actions/dependency-scan/generate-sbom/README.md b/actions/dependency-scan/generate-sbom/README.md new file mode 100644 index 0000000..bbe99f1 --- /dev/null +++ b/actions/dependency-scan/generate-sbom/README.md @@ -0,0 +1,27 @@ + +## Description + +Generate a Software Bill of Materials (SBOM) + + + +## Inputs + +| name | description | required | default | +| --- | --- | --- | --- | +| `types` |

Comma separated project types. Please refer to https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for supported languages/platforms.

| `true` | `""` | +| `cdxgen-version` |

Version of cdxgen to use

| `false` | `10.9.2` | +| `project-directory` |

Relative path (from root of repo) to the root of the golang project / module

| `false` | `.` | +| `fetch-license` |

Fetch license information for dependencies

| `false` | `true` | +| `recurse` |

Recurse mode suitable for mono-repos.

| `false` | `true` | + + + + + + + +## Runs + +This action is a `composite` action. + diff --git a/actions/dependency-scan/generate-sbom/action.yml b/actions/dependency-scan/generate-sbom/action.yml new file mode 100644 index 0000000..a379360 --- /dev/null +++ b/actions/dependency-scan/generate-sbom/action.yml @@ -0,0 +1,68 @@ +name: Generate SBOM +description: Generate a Software Bill of Materials (SBOM) + +inputs: + types: + description: 'Comma separated project types. Please refer to https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for supported languages/platforms.' + required: true + cdxgen-version: + description: 'Version of cdxgen to use' + required: false + default: '10.9.2' + project-directory: + description: 'Relative path (from root of repo) to the root of the golang project / module' + required: false + default: '.' + fetch-license: + description: 'Fetch license information for dependencies' + required: false + default: 'true' + recurse: + description: 'Recurse mode suitable for mono-repos.' + required: false + default: 'true' + +runs: + using: composite + + steps: + - name: Setup Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 20.x + + - name: Install cdxgen + run: npm list -g | grep -qF @cyclonedx/cdxgen@${{ inputs.cdxgen-version }} || npm install -g @cyclonedx/cdxgen@${{ inputs.cdxgen-version }} + shell: bash + + - name: Get information + id: info + shell: bash + run: | + if [[ "${{ inputs.types }}" == *,* ]]; then + echo "artifact_name=bom" >> "$GITHUB_OUTPUT" + echo "bom_file=bom.json" >> "$GITHUB_OUTPUT" + else + echo "artifact_name=bom-${{ inputs.types }}" >> "$GITHUB_OUTPUT" + echo "bom_file=bom.${{ inputs.types }}.json" >> "$GITHUB_OUTPUT" + fi + + - name: Generate Bill of Materials + shell: bash + run: | + IFS=, read -ra types <<<"${{ inputs.types }}" + args=() + for type in "${types[@]}"; do + args+=(-t "$type") + done + [[ "${{ inputs.recurse }}" != true ]] && args+=(--no-recurse) + cdxgen "${args[@]}" -o "${{ steps.info.outputs.bom_file }}" ${{ inputs.project-directory }} + env: + FETCH_LICENSE: ${{ inputs.fetch-license }} + + - name: Store Bill of Materials + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: ${{ steps.info.outputs.artifact_name }} + path: ${{ steps.info.outputs.bom_file }} + retention-days: 1