diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..fea5c74 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: skillxray + name: 'skillxray (AI skill security scan)' + entry: skillxray + language: python + files: '(^|/)SKILL\.md$' + pass_filenames: true diff --git a/README.md b/README.md index a42ace5..0cd12fc 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,18 @@ It also speaks SARIF, so findings show up in the GitHub Security tab: sarif_file: skillxray.sarif ``` +### Pre-commit + +You can also run skillxray as a pre-commit hook to block dangerous skills from being committed. Add this to your `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/munzzyy/skillxray + rev: v0.2.0 + hooks: + - id: skillxray +``` + ### Output formats - default — colored human report diff --git a/skillxray/cli.py b/skillxray/cli.py index 0b68df9..8886105 100644 --- a/skillxray/cli.py +++ b/skillxray/cli.py @@ -9,7 +9,7 @@ from . import __version__ from .finding import Severity from .report import render_human, render_json, render_sarif -from .scanner import scan_path, scan_git +from .scanner import scan_path, scan_paths, scan_git def build_parser() -> argparse.ArgumentParser: @@ -17,7 +17,7 @@ def build_parser() -> argparse.ArgumentParser: prog="skillxray", description="Security and hygiene scanner for AI agent skills (SKILL.md, plugins, MCP bundles).", ) - p.add_argument("target", nargs="?", default=".", + p.add_argument("target", nargs="*", default=["."], help="path to a skill dir, a SKILL.md, or a directory of skills (default: .)") p.add_argument("--git", metavar="URL", help="clone a git repo (shallow, read-only) and scan it instead of a local path") @@ -52,10 +52,11 @@ def main(argv=None) -> int: if args.git: result = scan_git(args.git, args.ref) else: - if not os.path.exists(args.target): - print(f"skillxray: no such path: {args.target}", file=sys.stderr) - return 2 - result = scan_path(args.target) + for target_path in args.target: + if not os.path.exists(target_path): + print(f"skillxray: no such path: {target_path}", file=sys.stderr) + return 2 + result = scan_paths(args.target) except RuntimeError as e: print(f"skillxray: {e}", file=sys.stderr) return 2 diff --git a/skillxray/scanner.py b/skillxray/scanner.py index 1453d70..bff2542 100644 --- a/skillxray/scanner.py +++ b/skillxray/scanner.py @@ -14,10 +14,15 @@ from .rules.quality import hygiene_checks -def scan_path(path) -> ScanResult: - path = Path(path) - units = discover(path) - result = ScanResult(root=str(path)) +def scan_paths(paths: list[str | Path]) -> ScanResult: + if not paths: + return ScanResult(root=".") + + units = [] + for p in paths: + units.extend(discover(Path(p))) + + result = ScanResult(root=str(paths[0]) if len(paths) == 1 else "[multiple]") result.units = len(units) scanned = 0 hygiene: dict = {} @@ -38,6 +43,10 @@ def scan_path(path) -> ScanResult: return result +def scan_path(path) -> ScanResult: + return scan_paths([path]) + + def scan_git(url: str, ref: str | None = None) -> ScanResult: """Clone a repo shallowly into a temp dir and scan it. Read-only: nothing from the cloned repo is executed, and git hooks are disabled during clone."""