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
45 changes: 45 additions & 0 deletions .github/workflows/ai-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,51 @@ jobs:
echo "OK — Catalog directory not yet populated (pre-development readiness phase)."
fi

- name: Audit Catalog Python Scripts via AST Security Gate
run: |
python3 - <<'PY'
import ast, pathlib, sys

FORBIDDEN_IMPORTS = {
"urllib", "urllib2", "urllib3", "http", "httplib", "socket",
"socketserver", "requests", "httpx", "aiohttp", "ftplib",
"smtplib", "telnetlib", "xmlrpc", "webbrowser", "paramiko",
"websocket", "websockets", "pickle", "cPickle", "marshal",
"shelve", "ctypes", "cffi", "pty", "commands",
}
FORBIDDEN_CALLS = {"eval", "exec", "compile", "__import__"}
FORBIDDEN_OS_ATTRS = {"system", "popen", "spawnl", "spawnle", "spawnlp", "spawnlpe", "spawnv", "spawnve", "spawnvp", "spawnvpe", "execl", "execle", "execlp", "execlpe", "execv", "execve", "execvp", "execvpe"}

errors = []
for py_file in sorted(pathlib.Path("catalog").rglob("*.py")):
source = py_file.read_text(encoding="utf-8")
tree = ast.parse(source, filename=py_file.as_posix())
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
root = alias.name.split(".")[0]
if root in FORBIDDEN_IMPORTS:
errors.append(f"{py_file.as_posix()}:{node.lineno}: forbidden import '{root}'")
elif isinstance(node, ast.ImportFrom) and node.module:
root = node.module.split(".")[0]
if root in FORBIDDEN_IMPORTS:
errors.append(f"{py_file.as_posix()}:{node.lineno}: forbidden from-import '{root}'")
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_CALLS:
errors.append(f"{py_file.as_posix()}:{node.lineno}: forbidden dynamic call '{node.func.id}()'")
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "os" and node.func.attr in FORBIDDEN_OS_ATTRS:
errors.append(f"{py_file.as_posix()}:{node.lineno}: forbidden shell call 'os.{node.func.attr}()'")
for kw in node.keywords:
if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
errors.append(f"{py_file.as_posix()}:{node.lineno}: forbidden 'shell=True' in subprocess call")

if errors:
for err in errors:
print(f"::error::{err}")
sys.exit(1)
print("OK — All catalog Python scripts passed AST security audit.")
PY

npx-engine-qa:
name: Node.js ${{ matrix.node-version }} NPX Engine & TypeScript QA Gate
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Catalog Script AST & Lexical Security Scanner (`#190`):** Added [`src/auditor/security.ts`](./src/auditor/security.ts) (`auditSkillSecurity`, `auditCatalogSecurity`) and a Python `ast` security gate in [`.github/workflows/ai-ci.yml`](./.github/workflows/ai-ci.yml) to block outbound network modules (`urllib`, `http`, `socket`, `requests`, `curl`, `wget`), dynamic execution/deserialization (`eval`, `exec`, `compile`, `__import__`, `pickle`, `marshal`, `ctypes`), and unsafe shell execution (`os.system`, `os.popen`, `shell=True`) in catalog Skill scripts.
- **Global User Home Directory Scope (`--global` / `-g`) (`#176`):** Added `isGlobalFlag()`, `resolveGlobalHomeDir()`, and `resolveTargetRepoPath()` in [`src/cli.ts`](./src/cli.ts), updated [`src/ui/help.ts`](./src/ui/help.ts), and surfaced `[Global: ~]` scope indicators in [`src/sync/adapters.ts`](./src/sync/adapters.ts) so `npx skillops-cli add -g` and `npx skillops-cli remove -g` install and remove Skills and Subagents directly in `os.homedir()` (`~/.claude/`, `~/.cursor/`, `~/.gemini/`, etc.).
- **Full Skill Folder & Companion File Bundling Across All 28 Targets (`#175`):** Extended `parseSkillDirectory()` in [`src/auditor/parser.ts`](./src/auditor/parser.ts) with recursive `allFiles` discovery and updated `renderSkillFilesForRuntime()` in [`src/sync/adapters.ts`](./src/sync/adapters.ts) so installing a Skill copies all companion files and nested subdirectories (`.py`, `.sh`, `.mjs`, `.json`, `.yaml`, `.txt`, `scripts/`, `references/`, `assets/`, `evals/`) across both `progressive-skill-dir` and rule-based AI IDEs with localized relative paths and complete cleanup on `remove`.
- **Step 1 TUI Auto-Detection & Pre-Selection of Existing Project AI IDEs (`#174`):** Updated `buildInteractiveTargetOptions(repoPath)` and `runCli()` in [`src/cli.ts`](./src/cli.ts) plus badge styling in [`src/ui/prompt.ts`](./src/ui/prompt.ts) so existing repository AI IDE directories are placed at the top of Step 1 with `[detected]` indicators and `initialSelected: true`, and non-interactive `add` defaults to detected project harnesses before falling back to `CORE_RUNTIME_TARGETS`.
Expand Down
7 changes: 7 additions & 0 deletions src/auditor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ export {
parseSkillDirectory,
parseSubagentFile,
} from "./parser.js";
export {
auditCatalogSecurity,
auditSkillSecurity,
type SkillSecurityAuditReport,
type SkillSecurityFinding,
type SkillSecurityRuleId,
} from "./security.js";
Loading
Loading