From e8b1e9350ceb08ca0de5ec1da548c3168df7edc4 Mon Sep 17 00:00:00 2001 From: Ajith Raghavan <37246967+ajithraghavan@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:54:53 +0530 Subject: [PATCH] Dependency License Checker Hook (#1046) --- docs/README.hooks.md | 1 + hooks/dependency-license-checker/README.md | 214 +++++++++++ .../check-licenses.sh | 354 ++++++++++++++++++ hooks/dependency-license-checker/hooks.json | 16 + 4 files changed, 585 insertions(+) create mode 100644 hooks/dependency-license-checker/README.md create mode 100755 hooks/dependency-license-checker/check-licenses.sh create mode 100644 hooks/dependency-license-checker/hooks.json diff --git a/docs/README.hooks.md b/docs/README.hooks.md index e2320032..1d11a84a 100644 --- a/docs/README.hooks.md +++ b/docs/README.hooks.md @@ -31,6 +31,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-hooks) for guidelines on how to | 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`
`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`
`audit-session-end.sh`
`audit-session-start.sh`
`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`
`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`
`hooks.json` | diff --git a/hooks/dependency-license-checker/README.md b/hooks/dependency-license-checker/README.md new file mode 100644 index 00000000..6d54f0cc --- /dev/null +++ b/hooks/dependency-license-checker/README.md @@ -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//package.json` license field | `npm view license` | +| pip | `requirements.txt`, `pyproject.toml` | `pip show ` License field | UNKNOWN | +| Go | `go.mod` | LICENSE file in module cache (keyword match) | UNKNOWN | +| Ruby | `Gemfile` | `gem spec 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 diff --git a/hooks/dependency-license-checker/check-licenses.sh b/hooks/dependency-license-checker/check-licenses.sh new file mode 100755 index 00000000..6e465d43 --- /dev/null +++ b/hooks/dependency-license-checker/check-licenses.sh @@ -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 diff --git a/hooks/dependency-license-checker/hooks.json b/hooks/dependency-license-checker/hooks.json new file mode 100644 index 00000000..f1371b84 --- /dev/null +++ b/hooks/dependency-license-checker/hooks.json @@ -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 + } + ] + } +}