Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,171 @@
#!/usr/bin/env bash
# audit.sh — license-audit skill (placeholder for PR 1 of CSE v1).
# audit.sh — license-audit skill helper.
#
# PR 2 of the Custom Stack Examples v1 round replaces this body with
# real license classification across npm/pip/go manifests. PR 1 ships
# only the scaffolding so the static contract validates.
# Walks direct dependencies of the project in cwd and classifies each
# declared license into one of four families: permissive, weak
# copyleft, strong copyleft, unknown. Emits a JSON object with a
# `counts` map and a `flagged` list of strong-copyleft hits.
#
# When PR 2 lands, the helper must:
# - Detect the project stack from package.json | requirements.txt |
# pyproject.toml | go.mod.
# - Classify each direct dependency's license into permissive,
# weak_copyleft, strong_copyleft, or unknown.
# - Print a JSON object on stdout: { counts, flagged }.
# - Exit 0 always; the artifact's summary.status carries OK/WARN/BLOCKED.
set -e

cat <<'JSON'
{
"counts": { "total": 0, "permissive": 0, "weak_copyleft": 0, "strong_copyleft": 0, "unknown": 0 },
"flagged": [],
"_placeholder": "license-audit/bin/audit.sh — PR 1 stub. Real behavior lands in PR 2."
# Stack detection is automatic: the helper looks for package.json,
# requirements.txt, pyproject.toml, or go.mod in the current directory.
# Pass an optional positional argument (node|python|go) to force a
# specific stack when more than one manifest is present.
#
# This is a release-hygiene check, not a production license scanner.
# Direct dependencies only; transitive deps are out of scope. For
# Node, license metadata is read from each module's package.json
# under node_modules/ when available; otherwise the dep is recorded
# as unknown. Python and Go manifests do not declare license metadata,
# so deps from those stacks always classify as unknown unless the
# user runs a deeper auditor.
#
# Exit always 0; the artifact's summary.status carries OK/WARN/BLOCKED
# (computed from the counts and flagged list by the calling skill).
set -eu

# ─── Stack detection ─────────────────────────────────────────
detect_stack() {
if [ -f package.json ]; then printf 'node'; return; fi
if [ -f pyproject.toml ] || [ -f requirements.txt ]; then printf 'python'; return; fi
if [ -f go.mod ]; then printf 'go'; return; fi
printf 'none'
}

STACK="${1:-$(detect_stack)}"

# ─── License family classifier ───────────────────────────────
classify() {
local lic="$1"
lic=$(printf '%s' "$lic" | tr '[:lower:]' '[:upper:]' | tr -d ' "')
case "$lic" in
MIT|BSD*|APACHE*|ISC|0BSD|UNLICENSE|CC0*) printf 'permissive' ;;
LGPL*|MPL*|EPL*) printf 'weak_copyleft' ;;
GPL*|AGPL*) printf 'strong_copyleft' ;;
*) printf 'unknown' ;;
esac
}

# ─── Per-stack scanners ──────────────────────────────────────
scan_node() {
[ -f package.json ] || return 0
jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' package.json 2>/dev/null | while read -r dep; do
[ -z "$dep" ] && continue
local mod_pkg="node_modules/$dep/package.json"
local lic="unknown"
if [ -f "$mod_pkg" ]; then
lic=$(jq -r '
.license // (
.licenses // []
| if type == "array" and length > 0 then (.[0].type // "unknown")
else "unknown" end
)
' "$mod_pkg" 2>/dev/null)
[ -z "$lic" ] || [ "$lic" = "null" ] && lic="unknown"
fi
printf '%s\t%s\n' "$dep" "$lic"
done
}

scan_python() {
if [ -f pyproject.toml ]; then
grep -E '^[a-zA-Z0-9_-]+[ ]*=|^"[^"]+' pyproject.toml 2>/dev/null | head -50 | \
awk -F'[="]' '{print $1}' | sed 's/[[:space:]]//g' | while read -r dep; do
[ -z "$dep" ] && continue
printf '%s\tunknown\n' "$dep"
done
elif [ -f requirements.txt ]; then
grep -vE '^#|^$' requirements.txt 2>/dev/null | while read -r line; do
local dep="${line%%[<>=~!]*}"
dep=$(printf '%s' "$dep" | tr -d '[:space:]')
[ -z "$dep" ] && continue
printf '%s\tunknown\n' "$dep"
done
fi
}

scan_go() {
[ -f go.mod ] || return 0
# go.mod has two require forms:
# 1. Block: `require ( <module> <version>\n ... )` -> indented module names.
# 2. Single: `require <module> <version>` -> top-level statement.
# Cover both; the original implementation only handled the block
# form and silently dropped single-line require statements.
awk '
BEGIN { in_block = 0 }
/^require[[:space:]]*\(/ { in_block = 1; next }
/^\)/ { in_block = 0; next }
in_block && /^[[:space:]]+[a-zA-Z0-9._\/-]+/ {
# Strip indirect comments and emit module name (column 1 after trim).
sub(/^[[:space:]]+/, ""); print $1; next
}
/^require[[:space:]]+[a-zA-Z0-9._\/-]+/ {
# Single-line: `require <module> <version>` -> module is column 2.
print $2
}
' go.mod 2>/dev/null | while read -r dep; do
[ -z "$dep" ] && continue
printf '%s\tunknown\n' "$dep"
done
}
JSON

# ─── Run and aggregate ───────────────────────────────────────
case "$STACK" in
node) RAW=$(scan_node) ;;
python) RAW=$(scan_python) ;;
go) RAW=$(scan_go) ;;
none)
# No supported manifest in cwd. Emit an empty result rather than
# erroring; the calling skill marks status=WARN with a clear
# next_action ("no supported manifest found").
RAW=""
;;
*) echo "unknown stack: $STACK" >&2; exit 2 ;;
esac

PERMISSIVE=0
WEAK=0
STRONG=0
UNKNOWN=0
FLAGGED_LIST=""

while IFS=$'\t' read -r name license; do
[ -z "$name" ] && continue
family=$(classify "$license")
case "$family" in
permissive) PERMISSIVE=$((PERMISSIVE + 1)) ;;
weak_copyleft) WEAK=$((WEAK + 1)) ;;
strong_copyleft)
STRONG=$((STRONG + 1))
FLAGGED_LIST="${FLAGGED_LIST}${name}|${license}
"
;;
unknown) UNKNOWN=$((UNKNOWN + 1)) ;;
esac
done <<< "$RAW"

FLAGGED="[]"
if [ -n "$FLAGGED_LIST" ]; then
FLAGGED=$(printf '%s' "$FLAGGED_LIST" | awk -F'|' 'NF==2 {printf "{\"name\":\"%s\",\"license\":\"%s\"}\n",$1,$2}' | jq -s '.')
fi

TOTAL=$((PERMISSIVE + WEAK + STRONG + UNKNOWN))

jq -n \
--arg stack "$STACK" \
--argjson total "$TOTAL" \
--argjson permissive "$PERMISSIVE" \
--argjson weak "$WEAK" \
--argjson strong "$STRONG" \
--argjson unknown "$UNKNOWN" \
--argjson flagged "$FLAGGED" \
'{
stack: $stack,
counts: {
total: $total,
permissive: $permissive,
weak_copyleft: $weak,
strong_copyleft: $strong,
unknown: $unknown
},
flagged: $flagged
}'
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
#!/usr/bin/env bash
# smoke.sh — license-audit smoke check (placeholder for PR 1).
# smoke.sh — license-audit runtime sanity check.
#
# PR 2 replaces this with a real /tmp project + manifests check.
# PR 1 ships only the file so the static contract validates that
# every skill folder has a smoke.sh.
set -e
# Sets up three minimal projects in /tmp (Node with one MIT dep
# installed under node_modules/, Python with a single requirements.txt
# entry, Go with a single go.mod require). Asserts:
# 1. Stack auto-detection picks the right manifest in each case.
# 2. The Node case classifies an MIT dep as permissive (read from
# node_modules/<dep>/package.json).
# 3. Python and Go cases classify their unknown-license deps as
# `unknown` (the helper has no way to know without a deeper
# auditor).
# 4. A GPL dep installed under node_modules/ classifies as
# strong_copyleft and lands in `flagged`.
set -eu

SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
AUDIT="$SKILL_DIR/bin/audit.sh"
Expand All @@ -13,17 +21,116 @@ if [ ! -x "$AUDIT" ]; then
echo "FAIL: $AUDIT is not executable" >&2
exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
echo "FAIL: jq is required for the smoke check" >&2
exit 1
fi

out=$( "$AUDIT" 2>/dev/null )
if ! printf '%s' "$out" | jq -e '.counts' >/dev/null 2>&1; then
echo "FAIL: audit.sh did not emit a JSON object with .counts" >&2
echo "$out" >&2
exit 1
tmp=$(mktemp -d /tmp/license-audit-smoke.XXXXXX)
trap 'rm -rf "$tmp"' EXIT
fail=0

# ─── Case 1: Node + permissive MIT dep installed ────────────
mkdir -p "$tmp/node-permissive/node_modules/lodash"
cat > "$tmp/node-permissive/package.json" <<'PKG'
{ "name": "smoke-node", "dependencies": { "lodash": "4.17.21" } }
PKG
cat > "$tmp/node-permissive/node_modules/lodash/package.json" <<'PKG'
{ "name": "lodash", "version": "4.17.21", "license": "MIT" }
PKG
out=$( cd "$tmp/node-permissive" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.stack == "node" and .counts.permissive == 1 and .counts.strong_copyleft == 0' >/dev/null 2>&1; then
echo " ok node MIT dep classifies as permissive"
else
echo "FAIL: node MIT case wrong"
echo "$out"
fail=1
fi

# ─── Case 2: Node + GPL dep installed ───────────────────────
mkdir -p "$tmp/node-gpl/node_modules/some-gpl-thing"
cat > "$tmp/node-gpl/package.json" <<'PKG'
{ "name": "smoke-gpl", "dependencies": { "some-gpl-thing": "1.0.0" } }
PKG
cat > "$tmp/node-gpl/node_modules/some-gpl-thing/package.json" <<'PKG'
{ "name": "some-gpl-thing", "version": "1.0.0", "license": "GPL-3.0" }
PKG
out=$( cd "$tmp/node-gpl" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.counts.strong_copyleft == 1 and (.flagged | length) == 1 and .flagged[0].name == "some-gpl-thing"' >/dev/null 2>&1; then
echo " ok GPL dep classifies as strong_copyleft and lands in flagged"
else
echo "FAIL: node GPL case wrong"
echo "$out"
fail=1
fi

# ─── Case 3: Python requirements.txt ────────────────────────
printf 'requests==2.31.0\nflask>=2.0\n' > "$tmp/py-req.txt"
mkdir -p "$tmp/python-req"
cp "$tmp/py-req.txt" "$tmp/python-req/requirements.txt"
out=$( cd "$tmp/python-req" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.stack == "python" and .counts.unknown >= 2' >/dev/null 2>&1; then
echo " ok python requirements.txt deps classify as unknown"
else
echo "FAIL: python case wrong"
echo "$out"
fail=1
fi

# ─── Case 4: Go go.mod ──────────────────────────────────────
mkdir -p "$tmp/go-mod"
cat > "$tmp/go-mod/go.mod" <<'GOMOD'
module smoke

go 1.21

require (
github.com/stretchr/testify v1.8.4
github.com/spf13/cobra v1.7.0
)
GOMOD
out=$( cd "$tmp/go-mod" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.stack == "go" and .counts.total >= 2 and .counts.unknown >= 2' >/dev/null 2>&1; then
echo " ok go module deps classify as unknown"
else
echo "FAIL: go case wrong"
echo "$out"
fail=1
fi

echo "OK: license-audit placeholder smoke passed (PR 2 wires the real behavior)"
# ─── Case 5: No manifest in cwd ─────────────────────────────
mkdir -p "$tmp/empty"
out=$( cd "$tmp/empty" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.stack == "none" and .counts.total == 0' >/dev/null 2>&1; then
echo " ok empty project classifies as stack=none with zero counts"
else
echo "FAIL: empty case wrong"
echo "$out"
fail=1
fi

# ─── Case 6: Go single-line require form ────────────────────
# A common minimal go.mod uses `require <module> <version>` without
# a require block. The original scanner only matched indented entries
# inside `require (...)`, dropping single-line deps silently.
mkdir -p "$tmp/go-single"
cat > "$tmp/go-single/go.mod" <<'GOMOD'
module smoke

go 1.21

require github.com/spf13/cobra v1.8.0
GOMOD
out=$( cd "$tmp/go-single" && "$AUDIT" 2>&1 )
if echo "$out" | jq -e '.stack == "go" and .counts.total == 1 and .counts.unknown == 1' >/dev/null 2>&1; then
echo " ok go single-line require captures the dep"
else
echo "FAIL: go single-line case wrong (counts.total should be 1)"
echo "$out"
fail=1
fi

if [ "$fail" -eq 0 ]; then
echo "OK: license-audit smoke passed (6 cases)"
fi
exit $fail
Loading
Loading