Supply Chain Scanner: npm #687
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Supply Chain Scanner: npm" | |
| on: | |
| schedule: | |
| - cron: '0 */3 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| jobs: | |
| scan: | |
| name: Scan npm packages | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.25' | |
| - name: Build knowing | |
| run: GOWORK=off go build -o knowing ./cmd/knowing | |
| - name: Fetch packages to scan | |
| run: | | |
| chmod +x scripts/fetch-recent-publishes.sh scripts/fetch-npm-recent-changes.sh scripts/check-isolation-score.sh | |
| # Try real-time feed, fall back to watchlist, fall back to inline | |
| ./scripts/fetch-npm-recent-changes.sh 180 50 > packages.txt 2>/dev/null || true | |
| COUNT=$(grep -cv "^#" packages.txt 2>/dev/null || echo 0) | |
| if [ "$COUNT" -lt 3 ]; then | |
| ./scripts/fetch-recent-publishes.sh > packages.txt 2>/dev/null || true | |
| COUNT=$(grep -cv "^#" packages.txt 2>/dev/null || echo 0) | |
| fi | |
| if [ "$COUNT" -lt 1 ]; then | |
| cat > packages.txt << 'FALLBACK' | |
| express 5.1.0 5.0.1 | |
| axios 1.9.0 1.8.4 | |
| lodash 4.17.21 4.17.20 | |
| FALLBACK | |
| fi | |
| echo "Packages to scan:" | |
| cat packages.txt | |
| - name: Scan packages | |
| run: | | |
| mkdir -p scan-output | |
| while IFS=' ' read -r pkg new_ver prev_ver; do | |
| pkg=$(echo "$pkg" | tr -d '[:space:]') | |
| new_ver=$(echo "$new_ver" | tr -d '[:space:]') | |
| prev_ver=$(echo "$prev_ver" | tr -d '[:space:]') | |
| [ -z "$pkg" ] && continue | |
| [[ "$pkg" == \#* ]] && continue | |
| echo "" | |
| echo "=== Scanning $pkg: $prev_ver -> $new_ver ===" | |
| WORK=$(mktemp -d) | |
| npm pack "${pkg}@${prev_ver}" --pack-destination "$WORK" 2>/dev/null || { rm -rf "$WORK"; continue; } | |
| npm pack "${pkg}@${new_ver}" --pack-destination "$WORK" 2>/dev/null || { rm -rf "$WORK"; continue; } | |
| mkdir -p "$WORK/old" "$WORK/new" | |
| for f in "$WORK"/*"${prev_ver}"*.tgz; do [ -f "$f" ] && tar xzf "$f" -C "$WORK/old" --strip-components=1 && break; done | |
| for f in "$WORK"/*"${new_ver}"*.tgz; do [ -f "$f" ] && tar xzf "$f" -C "$WORK/new" --strip-components=1 && break; done | |
| git config --global user.email "ci@knowing.dev" 2>/dev/null || true | |
| git config --global user.name "knowing-ci" 2>/dev/null || true | |
| (cd "$WORK/old" && git init -q && git add . && git commit -q -m "old" 2>/dev/null) || true | |
| (cd "$WORK/new" && git init -q && git add . && git commit -q -m "new" 2>/dev/null) || true | |
| ./knowing index -db "$WORK/new.db" -no-enrich "$WORK/new" 2>/dev/null || { rm -rf "$WORK"; continue; } | |
| SNAP=$(sqlite3 "$WORK/new.db" "SELECT hex(snapshot_hash) FROM snapshots LIMIT 1" 2>/dev/null || echo "") | |
| if [ -n "$SNAP" ]; then | |
| ./knowing audit-supply-chain -db "$WORK/new.db" -base "$SNAP" -head "$SNAP" -scan-all -o "$WORK/report.json" 2>/dev/null || true | |
| if [ -f "$WORK/report.json" ]; then | |
| MAX_SCORE=$(bash scripts/check-isolation-score.sh "$WORK/report.json" 0.3) && { | |
| echo " *** SUSPICIOUS: isolation score $MAX_SCORE ***" | |
| SAFE=$(echo "${pkg}_${new_ver}" | tr '/@' '__') | |
| cp "$WORK/report.json" "scan-output/${SAFE}.json" | |
| } || echo " Clean (score: $MAX_SCORE)" | |
| fi | |
| fi | |
| rm -rf "$WORK" | |
| done < packages.txt | |
| - name: Report | |
| if: always() | |
| run: | | |
| TOTAL=$(grep -cv "^#" packages.txt 2>/dev/null || echo 0) | |
| ALERTS=$(find scan-output -name "*.json" 2>/dev/null | wc -l | tr -d ' ') | |
| { | |
| echo "## npm Supply Chain Scanner" | |
| echo "" | |
| echo "Scanned **${TOTAL}** packages." | |
| echo "" | |
| if [ "$ALERTS" -gt 0 ]; then | |
| echo "### :rotating_light: $ALERTS suspicious package(s) detected" | |
| echo "" | |
| for f in scan-output/*.json; do | |
| [ -f "$f" ] || continue | |
| PKG=$(basename "$f" .json | tr '_' '/') | |
| echo "<details><summary>$PKG</summary>" | |
| echo "" | |
| echo '```json' | |
| python3 -m json.tool "$f" 2>/dev/null || cat "$f" | |
| echo '```' | |
| echo "</details>" | |
| echo "" | |
| done | |
| else | |
| echo ":white_check_mark: No suspicious packages detected." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$ALERTS" -gt 0 ]; then | |
| gh issue create \ | |
| --title "[DRAFT] npm supply chain alert: $(date -u +%Y-%m-%d)" \ | |
| --body "$(cat "$GITHUB_STEP_SUMMARY")" \ | |
| --label "supply-chain-alert,draft-disclosure" || true | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: npm-scan-${{ github.run_id }} | |
| path: scan-output/ | |
| retention-days: 90 | |
| if-no-files-found: ignore |