Skip to content

Commit

Permalink
Add grant summary generator action (#2)
Browse files Browse the repository at this point in the history
This adds a GitHub Action that generates a license compliance report (from a SBOM file) using [`grant`].

It can output in two ways:
- HTML: for pull request comments (e.g., when adding a new dependency, or when dependabot bumps versions)
- TTY: for terminal/CLI users

[`grant`]: https://github.com/anchore/grant
  • Loading branch information
NyanKiyoshi authored Sep 30, 2024
1 parent fb9c0a1 commit 2e7957e
Show file tree
Hide file tree
Showing 34 changed files with 2,142 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Generated SBOM files for test fixtures.
sample-*.json linguist-generated

44 changes: 44 additions & 0 deletions .github/workflows/test-grant-license-checker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Run tests against grant-license-checker
on:
pull_request:
types:
- opened
- synchronize
paths:
- .github/workflows/test-grant-license-checker.yaml
- ./grant-license-checker/**
push:
branches: [ main ]
paths:
- .github/workflows/test-grant-license-checker.yaml
- ./grant-license-checker/**

jobs:
main:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./grant-license-checker
steps:
- name: Checkout Code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
sparse-checkout: ./grant-license-checker

- name: Setup Python
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
# Note: make sure to update `poetry env use pythonX.Y` below.
python-version: '3.12'

- name: Install Python Dependencies
shell: bash
run: |
pip install poetry~=1.8.0
poetry env use python3.12
poetry install --with=dev
echo "$(poetry env info -p)"/bin >> "$GITHUB_PATH"
- name: pytest
run: |
pytest ./
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Files that may be accidentally pushed by developers
# when testing locally.
.grant/
bom.json
sbom.json
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Contributing

## GitHub Workflows: Best Practices

- Prefer creating script files (`.sh`, `.py`), it makes local testing easier;
- If the shell code cannot be ran locally (e.g., deeply depends on GitHub Workflows), it's acceptable to not put the script outside the workflow.
210 changes: 210 additions & 0 deletions grant-license-checker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# grant-license-checker

An action that generates a report of licenses used by main and transient dependencies,
and checks for compliance issues using [grant](https://github.com/anchore/grant).

## Example Output

<table>
<tr>
<th width='200px'>License Name</th>
<th>Package Count</th>
<th>Packages</th> </tr>
<tr>
<td>PSF-2.0</td>
<td>1</td>
<td>
<details>
<summary>Packages</summary>
<ul>
<li>typing-extensions</li>
</ul>
</details>
</td>
</tr>
<tr>
<td>0BSD</td>
<td>4</td>
<td>
<details>
<summary>Packages</summary>
<ul>
<li>colorama</li>
<li>Jinja2</li>
<li>MarkupSafe</li>
<li>packaging</li>
</ul>
</details>
</td>
</tr>
<tr>
<td>MIT</td>
<td>6</td>
<td>
<details>
<summary>Packages</summary>
<ul>
<li>annotated-types</li>
<li>iniconfig</li>
<li>pluggy</li>
<li>pydantic</li>
<li>pydantic-core</li>
<li>pytest</li>
</ul>
</details>
</td>
</tr>
</table>

## Usage

### GitHub Action

```yaml
on:
pull_request:
types:
- opened
- synchronize
paths:
# Python Ecosystem
- "**/pyproject.toml"
- "**/setup.py"
- "**/requirements*.txt"
- "**/Pipfile.lock"
- "**/poetry.lock"
# JS/TS Ecosystem
- "**/package.json"
- "**/pnpm-lock.yaml"
- "**/package-lock.json"

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Analyze Licenses
uses: saleor/saleor-internal-actions/grant-license-checker@v0
with:
# Needs to be a SBOM that contains license information (SPDX, CycloneDX,
# and Syft formats are supported).
# You can use 'saleor/saleor-internal-actions/sbom-generator' to generate such SBOMs.
sbom_path: ./sbom.json
# See https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES for the supported
# ecosystems ("Project Types" column).
ecosystems: |
python
javascript
# YAML rules for grant.
rules: |
- pattern: "BSD-*"
name: "allow-bsd"
mode: "allow"
- pattern: "*"
name: "default-deny-all"
mode: "deny"
reason: "All licenses need to be explicitly approved (allow-list)"
```
### Reusable GitHub Workflow
```yaml
name: Check Licenses
on:
pull_request:
types:
- opened
- synchronize
paths:
# Python Ecosystem
- "**/pyproject.toml"
- "**/setup.py"
- "**/requirements*.txt"
- "**/Pipfile.lock"
- "**/poetry.lock"
# JS/TS Ecosystem
- "**/package.json"
- "**/pnpm-lock.yaml"
- "**/package-lock.json"

jobs:
default:
permissions:
contents: read
pull-requests: write
uses: saleor/saleor-internal-actions/.github/workflows/run-license-check.yaml@v0
```
### CLI
Usage:
```
usage: grant-summarize [-h] -i INPUT [-l] [-m MAX_PACKAGES] [-f {html,tty}] [-o OUTPUT] [-v VERBOSE | -D DEBUG]

This command summarizes a grant JSON output with human friendly formats. Such as: - HTML table (GitHub Markdown-compatible), - TTY plaintext.

options:
-h, --help show this help message and exit
-v VERBOSE, --verbose VERBOSE
Enable verbose logging
-D DEBUG, --debug DEBUG
Enable debug logging

Input Preferences:
-i INPUT, --input INPUT
The grant JSON output file

Output Preferences:
-l, --list-packages Whether to include the package list in the output.
-m MAX_PACKAGES, --max-packages MAX_PACKAGES
The maximum number of packages to include in the output per license. A value too large can potentially not fit inside GitHub comments.
-f {html,tty}, --format {html,tty}
The output format, one of: 'text' (logs friendly), 'html' (markdown friendly)
-o OUTPUT, --output OUTPUT
The path to the output the result. Defaults to stdout.
```

End to end example:

1. `cd <path to project>`
2. Generate the SBOM:
```
docker run \
--rm \
-v "$(pwd):/app:rw" \
--env FETCH_LICENSE=true \
-t ghcr.io/cyclonedx/cdxgen:v10.9.5 \
-r /app -o /app/bom.json --profile license-compliance -t npm -t python
```
3. Generate the `grant` report:
```
grant check ./bom.json -o json > ./grant.json
```
4. Generate the summary:
```
grant-summarize -i grant.json
```

## Development

This project takes a `grant check` JSON report as input and renders it.

### Project Structure

- `cmd/`
- Module where commands should be defined at;
- When adding a new command, add it in `pyproject.toml` to ensure it is installed into the `PATH` (`PATH` is updated on `poetry install`).
- `renderers/`
- Module containing rendering templates and logics;
- When adding a new renderer, register it inside `__init__.py`, it will be automatically available for use via `--format=<name>`.
- `tests/fixtures/`
- Contains test data that can also be used during the project's development;
- `sample-sbom-v1.5.json` - a basic CycloneDX SBOM file (https://cyclonedx.org/docs/1.5/json/).
Can be used against `grant`, e.g `grant check ./bom.json -o table --show-packages`;
- `sample-grant-report.json` - a basic JSON report generated by `grant check`.
108 changes: 108 additions & 0 deletions grant-license-checker/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: grant-license-checker
description: >-
Generates a report of licenses used by main and transient dependencies,
and checks for compliance issues.
inputs:
sbom_path:
required: true
description: >-
The path of the SBOM to analyze.
Supported formats: SPDX, CycloneDX, Syft.
rules:
description: >-
A list of grant YAML rules (default: deny all GPL licenses).
More details at: https://github.com/anchore/grant/blob/v0.2.1/README.md#usage.
default: |
- pattern: "*gpl*"
name: "default-deny-gpl"
mode: "deny"
reason: "GPL licenses are not compatible with BSD-3-Clause."
outputs:
results_dir_path:
description: "Where all the results files are stored (JSON, and HTML)"
value: ${{ steps.results-paths.outputs.results_dir }}
grant_json_path:
description: "Where the JSON results of grant are stored"
value: ${{ steps.results-paths.outputs.grant_json_path }}
grant_html_path:
description: "Where the HTML results of grant are stored"
value: ${{ steps.results-paths.outputs.grant_html_path }}
runs:
using: composite
steps:
- name: Setup Python
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
# Note: make sure to update `poetry env use pythonX.Y` below.
python-version: '3.12'

- name: Install Python Dependencies
shell: bash
working-directory: ${{ github.action_path }}
run: |
pip install poetry~=1.8.0
poetry env use python3.12
poetry install --only=main
echo "$(poetry env info -p)"/bin >> "$GITHUB_PATH"
- name: Install Grant
shell: bash
run: |
"${GITHUB_ACTION_PATH}/scripts/download-grant.sh"
- name: Set-up Results Path
id: results-paths
shell: bash
run: |
results_dir=.grant/results
mkdir -p "$results_dir"
{
echo "results_dir=$results_dir"
echo "grant_json_path=$results_dir/grant.json"
echo "grant_html_path=$results_dir/grant.html"
} >> "$GITHUB_OUTPUT"
- name: Configure Grant
id: grant-cfg
shell: bash
env:
RULES_YAML_ARRAY: ${{ inputs.rules }}
run: |
cfg_path=./.grant.yaml
echo "cfg_path=$cfg_path" >> "$GITHUB_OUTPUT"
# Construct grant config YAML with the contents: `rules: [<user-input>]`
printf '%s' "$RULES_YAML_ARRAY" | yq '{"rules": .}' - > "$cfg_path"
- name: Check Dependencies
shell: bash
id: grant-check
env:
SBOM_PATH: ${{ inputs.sbom_path }}
RESULTS_PATH: ${{ steps.results-paths.outputs.grant_json_path }}
GRANT_CONFIG_PATH: ${{ steps.grant-cfg.outputs.cfg_path }}
run: |
cmd_args=()
# Enable trace and debug logging if the runner has debug mode enabled
test -z "${RUNNER_DEBUG+x}" || cmd_args+=( "-vvv" )
./.grant/grant check "$SBOM_PATH" \
-o json \
--show-packages \
"--config=$GRANT_CONFIG_PATH" \
"${cmd_args[@]}" > "$RESULTS_PATH"
- name: Generate Report
# Always generate the report, even if there are license violations
if: ${{ success() || ( failure() && steps.grant-check.conclusion == 'failure' ) }}
shell: bash
env:
RESULTS_JSON_PATH: ${{ steps.results-paths.outputs.grant_json_path }}
RESULTS_HTML_PATH: ${{ steps.results-paths.outputs.grant_html_path }}
run: |
grant-summarize \
-i "$RESULTS_JSON_PATH" \
-f html \
-o "$RESULTS_HTML_PATH" \
--list-packages
Empty file.
Empty file.
Loading

0 comments on commit 2e7957e

Please sign in to comment.