diff --git a/CHANGELOG.md b/CHANGELOG.md index e1272b7..06d3685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/) and semantic versioning. +## [0.1.4] - 2026-09-09 + +### Fixed + +- Include `.bat`, `.cmd`, `.fish`, and `.psm1` scripts in directory scans, + including nested files and uppercase extensions. + ## [0.1.3] - 2026-09-08 ### Fixed diff --git a/README.md b/README.md index cd41541..c3bf88a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Agent **skills** are the new plugin. A skill is a `SKILL.md` plus scripts that a It reads `SKILL.md` **prose** for instruction-injection and reads **scripts and fenced code blocks** for dangerous commands — so a `chmod 777` mentioned in a sentence won't false-positive, but the same command in a code block will. +Directory scans include `.bat`, `.cmd`, `.fish`, and `.psm1` scripts alongside the other supported script and text formats. Extensions are matched case-insensitively, including in nested directories. + See every rule: `npx @royalpinto007/skill-audit --rules`. ## Usage diff --git a/package.json b/package.json index 31d2dd3..6a65a69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@royalpinto007/skill-audit", - "version": "0.1.3", + "version": "0.1.4", "description": "Security scanner for agent skills. Scan a Claude/agent Skill for prompt-injection, dangerous shell, secret access, and exfiltration before you trust it. Zero dependencies, SARIF output, npx skill-audit .", "type": "module", "bin": { diff --git a/src/scan.js b/src/scan.js index c3293c1..6b4719b 100644 --- a/src/scan.js +++ b/src/scan.js @@ -3,7 +3,7 @@ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, s import { join, extname, basename, relative } from "node:path"; import { RULES, matchesOf } from "./rules.js"; -const CODE_EXT = new Set([".sh", ".bash", ".zsh", ".py", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl", ".ps1"]); +const CODE_EXT = new Set([".sh", ".bash", ".zsh", ".fish", ".bat", ".cmd", ".py", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl", ".ps1", ".psm1"]); const TEXT_EXT = new Set([".md", ".markdown", ".mdx", ".txt", ".json", ".yaml", ".yml", ".toml"]); const SKIP_DIR = new Set([".git", "node_modules", ".venv", "dist", "build", "__pycache__"]); const MAX_BYTES = 2_000_000; diff --git a/test/skill-audit.test.js b/test/skill-audit.test.js index 37166ba..4571336 100644 --- a/test/skill-audit.test.js +++ b/test/skill-audit.test.js @@ -1,9 +1,10 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; -import { basename, dirname, join } from "node:path"; +import { basename, dirname, join, relative } from "node:path"; import { collectFiles, scanSkill, scanText } from "../src/scan.js"; import { exitCode, sarifReport, jsonReport, counts } from "../src/report.js"; import { RULES } from "../src/rules.js"; @@ -153,3 +154,33 @@ test("hardening: browser creds, persistence, anti-forensics, dynamic exec", () = const py = "exec(payload)\n"; assert.ok(scanText(py, "x.py", null).some((x) => x.rule === "SKILL-OBF-003")); }); + +test("directory walks scan batch, fish, and PowerShell module scripts", (t) => { + const root = mkdtempSync(join(tmpdir(), "skill-audit-extensions-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + const expected = [join(root, "SKILL.md"), join(root, "existing.sh"), join(root, "existing.ps1")]; + for (const file of expected) writeFileSync(file, "echo hello\n"); + + for (const dir of [root, join(root, "nested")]) { + mkdirSync(dir, { recursive: true }); + for (const ext of ["bat", "cmd", "fish", "psm1"]) { + for (const suffix of [ext, ext.toUpperCase()]) { + const file = join(dir, `${suffix === ext ? "lower" : "upper"}.${suffix}`); + // Synthetic scanner input only; these files are never executed. + writeFileSync(file, "https://webhook.site/example\n"); + expected.push(file); + } + } + writeFileSync(join(dir, "ignored.bin"), "https://webhook.site/example\n"); + } + + assert.deepEqual(collectFiles(root).sort(), expected.sort()); + const result = scanSkill(root); + assert.equal(result.files, expected.length); + const flaggedFiles = result.findings + .filter((finding) => finding.rule === "SKILL-NET-002") + .map((finding) => finding.file).sort(); + assert.deepEqual(flaggedFiles, expected + .filter((file) => /\.(bat|cmd|fish|psm1)$/i.test(file)) + .map((file) => relative(root, file)).sort()); +});