mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-19 07:35:17 +00:00
Dependency License Checker Hook (#1046)
This commit is contained in:
@@ -31,6 +31,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-hooks) for guidelines on how to
|
|||||||
|
|
||||||
| Name | Description | Events | Bundled Assets |
|
| Name | Description | Events | Bundled Assets |
|
||||||
| ---- | ----------- | ------ | -------------- |
|
| ---- | ----------- | ------ | -------------- |
|
||||||
|
| [Dependency License Checker](../hooks/dependency-license-checker/README.md) | Scans newly added dependencies for license compliance (GPL, AGPL, etc.) at session end | sessionEnd | `check-licenses.sh`<br />`hooks.json` |
|
||||||
| [Governance Audit](../hooks/governance-audit/README.md) | Scans Copilot agent prompts for threat signals and logs governance events | sessionStart, sessionEnd, userPromptSubmitted | `audit-prompt.sh`<br />`audit-session-end.sh`<br />`audit-session-start.sh`<br />`hooks.json` |
|
| [Governance Audit](../hooks/governance-audit/README.md) | Scans Copilot agent prompts for threat signals and logs governance events | sessionStart, sessionEnd, userPromptSubmitted | `audit-prompt.sh`<br />`audit-session-end.sh`<br />`audit-session-start.sh`<br />`hooks.json` |
|
||||||
| [Secrets Scanner](../hooks/secrets-scanner/README.md) | Scans files modified during a Copilot coding agent session for leaked secrets, credentials, and sensitive data | sessionEnd | `hooks.json`<br />`scan-secrets.sh` |
|
| [Secrets Scanner](../hooks/secrets-scanner/README.md) | Scans files modified during a Copilot coding agent session for leaked secrets, credentials, and sensitive data | sessionEnd | `hooks.json`<br />`scan-secrets.sh` |
|
||||||
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`<br />`hooks.json` |
|
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`<br />`hooks.json` |
|
||||||
|
|||||||
214
hooks/dependency-license-checker/README.md
Normal file
214
hooks/dependency-license-checker/README.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
name: 'Dependency License Checker'
|
||||||
|
description: 'Scans newly added dependencies for license compliance (GPL, AGPL, etc.) at session end'
|
||||||
|
tags: ['compliance', 'license', 'dependencies', 'session-end']
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dependency License Checker Hook
|
||||||
|
|
||||||
|
Scans newly added dependencies for license compliance at the end of a GitHub Copilot coding agent session, flagging copyleft and restrictive licenses (GPL, AGPL, SSPL, etc.) before they get committed.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
AI coding agents may add new dependencies during a session without considering license implications. This hook acts as a compliance safety net by detecting new dependencies across multiple ecosystems, looking up their licenses, and checking them against a configurable blocked list of copyleft and restrictive licenses.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-ecosystem support**: npm, pip, Go, Ruby, and Rust dependency detection
|
||||||
|
- **Two modes**: `warn` (log only) or `block` (exit non-zero to prevent commit)
|
||||||
|
- **Configurable blocked list**: Default copyleft set with full SPDX variant coverage
|
||||||
|
- **Allowlist support**: Skip known-acceptable packages via `LICENSE_ALLOWLIST`
|
||||||
|
- **Smart detection**: Uses `git diff` to detect only newly added dependencies
|
||||||
|
- **Multiple lookup strategies**: Local cache, package manager CLI, with fallback to UNKNOWN
|
||||||
|
- **Structured logging**: JSON Lines output for integration with monitoring tools
|
||||||
|
- **Timeout protection**: Each license lookup wrapped with 5-second timeout
|
||||||
|
- **Zero mandatory dependencies**: Uses standard Unix tools; optional `jq` for better JSON parsing
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the hook folder to your repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r hooks/dependency-license-checker .github/hooks/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure the script is executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x .github/hooks/dependency-license-checker/check-licenses.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create the logs directory and add it to `.gitignore`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p logs/copilot/license-checker
|
||||||
|
echo "logs/" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Commit the hook configuration to your repository's default branch.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The hook is configured in `hooks.json` to run on the `sessionEnd` event:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"hooks": {
|
||||||
|
"sessionEnd": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"bash": ".github/hooks/dependency-license-checker/check-licenses.sh",
|
||||||
|
"cwd": ".",
|
||||||
|
"env": {
|
||||||
|
"LICENSE_MODE": "warn"
|
||||||
|
},
|
||||||
|
"timeoutSec": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Values | Default | Description |
|
||||||
|
|----------|--------|---------|-------------|
|
||||||
|
| `LICENSE_MODE` | `warn`, `block` | `warn` | `warn` logs violations only; `block` exits non-zero to prevent auto-commit |
|
||||||
|
| `SKIP_LICENSE_CHECK` | `true` | unset | Disable the checker entirely |
|
||||||
|
| `LICENSE_LOG_DIR` | path | `logs/copilot/license-checker` | Directory where check logs are written |
|
||||||
|
| `BLOCKED_LICENSES` | comma-separated SPDX IDs | copyleft set | Licenses to flag as violations |
|
||||||
|
| `LICENSE_ALLOWLIST` | comma-separated | unset | Package names to skip (e.g., `linux-headers,glibc`) |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. When a Copilot coding agent session ends, the hook executes
|
||||||
|
2. Runs `git diff HEAD` against manifest files (package.json, requirements.txt, go.mod, etc.)
|
||||||
|
3. Extracts newly added package names from the diff output
|
||||||
|
4. Looks up each package's license using local caches and package manager CLIs
|
||||||
|
5. Checks each license against the blocked list using case-insensitive substring matching
|
||||||
|
6. Skips packages in the allowlist before flagging
|
||||||
|
7. Reports findings in a formatted table with package, ecosystem, license, and status
|
||||||
|
8. Writes a structured JSON log entry for audit purposes
|
||||||
|
9. In `block` mode, exits non-zero to signal the agent to stop before committing
|
||||||
|
|
||||||
|
## Supported Ecosystems
|
||||||
|
|
||||||
|
| Ecosystem | Manifest File | Primary Lookup | Fallback |
|
||||||
|
|-----------|--------------|----------------|----------|
|
||||||
|
| npm/yarn/pnpm | `package.json` | `node_modules/<pkg>/package.json` license field | `npm view <pkg> license` |
|
||||||
|
| pip | `requirements.txt`, `pyproject.toml` | `pip show <pkg>` License field | UNKNOWN |
|
||||||
|
| Go | `go.mod` | LICENSE file in module cache (keyword match) | UNKNOWN |
|
||||||
|
| Ruby | `Gemfile` | `gem spec <pkg> license` | UNKNOWN |
|
||||||
|
| Rust | `Cargo.toml` | `cargo metadata` license field | UNKNOWN |
|
||||||
|
|
||||||
|
## Default Blocked Licenses
|
||||||
|
|
||||||
|
The following licenses are blocked by default (copyleft and restrictive):
|
||||||
|
|
||||||
|
- **GPL**: GPL-2.0, GPL-2.0-only, GPL-2.0-or-later, GPL-3.0, GPL-3.0-only, GPL-3.0-or-later
|
||||||
|
- **AGPL**: AGPL-1.0, AGPL-3.0, AGPL-3.0-only, AGPL-3.0-or-later
|
||||||
|
- **LGPL**: LGPL-2.0, LGPL-2.1, LGPL-2.1-only, LGPL-2.1-or-later, LGPL-3.0, LGPL-3.0-only, LGPL-3.0-or-later
|
||||||
|
- **Other**: SSPL-1.0, EUPL-1.1, EUPL-1.2, OSL-3.0, CPAL-1.0, CPL-1.0
|
||||||
|
- **Creative Commons (restrictive)**: CC-BY-SA-4.0, CC-BY-NC-4.0, CC-BY-NC-SA-4.0
|
||||||
|
|
||||||
|
Override with `BLOCKED_LICENSES` to customize.
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
### Clean scan (no new dependencies)
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ No new dependencies detected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean scan (all compliant)
|
||||||
|
|
||||||
|
```
|
||||||
|
🔍 Checking licenses for 3 new dependency(ies)...
|
||||||
|
|
||||||
|
PACKAGE ECOSYSTEM LICENSE STATUS
|
||||||
|
------- --------- ------- ------
|
||||||
|
express npm MIT OK
|
||||||
|
lodash npm MIT OK
|
||||||
|
axios npm MIT OK
|
||||||
|
|
||||||
|
✅ All 3 dependencies have compliant licenses
|
||||||
|
```
|
||||||
|
|
||||||
|
### Violations detected (warn mode)
|
||||||
|
|
||||||
|
```
|
||||||
|
🔍 Checking licenses for 2 new dependency(ies)...
|
||||||
|
|
||||||
|
PACKAGE ECOSYSTEM LICENSE STATUS
|
||||||
|
------- --------- ------- ------
|
||||||
|
react npm MIT OK
|
||||||
|
readline-sync npm GPL-3.0 BLOCKED
|
||||||
|
|
||||||
|
⚠️ Found 1 license violation(s):
|
||||||
|
|
||||||
|
- readline-sync (npm): GPL-3.0
|
||||||
|
|
||||||
|
💡 Review the violations above. Set LICENSE_MODE=block to prevent commits with license issues.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Violations detected (block mode)
|
||||||
|
|
||||||
|
```
|
||||||
|
🔍 Checking licenses for 2 new dependency(ies)...
|
||||||
|
|
||||||
|
PACKAGE ECOSYSTEM LICENSE STATUS
|
||||||
|
------- --------- ------- ------
|
||||||
|
flask pip BSD-3-Clause OK
|
||||||
|
copyleft-lib pip AGPL-3.0 BLOCKED
|
||||||
|
|
||||||
|
⚠️ Found 1 license violation(s):
|
||||||
|
|
||||||
|
- copyleft-lib (pip): AGPL-3.0
|
||||||
|
|
||||||
|
🚫 Session blocked: resolve license violations above before committing.
|
||||||
|
Set LICENSE_MODE=warn to log without blocking, or add packages to LICENSE_ALLOWLIST.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log Format
|
||||||
|
|
||||||
|
Check events are written to `logs/copilot/license-checker/check.log` in JSON Lines format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"timestamp":"2026-03-17T10:30:00Z","event":"license_check_complete","mode":"warn","dependencies_checked":3,"violation_count":1,"violations":[{"package":"readline-sync","ecosystem":"npm","license":"GPL-3.0","status":"BLOCKED"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"timestamp":"2026-03-17T10:30:00Z","event":"license_check_complete","mode":"warn","status":"clean","dependencies_checked":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pairing with Other Hooks
|
||||||
|
|
||||||
|
This hook pairs well with:
|
||||||
|
|
||||||
|
- **Secrets Scanner**: Run secrets scanning first, then license checking, before auto-commit
|
||||||
|
- **Session Auto-Commit**: When both are installed, order them so that `dependency-license-checker` runs first. Set `LICENSE_MODE=block` to prevent auto-commit when violations are detected.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
- **Modify blocked licenses**: Set `BLOCKED_LICENSES` to a custom comma-separated list of SPDX IDs
|
||||||
|
- **Allowlist packages**: Use `LICENSE_ALLOWLIST` for known-acceptable packages with copyleft licenses
|
||||||
|
- **Change log location**: Set `LICENSE_LOG_DIR` to route logs to your preferred directory
|
||||||
|
- **Add ecosystems**: Extend the detection and lookup sections in `check-licenses.sh`
|
||||||
|
|
||||||
|
## Disabling
|
||||||
|
|
||||||
|
To temporarily disable the checker:
|
||||||
|
|
||||||
|
- Set `SKIP_LICENSE_CHECK=true` in the hook environment
|
||||||
|
- Or remove the `sessionEnd` entry from `hooks.json`
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- License detection relies on manifest file diffs; dependencies added outside standard manifest files are not detected
|
||||||
|
- License lookup requires the package manager CLI or local cache to be available
|
||||||
|
- Compound SPDX expressions (e.g., `MIT OR GPL-3.0`) are flagged if any component matches the blocked list
|
||||||
|
- Does not perform deep transitive dependency license analysis
|
||||||
|
- Network lookups (npm view, etc.) may fail in offline or restricted environments
|
||||||
|
- Requires `git` to be available in the execution environment
|
||||||
354
hooks/dependency-license-checker/check-licenses.sh
Executable file
354
hooks/dependency-license-checker/check-licenses.sh
Executable file
@@ -0,0 +1,354 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Dependency License Checker Hook
|
||||||
|
# Scans newly added dependencies for license compliance (GPL, AGPL, etc.)
|
||||||
|
# at session end, before they get committed.
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# LICENSE_MODE - "warn" (log only) or "block" (exit non-zero on violations) (default: warn)
|
||||||
|
# SKIP_LICENSE_CHECK - "true" to disable entirely (default: unset)
|
||||||
|
# LICENSE_LOG_DIR - Directory for check logs (default: logs/copilot/license-checker)
|
||||||
|
# BLOCKED_LICENSES - Comma-separated SPDX IDs to flag (default: copyleft set)
|
||||||
|
# LICENSE_ALLOWLIST - Comma-separated package names to skip (default: unset)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Early exit if disabled
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ "${SKIP_LICENSE_CHECK:-}" == "true" ]]; then
|
||||||
|
echo "⏭️ License check skipped (SKIP_LICENSE_CHECK=true)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure we are in a git repository
|
||||||
|
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
|
echo "⚠️ Not in a git repository, skipping license check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
MODE="${LICENSE_MODE:-warn}"
|
||||||
|
LOG_DIR="${LICENSE_LOG_DIR:-logs/copilot/license-checker}"
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
FINDING_COUNT=0
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
LOG_FILE="$LOG_DIR/check.log"
|
||||||
|
|
||||||
|
# Default blocked licenses (copyleft / restrictive)
|
||||||
|
DEFAULT_BLOCKED="GPL-2.0,GPL-2.0-only,GPL-2.0-or-later,GPL-3.0,GPL-3.0-only,GPL-3.0-or-later,AGPL-1.0,AGPL-3.0,AGPL-3.0-only,AGPL-3.0-or-later,LGPL-2.0,LGPL-2.1,LGPL-2.1-only,LGPL-2.1-or-later,LGPL-3.0,LGPL-3.0-only,LGPL-3.0-or-later,SSPL-1.0,EUPL-1.1,EUPL-1.2,OSL-3.0,CPAL-1.0,CPL-1.0,CC-BY-SA-4.0,CC-BY-NC-4.0,CC-BY-NC-SA-4.0"
|
||||||
|
|
||||||
|
BLOCKED_LIST=()
|
||||||
|
IFS=',' read -ra BLOCKED_LIST <<< "${BLOCKED_LICENSES:-$DEFAULT_BLOCKED}"
|
||||||
|
|
||||||
|
# Parse allowlist
|
||||||
|
ALLOWLIST=()
|
||||||
|
if [[ -n "${LICENSE_ALLOWLIST:-}" ]]; then
|
||||||
|
IFS=',' read -ra ALLOWLIST <<< "$LICENSE_ALLOWLIST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
json_escape() {
|
||||||
|
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
is_allowlisted() {
|
||||||
|
local pkg="$1"
|
||||||
|
for entry in "${ALLOWLIST[@]}"; do
|
||||||
|
entry=$(printf '%s' "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||||
|
[[ -z "$entry" ]] && continue
|
||||||
|
if [[ "$pkg" == "$entry" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_blocked_license() {
|
||||||
|
local license="$1"
|
||||||
|
local license_lower
|
||||||
|
license_lower=$(printf '%s' "$license" | tr '[:upper:]' '[:lower:]')
|
||||||
|
for blocked in "${BLOCKED_LIST[@]}"; do
|
||||||
|
blocked=$(printf '%s' "$blocked" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||||
|
[[ -z "$blocked" ]] && continue
|
||||||
|
local blocked_lower
|
||||||
|
blocked_lower=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
|
||||||
|
# Substring match to handle SPDX variants and compound expressions
|
||||||
|
if [[ "$license_lower" == *"$blocked_lower"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 1: Detect new dependencies per ecosystem
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
NEW_DEPS=()
|
||||||
|
|
||||||
|
# npm / yarn / pnpm — package.json
|
||||||
|
if git diff HEAD -- package.json &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Match added lines like: "package-name": "^1.0.0"
|
||||||
|
pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*"\([^"]*\)"[[:space:]]*:[[:space:]]*"[^"]*".*/\1/p')
|
||||||
|
if [[ -n "$pkg" && "$pkg" != "name" && "$pkg" != "version" && "$pkg" != "description" && "$pkg" != "main" && "$pkg" != "scripts" && "$pkg" != "dependencies" && "$pkg" != "devDependencies" && "$pkg" != "peerDependencies" && "$pkg" != "optionalDependencies" ]]; then
|
||||||
|
NEW_DEPS+=("npm:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- package.json 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pip — requirements.txt
|
||||||
|
if git diff HEAD -- requirements.txt &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Skip comments and blank lines
|
||||||
|
clean=$(printf '%s' "$line" | sed 's/^+//')
|
||||||
|
[[ "$clean" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
[[ -z "$clean" ]] && continue
|
||||||
|
# Extract package name before ==, >=, <=, ~=, !=, etc.
|
||||||
|
pkg=$(printf '%s' "$clean" | sed 's/[[:space:]]*[><=!~].*//' | sed 's/[[:space:]]*//')
|
||||||
|
if [[ -n "$pkg" ]]; then
|
||||||
|
NEW_DEPS+=("pip:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- requirements.txt 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pip — pyproject.toml
|
||||||
|
if git diff HEAD -- pyproject.toml &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Match added lines with quoted dependency strings
|
||||||
|
pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*"\([A-Za-z0-9_-]*\).*/\1/p')
|
||||||
|
if [[ -n "$pkg" ]]; then
|
||||||
|
NEW_DEPS+=("pip:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- pyproject.toml 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Go — go.mod
|
||||||
|
if git diff HEAD -- go.mod &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Match added require entries like: + github.com/foo/bar v1.2.3
|
||||||
|
pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*\([a-zA-Z0-9._/-]*\.[a-zA-Z0-9._/-]*\)[[:space:]].*/\1/p')
|
||||||
|
if [[ -n "$pkg" && "$pkg" != "module" && "$pkg" != "go" && "$pkg" != "require" ]]; then
|
||||||
|
NEW_DEPS+=("go:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- go.mod 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ruby — Gemfile
|
||||||
|
if git diff HEAD -- Gemfile &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Match added gem lines like: +gem 'package-name'
|
||||||
|
pkg=$(printf '%s' "$line" | sed -n "s/^+[[:space:]]*gem[[:space:]]*['\"\`]\([^'\"\`]*\)['\"\`].*/\1/p")
|
||||||
|
if [[ -n "$pkg" ]]; then
|
||||||
|
NEW_DEPS+=("ruby:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- Gemfile 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rust — Cargo.toml
|
||||||
|
if git diff HEAD -- Cargo.toml &>/dev/null; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Match added dependency entries like: +package-name = "1.0" or +package-name = { version = "1.0" }
|
||||||
|
pkg=$(printf '%s' "$line" | sed -n 's/^+[[:space:]]*\([a-zA-Z0-9_-]*\)[[:space:]]*=.*/\1/p')
|
||||||
|
if [[ -n "$pkg" && "$pkg" != "name" && "$pkg" != "version" && "$pkg" != "edition" && "$pkg" != "authors" && "$pkg" != "description" && "$pkg" != "license" && "$pkg" != "repository" && "$pkg" != "rust-version" ]]; then
|
||||||
|
NEW_DEPS+=("rust:$pkg")
|
||||||
|
fi
|
||||||
|
done < <(git diff HEAD -- Cargo.toml 2>/dev/null | grep '^+' | grep -v '^+++')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit clean if no new dependencies found
|
||||||
|
if [[ ${#NEW_DEPS[@]} -eq 0 ]]; then
|
||||||
|
echo "✅ No new dependencies detected"
|
||||||
|
printf '{"timestamp":"%s","event":"license_check_complete","mode":"%s","status":"clean","dependencies_checked":0}\n' \
|
||||||
|
"$TIMESTAMP" "$MODE" >> "$LOG_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Checking licenses for ${#NEW_DEPS[@]} new dependency(ies)..."
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 2: Check license per dependency
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RESULTS=()
|
||||||
|
|
||||||
|
get_license() {
|
||||||
|
local ecosystem="$1"
|
||||||
|
local pkg="$2"
|
||||||
|
local license="UNKNOWN"
|
||||||
|
|
||||||
|
case "$ecosystem" in
|
||||||
|
npm)
|
||||||
|
# Primary: check node_modules
|
||||||
|
if [[ -f "node_modules/$pkg/package.json" ]]; then
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
license=$(jq -r '.license // "UNKNOWN"' "node_modules/$pkg/package.json" 2>/dev/null || echo "UNKNOWN")
|
||||||
|
else
|
||||||
|
license=$(grep -oE '"license"\s*:\s*"[^"]*"' "node_modules/$pkg/package.json" 2>/dev/null | head -1 | sed 's/.*"license"\s*:\s*"//;s/"//' || echo "UNKNOWN")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Fallback: npm view
|
||||||
|
if [[ "$license" == "UNKNOWN" ]] && command -v npm &>/dev/null; then
|
||||||
|
license=$(timeout 5 npm view "$pkg" license 2>/dev/null || echo "UNKNOWN")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
pip)
|
||||||
|
# Primary: pip show
|
||||||
|
if command -v pip &>/dev/null; then
|
||||||
|
license=$(timeout 5 pip show "$pkg" 2>/dev/null | grep -i '^License:' | sed 's/^[Ll]icense:[[:space:]]*//' || echo "UNKNOWN")
|
||||||
|
elif command -v pip3 &>/dev/null; then
|
||||||
|
license=$(timeout 5 pip3 show "$pkg" 2>/dev/null | grep -i '^License:' | sed 's/^[Ll]icense:[[:space:]]*//' || echo "UNKNOWN")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
go)
|
||||||
|
# Check module cache for LICENSE file
|
||||||
|
local gopath="${GOPATH:-$HOME/go}"
|
||||||
|
local mod_dir="$gopath/pkg/mod/$pkg"
|
||||||
|
# Try to find the latest version directory
|
||||||
|
if [[ -d "$gopath/pkg/mod" ]]; then
|
||||||
|
local found_dir
|
||||||
|
found_dir=$(find "$gopath/pkg/mod" -maxdepth 4 -path "*${pkg}@*" -type d 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$found_dir" ]]; then
|
||||||
|
local lic_file
|
||||||
|
lic_file=$(find "$found_dir" -maxdepth 1 -iname 'LICENSE*' -type f 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$lic_file" ]]; then
|
||||||
|
# Keyword match against common license identifiers
|
||||||
|
if grep -qiE 'GNU GENERAL PUBLIC LICENSE' "$lic_file" 2>/dev/null; then
|
||||||
|
if grep -qiE 'Version 3' "$lic_file" 2>/dev/null; then
|
||||||
|
license="GPL-3.0"
|
||||||
|
elif grep -qiE 'Version 2' "$lic_file" 2>/dev/null; then
|
||||||
|
license="GPL-2.0"
|
||||||
|
else
|
||||||
|
license="GPL"
|
||||||
|
fi
|
||||||
|
elif grep -qiE 'GNU LESSER GENERAL PUBLIC' "$lic_file" 2>/dev/null; then
|
||||||
|
license="LGPL"
|
||||||
|
elif grep -qiE 'GNU AFFERO GENERAL PUBLIC' "$lic_file" 2>/dev/null; then
|
||||||
|
license="AGPL-3.0"
|
||||||
|
elif grep -qiE 'MIT License' "$lic_file" 2>/dev/null; then
|
||||||
|
license="MIT"
|
||||||
|
elif grep -qiE 'Apache License' "$lic_file" 2>/dev/null; then
|
||||||
|
license="Apache-2.0"
|
||||||
|
elif grep -qiE 'BSD' "$lic_file" 2>/dev/null; then
|
||||||
|
license="BSD"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
ruby)
|
||||||
|
# gem spec
|
||||||
|
if command -v gem &>/dev/null; then
|
||||||
|
license=$(timeout 5 gem spec "$pkg" license 2>/dev/null | grep -v '^---' | grep -v '^\.\.\.' | sed 's/^- //' | head -1 || echo "UNKNOWN")
|
||||||
|
[[ -z "$license" ]] && license="UNKNOWN"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rust)
|
||||||
|
# cargo metadata
|
||||||
|
if command -v cargo &>/dev/null; then
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
license=$(timeout 5 cargo metadata --format-version 1 2>/dev/null | jq -r ".packages[] | select(.name == \"$pkg\") | .license // \"UNKNOWN\"" 2>/dev/null | head -1 || echo "UNKNOWN")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Normalize empty / whitespace-only to UNKNOWN
|
||||||
|
license=$(printf '%s' "$license" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||||
|
[[ -z "$license" ]] && license="UNKNOWN"
|
||||||
|
|
||||||
|
printf '%s' "$license"
|
||||||
|
}
|
||||||
|
|
||||||
|
for dep in "${NEW_DEPS[@]}"; do
|
||||||
|
ecosystem="${dep%%:*}"
|
||||||
|
pkg="${dep#*:}"
|
||||||
|
|
||||||
|
license=$(get_license "$ecosystem" "$pkg")
|
||||||
|
RESULTS+=("$ecosystem $pkg $license")
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 3 & 4: Check against blocked list and allowlist
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
VIOLATIONS=()
|
||||||
|
|
||||||
|
for result in "${RESULTS[@]}"; do
|
||||||
|
IFS=$'\t' read -r ecosystem pkg license <<< "$result"
|
||||||
|
|
||||||
|
# Phase 4: Skip allowlisted packages
|
||||||
|
if [[ ${#ALLOWLIST[@]} -gt 0 ]] && is_allowlisted "$pkg"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 3: Check against blocked list
|
||||||
|
if is_blocked_license "$license"; then
|
||||||
|
VIOLATIONS+=("$pkg $ecosystem $license BLOCKED")
|
||||||
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 5: Output & logging
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
printf " %-30s %-12s %-30s %s\n" "PACKAGE" "ECOSYSTEM" "LICENSE" "STATUS"
|
||||||
|
printf " %-30s %-12s %-30s %s\n" "-------" "---------" "-------" "------"
|
||||||
|
|
||||||
|
for result in "${RESULTS[@]}"; do
|
||||||
|
IFS=$'\t' read -r ecosystem pkg license <<< "$result"
|
||||||
|
|
||||||
|
status="OK"
|
||||||
|
if [[ ${#ALLOWLIST[@]} -gt 0 ]] && is_allowlisted "$pkg"; then
|
||||||
|
status="ALLOWLISTED"
|
||||||
|
elif is_blocked_license "$license"; then
|
||||||
|
status="BLOCKED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %-30s %-12s %-30s %s\n" "$pkg" "$ecosystem" "$license" "$status"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build JSON findings array
|
||||||
|
FINDINGS_JSON="["
|
||||||
|
FIRST=true
|
||||||
|
for violation in "${VIOLATIONS[@]}"; do
|
||||||
|
IFS=$'\t' read -r pkg ecosystem license status <<< "$violation"
|
||||||
|
if [[ "$FIRST" != "true" ]]; then
|
||||||
|
FINDINGS_JSON+=","
|
||||||
|
fi
|
||||||
|
FIRST=false
|
||||||
|
FINDINGS_JSON+="{\"package\":\"$(json_escape "$pkg")\",\"ecosystem\":\"$(json_escape "$ecosystem")\",\"license\":\"$(json_escape "$license")\",\"status\":\"$(json_escape "$status")\"}"
|
||||||
|
done
|
||||||
|
FINDINGS_JSON+="]"
|
||||||
|
|
||||||
|
# Write structured log entry
|
||||||
|
printf '{"timestamp":"%s","event":"license_check_complete","mode":"%s","dependencies_checked":%d,"violation_count":%d,"violations":%s}\n' \
|
||||||
|
"$TIMESTAMP" "$MODE" "${#RESULTS[@]}" "$FINDING_COUNT" "$FINDINGS_JSON" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
if [[ $FINDING_COUNT -gt 0 ]]; then
|
||||||
|
echo "⚠️ Found $FINDING_COUNT license violation(s):"
|
||||||
|
echo ""
|
||||||
|
for violation in "${VIOLATIONS[@]}"; do
|
||||||
|
IFS=$'\t' read -r pkg ecosystem license status <<< "$violation"
|
||||||
|
echo " - $pkg ($ecosystem): $license"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$MODE" == "block" ]]; then
|
||||||
|
echo "🚫 Session blocked: resolve license violations above before committing."
|
||||||
|
echo " Set LICENSE_MODE=warn to log without blocking, or add packages to LICENSE_ALLOWLIST."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "💡 Review the violations above. Set LICENSE_MODE=block to prevent commits with license issues."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "✅ All ${#RESULTS[@]} dependencies have compliant licenses"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
16
hooks/dependency-license-checker/hooks.json
Normal file
16
hooks/dependency-license-checker/hooks.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"hooks": {
|
||||||
|
"sessionEnd": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"bash": ".github/hooks/dependency-license-checker/check-licenses.sh",
|
||||||
|
"cwd": ".",
|
||||||
|
"env": {
|
||||||
|
"LICENSE_MODE": "warn"
|
||||||
|
},
|
||||||
|
"timeoutSec": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user