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
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ on:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ["20", "22"]
node-version: ["22", "24"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm run check:runtime
- run: npm ci
- run: npx tsc --noEmit -p tsconfig.json
- run: npm test
Expand All @@ -29,11 +33,13 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
node-version: "24"
cache: "npm"
- run: npm run check:runtime
- run: npm ci
- run: npm run coverage
- uses: actions/upload-artifact@v7
if: always()
with:
name: coverage-report
path: coverage/
29 changes: 29 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CodeQL

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "23 4 * * 1"

permissions:
contents: read
security-events: write

jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: github/codeql-action/init@v4.37.3
with:
languages: javascript-typescript
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
- run: npm run check:runtime
- run: npm ci
- run: npm run build
- uses: github/codeql-action/analyze@v4.37.3
21 changes: 21 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Dependency Audit

on:
pull_request:

permissions:
contents: read

jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm run check:runtime
- run: npm ci
- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high
38 changes: 0 additions & 38 deletions .github/workflows/publish.yml

This file was deleted.

40 changes: 40 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: OpenSSF Scorecard

on:
branch_protection_rule:
schedule:
- cron: "31 5 * * 1"
push:
branches: [main]

permissions: read-all

jobs:
scorecard:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
security-events: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run OpenSSF Scorecard
uses: ossf/scorecard-action@v2.4.2
with:
results_file: scorecard-results.sarif
results_format: sarif
publish_results: true
- name: Upload Scorecard artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: scorecard-results
path: scorecard-results.sarif
retention-days: 5
- name: Upload Scorecard to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v4.37.3
with:
sarif_file: scorecard-results.sarif
19 changes: 7 additions & 12 deletions .github/workflows/secureai-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,16 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
node-version: "24"
cache: "npm"
- name: Run SecureAI-Scan (non-blocking)
run: |
npx --yes secureai-scan@latest scan . \
--baseline secureai-baseline.json \
--output report.md || true
- run: npm run check:runtime
- run: npm ci
- run: npm run build
- name: Run SecureAI-Scan
run: node dist/index.js scan . --fail-on high --output report.md
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: secureai-scan-report
path: report.md

# Optional strict mode: fail CI when High/Critical findings are present.
# - name: Fail on High/Critical findings
# run: |
# npx --yes secureai-scan@latest scan . --severity high --output report.json
# node -e "const fs=require('node:fs'); const r=JSON.parse(fs.readFileSync('report.json','utf-8')); if ((r.summary.bySeverity.critical + r.summary.bySeverity.high) > 0) process.exit(1)"
3 changes: 2 additions & 1 deletion .secureai-policy.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"$comment": "SecureAI-Scan policy file — commit this to your repo. See: secureai-scan explain <RULE_ID>",
"$skipPathsRationale": "test-fixtures/ is deliberately vulnerable code that the suite requires to fire (see test/corpus.test.js, which scans it directly as its own root); .regression-cache/ holds repos cloned by npm run regression. Neither is this project's source.",
"minSeverity": "medium",
"minConfidence": 0.45,
"failOnSeverity": "high",
"skipPaths": [],
"skipPaths": ["test-fixtures", ".regression-cache"],
"blockedRules": [],
"onlyRules": [],
"requireOutputValidation": true
Expand Down
39 changes: 38 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
# Changelog

## 0.8.0 — 2026-08-05

Audit-driven hardening. Every item below came from reviewing the project the way an external evaluator would: claims checked against code, then the gaps closed.

### Added
- **Public trust controls for a single-maintainer project.** `GOVERNANCE.md` identifies release authority, review expectations, bus-factor limits, and succession behavior. `docs/ReleaseAssurance.md` documents release controls and non-guarantees, while `docs/benchmarks/v0.8.0.json` preserves machine-readable test, coverage, regression, and package evidence.
- **Independent security workflows.** CodeQL, a portable production dependency audit, and OpenSSF Scorecard now run alongside cross-platform CI on Node 22.12+ and 24. Every install job asserts the runtime before `npm ci`, preventing stale Node 20 jobs from falling through to native node-gyp compilation. The audit uses npm's advisory service instead of GitHub Dependency Review, so it works without enabling Dependency Graph. The project's own SecureAI-Scan workflow is blocking at high severity and uploads its report even on failure.
- **Search and evaluation metadata.** The README leads with a pinned install command and measured release evidence; GitHub Pages now exposes canonical metadata, structured application data, `robots.txt`, a sitemap, and `llms.txt` for MCP security, tool-poisoning, prompt-injection, and Agent Skill scanner discovery. `CITATION.cff` gives researchers and ecosystem audits a version-aware citation path.
- **Release integrity is now enforced locally.** `npm run release:check` runs the complete test suite, coverage thresholds, the reviewed real-repository regression gate, and an npm tarball dry run. `prepublishOnly` runs the same gate before a maintainer can publish manually; GitHub Actions never receives npm credentials or publishes packages.
- **The GitHub Action installs an exact scanner version by default.** Its shell inputs are passed through environment variables and Bash arrays instead of expression-expanded command strings, preventing workflow inputs from being reinterpreted as shell syntax.
- **Python is now AST-based.** Every `.py` file is parsed once with `tree-sitter` + `tree-sitter-python`; `src/scanner/python-ast.ts` indexes imports, calls, positional/keyword arguments, assignment targets (identifier, attribute, tuple/list), functions, decorators, scopes, dictionary fields, and strings for all Python rules. The interim lexical `code`/`logical` views and hand-written assignment parser were deleted — structural detection no longer depends on physical-line regex. This closes fake imports/calls in comments and docstrings, multiline calls and keyword arguments, class-handler attribute taint (`self.user_message = request.json[...]`), tuple assignment, and decorated async handlers. Tree-sitter error recovery keeps incomplete files scanable; target code is never imported or executed and no Python interpreter is required.
- **Python AST contract tests** cover node indexing, receiver/call identity, attribute and tuple taint, decorators, multiline keywords, malformed-file recovery, and fake syntax inside comments/docstrings. The existing safe/vulnerable corpus and real-repo regression gate validate the full rule surface.
- **A shared per-file AST index** (`getFileCalls` / `getFileFunctions` / `getCallsWithin` in `src/utils/ast.ts`), replacing the ~20 independent whole-file walks rules used to each perform. `getCallsWithin` slices the pre-order index by compiler span instead of walking a subtree.
- **DEP003 advisory data is now generated from OSV, not hand-typed.** `npm run sync-advisories` ([`scripts/sync-advisories.js`](scripts/sync-advisories.js)) pulls HIGH/CRITICAL advisories for an explicit LLM/MCP/RAG package watchlist and writes `src/scanner/advisories-generated.ts`. The check stays fully offline at scan time — the snapshot is bundled — but it is now refreshable instead of stale-by-construction. **2 → 164 advisories.** Advisories whose affected range can't be parsed into an exact comparison are dropped rather than shipped, since an always-on advisory is a false positive by construction.
- **`npm run regression` is a real gate.** It now writes a structured report per repo, fingerprints every `proven`/`likely` finding as `repo|rule|file` (line-free, so upstream churn isn't noise), and **exits non-zero on anything not in the reviewed baseline** at `test/regression-baseline.json`. `--update-baseline` accepts the current set. Previously this was "run it and read the output with judgment," which is not a gate.
- **Python LLM sinks are resolved through AST constructor bindings.** Variables (and `self.x` attributes) assigned from a known SDK constructor — `gateway = OpenAI()`, `bedrock = boto3.client("bedrock-runtime")` — make invocation-shaped calls on that receiver visible whatever it is named. This is the Python analogue of the TS scanner's import-resolved `resolveLlmSink`.

### Performance
**A `vercel/ai` scan (5,691 files) went from 217s to 62s — 3.5× — with identical findings.** Driven by a CPU profile rather than guesswork; details and the remaining known gaps are in [`docs/Performance.md`](docs/Performance.md), which previously claimed scan times were "in the multi-second range" and was simply wrong.

- The profile attributed **~95s of 150s to AST descendant iteration alone**, more than everything else combined — type resolution barely registered. Cause: every rule ran its own `getDescendantsOfKind`/`getDescendants` per file, so each file's AST was walked ~20 times per scan, and nested functions were re-walked once per enclosing scope on top of that. Rules now share one memoized pre-order walk per file.
- `resolveLlmSink` checks the generation-shaped method name and the file's imports *before* consulting the type checker. Since `resolveIdentifierModule` can only ever report a specifier the file itself imports, a file with no LLM SDK import cannot produce a resolved sink — the type checker never needed asking for the vast majority of files.
- `npm test` dropped from 42s to 16s as a side effect.
- `test/ast-index.test.js` asserts the index and the containment slice against ts-morph's own traversal (exact membership *and* document order). The optimization's failure mode is silence, so it is guarded by a correctness test rather than a flaky wall-clock one.

### Fixed
- **Python DEP003 compared the wrong version.** `readRequirementsCandidates` stripped the comparison operator, so `langchain>=0.1.0` was parsed as an exact pin of `0.1.0` and then tested against advisory ranges as though that were the installed version — producing `proven`-tier findings about a version the repo never declared. Only `==`/`===` are treated as exact pins now; every other specifier keeps its operator and resolves to "unknown".
- **Renaming a Python LLM client silently disabled every rule.** `LLM_CALL_PATTERNS` hardcoded the receiver names `client`, `llm`, `chain`, `model`, `co`, so an app using any other variable name got zero AI-rule coverage with no indication anything was skipped. The legacy name patterns are kept for cross-module clients but every rule relying on them (AI001, AI003, AI004, AI010) is now gated on the file actually importing an LLM SDK — closing the inverse false positive, where a `chain.invoke(...)` in an unrelated ETL file was reported as prompt injection.
- **`test-fixtures/`-style directories weren't recognized as non-production.** `NON_PRODUCTION_SEGMENT` matched suffixed conventions (`ecosystem-tests`) but not prefixed ones, so a real repo laid out with `test-fixtures/`, `example-app/`, or `demo-server/` got undemoted findings — this repo's own self-scan exited 1 with 38 high-severity findings from its own fixtures. Both affix positions are handled now, still per-segment so `attestation`/`protest` are untouched.
- **`isTestFile` in the Python scanner recognized fewer paths than the TypeScript one.** It now delegates to the shared `isTestFilePath` and keeps only the Python-specific additions (`test_*.py`, `*_test.py`, `conftest.py`).

### Changed
- **CVEs and malicious packages get different ambiguity handling in DEP003.** A documented-malicious package still fires when the version can't be resolved — installing a backdoor is unrecoverable. A CVE now fires at `proven` only when the declared version is an exact pin provably inside the affected range; unpinned-but-possibly-affected drops to `heuristic` (`--paranoid`). Applying the malicious-kind rule to a 162-entry CVE snapshot would have put a critical finding on every repo with `langchain>=0.1.0`.
- Multiple advisories on one package now group into a single finding instead of one per CVE.
- The repo's own [`.secureai-policy.json`](.secureai-policy.json) now skips `test-fixtures/` and `.regression-cache/`, so `secureai-scan scan .` on this repo is a real signal instead of a wall of intentionally-vulnerable fixture hits. It reports clean.
- README: dropped the unprovable "first scanner mapped to all three OWASP frameworks" claim, and corrected the advisory-list and regression-benchmark descriptions to match what the code actually does.

## 0.7.0 — 2026-08-01

### Added
Expand All @@ -26,7 +63,7 @@
Evasion-resistant Agent Skill scanning. In July 2026, [*Cloak and Detonate*](https://arxiv.org/abs/2607.02357) (arXiv:2607.02357) showed that nine published skill scanners could be bypassed by >80% (structural obfuscation) and ≥90% (self-extracting packing) using transformations that preserve the payload exactly; separately, Gecko Security demonstrated an exfiltration payload hidden in a `*.test.ts` file that every public scanner skipped. SecureAI-Scan v0.5.0 was vulnerable to all of these. This release closes each published technique, and was additionally validated against two real-world corpora added to `scripts/regression-scan.js`: the canonical [anthropics/skills](https://github.com/anthropics/skills) repo (18 real skill bundles, zero findings — a pure precision check) and [cisco-ai-defense/skill-scanner](https://github.com/cisco-ai-defense/skill-scanner)'s own labeled eval corpus (20 skills under `evals/`, each with an `_expected.json` verdict and a directory literally named `malicious/` or `safe/`) — a rare case where a real-world repo doubles as a recall check, not just a precision one. Result: 6/6 in-scope malicious fixtures correctly flagged, zero findings on any fixture labeled safe.

### Added
- **Python AST migration spike (`spike/python-ast-poc/`)** — not part of the shipped package (`tree-sitter-python`/`web-tree-sitter` are `devDependencies` only). Confirms `web-tree-sitter` + `tree-sitter-python`'s bundled `.wasm` grammar parses real Python with zero native compilation, and ports enough of AI001 to demonstrate a concrete, real gap in the current regex scanner: `self.user_message = request.json[...]` (any class-based handler — Flask `MethodView`, FastAPI DI classes) is completely invisible to `collectRequestTaintedVars`, which only recognizes bare-identifier assignment targets, even at `--paranoid`. The AST-based POC catches it with no special-casing. See `ROADMAP.md` for the full findings and effort estimate.
- **Python AST migration spike** — established the feasibility of Tree-sitter and demonstrated the class-handler attribute-taint gap (`self.user_message = request.json[...]`). The spike was later superseded and removed when the production AST engine shipped; see the Unreleased section.
- **`secureai-scan skill <target>` / `secureai-scan mcp <target>`** — the pre-install wedge: fetch and scan a single Agent Skill or MCP server *before* trusting it, with no clone, no config, and nothing fetched ever executed. `target` accepts a local path, a full git URL, a GitHub `owner/repo` shorthand, or (for `mcp`) a bare npm package name. npm targets are downloaded with `npm pack` — the tarball only, no `install`, no lifecycle scripts; git targets with `git clone --depth 1`. New module: `src/scanner/fetch-target.ts`.
- **MCP server: `scan_untrusted_target` tool** — the same fetch-and-scan capability exposed to Claude itself via the bundled MCP server (`mcp-server/index.js`), so an agent can check a skill or MCP server before recommending or installing it, from inside the conversation. `skills/secureai-scan/SKILL.md` updated to use this flow instead of its previous "clone it first" instructions.
- **Deobfuscation layer (`src/scanner/deobfuscate.ts`)** — content checks now match against normalized *variants* of the text rather than one fixed byte sequence: zero-width/bidi stripping, Unicode homoglyph folding, spliced-string-literal joining (`'cu' + 'rl'`), and intra-word line-break joining. Transforms are applied cumulatively, so a payload cloaked with two techniques at once is still recovered.
Expand Down
19 changes: 19 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
cff-version: 1.2.0
message: "If you use SecureAI-Scan in research or an ecosystem audit, cite the software and the exact version tested."
title: "SecureAI-Scan: evidence-tiered static analysis for LLM, MCP, RAG, and Agent Skill security"
type: software
authors:
- family-names: Kanthed
given-names: Akshay
repository-code: "https://github.com/akanthed/SecureAI-Scan"
url: "https://www.npmjs.com/package/secureai-scan"
license: MIT
abstract: "A local-first static AI security scanner for TypeScript, JavaScript, Python, MCP configurations, and Agent Skill bundles, with source-to-sink evidence, SARIF reporting, and reviewed real-repository regression testing."
keywords:
- AI security
- LLM security
- MCP security
- prompt injection
- Agent Skill security
- RAG poisoning
- static analysis
Loading