diff --git a/CHANGELOG.md b/CHANGELOG.md index 778c660..3a8ab56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/) and semantic versioning. +## [0.1.6] - 2026-09-11 + +### Fixed + +- Reject multiple positional paths with exit code 2 instead of silently scanning only the last target. + ## [0.1.5] - 2026-09-11 ### Fixed diff --git a/README.md b/README.md index 855340a..993c172 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ npx @royalpinto007/skill-audit ./my-skill --format sarif > skill-audit.sarif **Exit codes:** `0` clean (below threshold) · `1` findings at/above `--fail-on` · `2` bad usage. +Pass at most one target path per invocation; extra paths are rejected with exit code `2` before scanning. With no path, the current directory is scanned. + Unknown options are rejected with exit code `2` before scanning, so a misspelled flag cannot silently change the scan. ## In CI (GitHub Action) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4c0e235 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "@royalpinto007/skill-audit", + "version": "0.1.6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@royalpinto007/skill-audit", + "version": "0.1.6", + "license": "MIT", + "bin": { + "skill-audit": "bin/skill-audit.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index 4800394..9106aec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@royalpinto007/skill-audit", - "version": "0.1.5", + "version": "0.1.6", "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/cli.js b/src/cli.js index 70e6d85..76fe837 100644 --- a/src/cli.js +++ b/src/cli.js @@ -41,7 +41,10 @@ function parseArgs(argv) { else if (a === "--fail-on") opts.failOn = argv[++i]; else if (a.startsWith("--format=")) opts.format = a.split("=")[1]; else if (a.startsWith("--fail-on=")) opts.failOn = a.split("=")[1]; - else if (!a.startsWith("-")) opts.path = a; + else if (!a.startsWith("-")) { + if (opts.path !== null) return { error: "skill-audit: only one path is supported; scan each target separately\n" }; + opts.path = a; + } else return { error: `skill-audit: unknown option "${a}"\n` }; } return opts; diff --git a/test/skill-audit.test.js b/test/skill-audit.test.js index 4571336..f4d8b3e 100644 --- a/test/skill-audit.test.js +++ b/test/skill-audit.test.js @@ -36,6 +36,46 @@ test("CLI still accepts supported value option forms", () => { } }); +test("CLI rejects multiple positional paths before scanning", () => { + const cli = join(here, "..", "bin", "skill-audit.js"); + const clean = fixture("clean-skill"); + const malicious = fixture("malicious-skill"); + for (const args of [ + [malicious, clean], + [clean, malicious], + [fixture("missing-skill"), clean], + [clean, clean], + ["", clean], + [malicious, "--format", "json", clean], + ["--format=sarif", malicious, "--fail-on", "high", clean], + ]) { + const result = spawnSync(process.execPath, [cli, ...args], { encoding: "utf8" }); + assert.equal(result.status, 2, `${args.join(" ")}: ${result.stderr}`); + assert.match(result.stderr, /only one.*path/i); + assert.equal(result.stdout, ""); + } +}); + +test("CLI preserves default and single paths with value options", () => { + const cli = join(here, "..", "bin", "skill-audit.js"); + const clean = fixture("clean-skill"); + for (const args of [ + ["--format", "json"], + [clean, "--format", "json", "--fail-on", "high"], + ["--format", "json", clean, "--fail-on", "high"], + ["--format=json", "--fail-on=high", clean], + ]) { + const result = spawnSync(process.execPath, [cli, ...args], { + cwd: args.includes(clean) ? fixture("malicious-skill") : clean, encoding: "utf8", + }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, ""); + const report = JSON.parse(result.stdout); + assert.deepEqual(report.findings, []); + assert.equal(report.filesScanned, scanSkill(clean).files); + } +}); + test("malicious skill triggers the expected high-signal rules", () => { const { findings } = scanSkill(fixture("malicious-skill")); const ids = new Set(findings.map((f) => f.rule));