aiscan — AI Security Scanner
Scans prompts, agents, APIs, MCP tools, integrations, dependencies (known CVEs and end-of-life software), and Python code for security vulnerabilities — prompt injection, data leakage, tool abuse, security risks, and TMotions Python coding standards violations.
Built-in rules + AI deep scan (Claude / GPT-4o / Gemini). Runs as a CLI, REST API, Python SDK, or middleware.
Table of contents
- Why aiscan
- Installation
- Quick start
- CLI commands
- Including and skipping files
- Scan types
- Rules reference
- TMotions Python Coding Standards
- CVE and end-of-life scanning
- PDF export
- REST API
- Python SDK
- Middleware
- Decorators
- HTTP client (other languages)
- CI/CD integration
- Generating CI/CD files with aiscan
- AI provider configuration
- Suppressing false positives
- Sample files
- Troubleshooting
Why aiscan
Traditional security tools (SonarQube, Snyk, Semgrep) were built before AI systems existed. They have no rules for prompt injection, agent tool permissions, or MCP tool definitions. aiscan fills that gap — it treats prompts, agent configs, and AI integrations as first-class security artifacts. It also enforces TMotions internal Python coding standards on top of security scanning.
Installation
From source
git clone <your-repo-url>
cd ai-security-scanner
pip install -r requirements.txt
pip install -e .
Verify install
aiscan version
# aiscan 0.2.5
If aiscan command is not found, use python -m aiscan.cli instead everywhere below.
Quick start
# 1. Generate config
aiscan init
# 2. Add your API key to .env
# ANTHROPIC_API_KEY=sk-ant-...
# 3. Scan files
aiscan scan my_prompt.txt
aiscan scan --type api openapi.yaml
aiscan scan --type mcp tool_config.json
aiscan scan --type sca requirements.txt
aiscan scan --type cve package-lock.json # known CVEs + end-of-life in resolved dependencies
aiscan scan --type code my_script.py # TMotions coding standards check
# 4. Scan without AI (static rules only, no API key needed)
aiscan scan --no-ai openapi.yaml
# 5. Export results to PDF
aiscan scan --type api openapi.yaml --output pdf --out report.pdf
# 6. Skip or restrict files
aiscan scan . --skip-files "Bar.cs,config.json" # skip these names anywhere in the project
aiscan scan . --only-files-from changed.txt # scan only the files listed in changed.txt
CLI commands
aiscan scan
Analyze one or more files for AI security threats, code quality issues, and compliance with TMotions Python coding standards.
Usage: aiscan scan [OPTIONS] FILES...
Options:
-t, --type TEXT Scan type: prompt | agent | api | mcp | integration | container | sca | code | cve | all | auto [default: auto]
-o, --output TEXT Output format: console | json | sarif | pdf [default: console]
-O, --out PATH Write output to file instead of stdout
-f, --fail-on TEXT Exit 1 if severity reached: critical | high | medium | none [default: high]
--no-ai Skip AI deep scan — static rules only
-x, --exclude TEXT Filename(s) or glob(s) to skip, anywhere in the tree. Comma-separated or repeated.
-S, --skip-files TEXT Alias of --exclude
--exclude-from PATH Text file listing names/globs to skip, one per line (alias: --skip-files-from)
-F, --only-files TEXT Scan ONLY these files/globs. Repeatable.
--only-files-from PATH Text file listing files/globs to scan, one per line
-L, --log-file PATH Write a summary log with severity counts
-q, --quiet Suppress output except errors
Default exclusions when scanning a directory: hidden files/folders (
.venv,.git,.aiscanner, any path segment starting with.), any file matching*pipeline.yml, any file matching*.log, andreport_generation.py(the script generated byaiscan generate-report-script, so a CI run doesn't scan itself). Use--excludeto add more patterns on top of these, e.g.aiscan scan . --exclude '*.min.js' --exclude 'vendor/*'. To skip specific files by name, or to scan only specific files, see Including and skipping files.
Examples:
# Auto-detect scan type from extension
aiscan scan system_prompt.txt
aiscan scan agent_config.yaml
# Explicit scan type
aiscan scan --type prompt system_prompt.txt
aiscan scan --type agent agent.yaml
aiscan scan --type api openapi.yaml
aiscan scan --type mcp tool.json
aiscan scan --type integration webhook_config.yaml
aiscan scan --type sca requirements.txt
aiscan scan --type code my_script.py
aiscan scan --type cve package-lock.json
# Python files auto-run BOTH prompt scanner AND coding standards scanner
aiscan scan app.py # runs prompt + code automatically
# Scan a whole directory
aiscan scan --type auto ./my-project/
# Run all scanners on the same file
aiscan scan --type all config.yaml
# Output formats
aiscan scan --type api openapi.yaml --output json
aiscan scan --type api openapi.yaml --output pdf --out report.pdf
aiscan scan --type prompt prompts/ --output sarif --out results.sarif
# Skip or restrict files (see "Including and skipping files")
aiscan scan . --skip-files "Bar.cs,config.json"
aiscan scan . --only-files src/Foo.cs --only-files src/Bar.js
# CI mode — exit 1 if any high+ finding
aiscan scan --quiet --fail-on high prompts/ && echo "passed"
Note:
.pyfiles automatically run both the prompt scanner (for injection/leakage patterns) and the TMotions coding standards scanner.
aiscan rules
List all security and coding standards rules.
aiscan rules # list all rules
aiscan rules prompt # filter by scan type
aiscan rules api --severity critical # filter by severity
aiscan rules code # show TMotions Python coding standards rules (PY-*)
aiscan init
Create a .env config file in the current directory.
aiscan init # creates .env for Claude (default)
aiscan init --provider openai # pre-configure for OpenAI
aiscan init --provider vertex # pre-configure for Vertex AI
aiscan init --force # overwrite existing .env
aiscan generate-report-script
Generate a standalone report_generation.py in the current directory. It runs a scan, exports a PDF, and emails it via SMTP — credentials are read from environment variables when present (CI use) and prompted for interactively otherwise (local use). It also re-installs/upgrades ai-security-scanner to the latest version on every run, and always excludes itself from the scan (see the default exclusions note above).
aiscan generate-report-script # creates report_generation.py
aiscan generate-report-script --output my_script.py # custom filename
aiscan generate-report-script --force # overwrite existing file
Recognised environment variables (all required for non-interactive/CI use, except where noted):
| Variable | Purpose |
|---|---|
SMTP_SERVER |
SMTP host |
SMTP_PORT |
SMTP port (default 587) |
SMTP_LOGIN |
SMTP authentication login — not necessarily the same as the From address |
SMTP_PASSWORD |
SMTP authentication password / key |
SMTP_FROM |
From address (optional, defaults to SMTP_LOGIN) |
SMTP_RECIPIENTS |
Comma-separated recipient list |
BRANCH_NAME |
Git branch name, used in the PDF filename (falls back to GITHUB_REF_NAME, then git rev-parse) |
PROJECT_NAME |
Project name shown in the PDF filename and in-report file paths (falls back to the current directory name) |
EXCLUDED_FILES |
Optional. Comma-separated filenames to skip, passed to aiscan scan --skip-files (e.g. Bar.cs,config.json). If unset and a terminal is attached you are prompted (blank is fine); if unset in CI, nothing is excluded |
The generated PDF is named <project_name>_<branch_name>_report_<timestamp>.pdf.
aiscan generate-workflow
Generate a CI/CD workflow file (GitHub Actions .yml by default) into .github/workflows/ that installs aiscan, generates report_generation.py fresh, runs it, and emails the report.
aiscan generate-workflow # creates .github/workflows/security-scan-report.yml
aiscan generate-workflow --output .github/workflows/my-scan.yml # custom path
aiscan generate-workflow --force # overwrite existing file
Add these as repo secrets before running the generated workflow: SMTP_SERVER, SMTP_PORT, SMTP_LOGIN, SMTP_PASSWORD, SMTP_FROM, SMTP_RECIPIENTS.
aiscan serve
Start the REST API server.
aiscan serve # http://localhost:8000
aiscan serve --port 9000 # custom port
aiscan serve --reload # dev mode, auto-restart on code changes
aiscan version
aiscan version
# aiscan 0.2.5
Including and skipping files
By default aiscan scans everything it recognises under the paths you give it. The options below narrow that down — none of them does anything unless you pass it.
Skip files by name
Give aiscan a list of filenames and it finds and skips them anywhere in the project, in any folder. Everything else is scanned normally.
# A comma-separated list in one flag
aiscan scan . --skip-files "Bar.cs,config.json,secrets.env"
# Or repeat the flag
aiscan scan . --skip-files Bar.cs --skip-files config.json
# Or keep the list in a file (one name or pattern per line; blank lines and # comments are ignored)
aiscan scan . --skip-files-from skip-list.txt
--skip-files/-Sis an alias of--exclude/-x, and--skip-files-fromis an alias of--exclude-from. They behave identically — use whichever name reads better.- A plain name such as
Bar.csmatches that exact filename (case-insensitive) in every folder. Glob patterns such as'*.min.js'work too. - The skip list also applies to files passed directly on the command line, not only to files found while walking a directory.
- The default exclusions (hidden folders,
*pipeline.yml,*.log,report_generation.py) always apply on top. - In CI, the generated report script reads the list from the
EXCLUDED_FILESenvironment variable (seeaiscan generate-report-script).
Scan only specific files
aiscan scan . --only-files src/Foo.cs --only-files 'src/*.ts'
aiscan scan . --only-files-from changed.txt
# CI: scan only what changed in a pull request
git diff --name-only origin/main... > changed.txt
aiscan scan . --only-files-from changed.txt
- Entries can be exact paths or glob patterns. Exact paths are resolved from the current directory. In a glob,
*also matches across folders, so'src/*.ts'covers nested folders too ('src/**/*.ts'would miss files directly insrc/). A pattern with a folder in it is matched against the path as it appears under the directory you passed toscan, so run from the project root with.. - If a listed path isn't found among the scanned files, aiscan prints a warning naming it instead of silently scanning fewer files than you asked for.
- Both options combine:
--only-filespicks the set, then--skip-filestrims it.
Scan types
| Type | Detects | File extensions (auto-detect) |
|---|---|---|
prompt |
Prompt injection, PII leakage, jailbreaks, code security issues | .txt .md .py .js .ts .jsx .tsx .go .java .rb .php .cs .cpp .c .rs .swift .kt |
agent |
Wildcard tools, missing human approval, infinite loops, data persistence | .yaml .yml |
api |
Hardcoded keys, wildcard CORS, insecure transport, missing auth | .yaml .yml .json |
mcp |
Wildcard permissions, code execution, missing rate limits, no auth | .json |
integration |
Default webhook secrets, SSL disabled, raw input forwarding, silent errors | .env .toml .ini .cfg .conf .properties |
sca |
Known vulnerable dependency versions via OSV | requirements.txt pyproject.toml setup.py package.json |
cve |
Known CVEs in resolved dependency versions (OSV.dev) and end-of-life runtimes/packages (endoflife.date) | package-lock.json npm-shrinkwrap.json yarn.lock packages.lock.json *.csproj requirements*.txt Pipfile.lock poetry.lock |
code |
TMotions Python coding standards (PY-* rules) | .py only |
Auto-detect behaviour for
.pyfiles: aiscan runs bothpromptandcodescanners simultaneously.
Auto-detect behaviour for lockfiles:
package-lock.json,yarn.lock,packages.lock.json,*.csproj,Pipfile.lockandpoetry.lockgo to thecvescanner only (sopackage-lock.jsonis no longer treated as an MCP file).requirements*.txtrunscvein addition to its existing scan. Lockfiles insidenode_modules/are ignored. See CVE and end-of-life scanning.
Rules reference
Run aiscan rules to see the full live list.
AI security rules
| Category | Rule IDs | Example |
|---|---|---|
| Prompt | PI-001 PI-002 DL-001 DL-002 EX-001 |
ignore previous instructions |
| Agent | AG-001–AG-006 |
allow_all_tools: true |
| API | AK-001 AK-002 CO-001 HT-001 JW-001 AU-001 |
hardcoded api_key |
| MCP | MC-001–MC-006 |
execute_code: true |
| Integration | IN-001–IN-006 |
webhook_secret: changeme |
| Code (security) | CD-001–CD-012 |
eval(), f-string SQL |
| C# | CS-001 CS-002 |
File.ReadAllText(Request.Query[...]) (path traversal); BinaryFormatter or a custom IFormatter / IClientMessageFormatter (insecure deserialization) |
| JavaScript / TypeScript | JS-001 JS-002 |
dangerouslySetInnerHTML / .innerHTML without DOMPurify.sanitize() (XSS); fetch(req.query.url) (SSRF) |
| Secrets | SEC-001–SEC-003 |
connection strings with embedded passwords, .pem private keys, GitHub / Slack / Stripe / SendGrid tokens |
| Dependencies | CVE-… / GHSA-… advisory IDs, SCA-EOL |
vulnerable or end-of-life dependency (see CVE and end-of-life scanning) |
| AI deep scan | AI-SCAN |
Subtle issues from Claude/GPT-4o/Gemini |
Which scan type runs these:
CS-*,JS-*andSEC-*are part of thepromptscan type (SEC-*also run forapi,agent,mcpandintegration)..csfiles auto-detect toprompt..js/.jsx/.ts/.tsxfiles auto-detect tocode, so to applyJS-001/JS-002runaiscan scan --type prompt app.tsx(or--type all)..pemand.configfiles aren't picked up by auto-detect or directory scans — pass the file explicitly with--type integration.
TMotions Python coding standards rules (PY-*)
| Rule ID | Severity | What it checks |
|---|---|---|
PY-010 |
Critical | Hardcoded password in source |
PY-011 |
Critical | SQL injection via string concatenation |
PY-012 |
Critical | Use of eval() |
PY-013 |
Critical | Use of exec() |
PY-021 |
Critical | subprocess with shell=True |
PY-022 |
Critical | Weak hashing — hashlib.md5 |
PY-014 |
High | Bare except: clause |
PY-015 |
High | Generic raise Exception() |
PY-023 |
High | Insecure random (random.randint etc.) |
Run aiscan rules code to see all PY-* rules with full details.
TMotions Python Coding Standards
The code scan type enforces TMotions internal Python coding standards on .py files.
# CLI
aiscan scan --type code my_script.py
# Python files auto-run this alongside the security scan
aiscan scan app.py # runs both prompt + code
# REST API
curl -X POST http://localhost:8000/tm-coding-standards \
-F "file=@my_script.py"
# Via directory scan
aiscan scan --type auto ./src/
REST endpoint (POST /tm-coding-standards) accepts a file upload (not JSON body) and returns:
{
"target": "my_script.py",
"total_findings": 2,
"findings": [
{
"rule_id": "PY-012",
"severity": "critical",
"title": "Use of eval() detected",
"detail": "eval(user_input)",
"file": "my_script.py",
"line": 15,
"fix": "Avoid eval(); use safer alternatives."
}
]
}
Sample file for testing: samples/code/tmcs_bad_code.py
aiscan scan --type code samples/code/tmcs_bad_code.py
CVE and end-of-life scanning
The cve scan type checks the exact versions your project actually resolves against OSV.dev for known vulnerabilities, and checks runtimes and selected packages against endoflife.date for end-of-life status.
aiscan scan --type cve package-lock.json
aiscan scan . # lockfiles are picked up automatically in a directory scan
aiscan scan . --output pdf --out report.pdf
Supported files
| File | Ecosystem | Notes |
|---|---|---|
package-lock.json, npm-shrinkwrap.json |
npm | lockfile versions 1, 2 and 3 |
yarn.lock |
npm | classic and berry |
packages.lock.json |
NuGet | resolved versions — preferred for .NET |
*.csproj |
NuGet | declared PackageReference versions only; also reads <TargetFramework(s)> for .NET end-of-life |
requirements*.txt |
PyPI | exact pins only (name==1.2.3) |
Pipfile.lock, poetry.lock |
PyPI | resolved versions |
What a finding contains
| Field | Value |
|---|---|
| Rule ID | the CVE ID when the advisory has one, otherwise the advisory ID (e.g. GHSA-…) — so each advisory is its own finding |
| Severity | the advisory's own rating when present (CRITICAL / HIGH / MODERATE→medium / LOW), otherwise computed from its CVSS v3 vector; defaults to medium if neither exists |
| Title / detail | package, installed version, advisory IDs and aliases, summary, and a link to osv.dev/vulnerability/<id> |
| Fix | the lowest fixed version above the installed one (e.g. Upgrade lodash to 4.17.21 or later), or a note that no fix is published |
| File / line | the lockfile path; line numbers are given for requirements*.txt and yarn.lock |
Withdrawn advisories are ignored. Findings count towards --fail-on like any other.
End-of-life checks
| Finding | Severity |
|---|---|
| Runtime or package past its end-of-life date | Medium (SCA-EOL) |
| End-of-life within the next 90 days | Info (SCA-EOL) |
- .NET and .NET Framework are checked from
<TargetFramework>/<TargetFrameworks>in.csprojfiles (e.g.net6.0,net48). - Packages are checked only if they appear in the mapping in
aiscan/sca/eol.py(EOL_PRODUCTS). It currently covers@angular/core,react,vue,next,nuxt,electron,bootstrapanddjango. Add entries there for others; unmapped packages are simply not checked.
Network behaviour
- Calls go to
api.osv.devandendoflife.dateusing the Python standard library, so the usualHTTPS_PROXYenvironment variable applies. No AI key is needed and--no-aihas no effect on this scanner. - Rate limits and transient errors (HTTP 429, 5xx, timeouts) are retried up to 5 times with exponential backoff, honouring the server's
Retry-Afterheader. Lookups are batched (up to 1,000 packages per request) and cached for the length of a run. - If OSV.dev can't be reached, the result carries an error — "CVE lookup via OSV.dev failed" — instead of silently reporting no vulnerabilities. The first file waits out the retries (about 35 seconds); remaining files in the same run fail immediately.
- If only an advisory's details can't be fetched, the finding is still reported (medium severity) with a link to the advisory.
- End-of-life lookup problems are logged as warnings and don't fail the scan.
Limitations
.csprojgives declared versions, not resolved ones; usepackages.lock.jsonwhen you have it. Floating versions (1.*), MSBuild properties ($(Version)) and central package management (Directory.Packages.props) are skipped.requirements*.txtranges (>=2.0) can't be matched to a CVE and are skipped.package.jsonhas version ranges only, so it is not CVE-scanned — its lockfile is.- CVE scanning is available from the CLI. It is not yet exposed through the REST API or the Python SDK.
PDF export
Export any scan result directly to a PDF report from the CLI.
# Save to default name (aiscan-report.pdf)
aiscan scan --type prompt prompts/ --output pdf
# Save to custom path
aiscan scan --type api openapi.yaml --output pdf --out reports/scan.pdf
# Scan everything, export PDF
aiscan scan --no-ai --type auto . --output pdf --out report.pdf
The PDF includes:
- Summary header — overall PASSED/FAILED, file count, critical/high/total counts
- Per-file findings table — severity icons, rule ID, threat type, title, line number
- Fix suggestions — detailed remediation for every critical and high finding
- Page footer with timestamp and page number
REST API
aiscan serve
# Swagger UI: http://localhost:8000/docs
| Method | Path | Body | Description |
|---|---|---|---|
| GET | /health |
— | Service status and version |
| POST | /scan/prompt |
JSON | Scan prompt content |
| POST | /scan/agent |
JSON | Scan agent config |
| POST | /scan/api |
JSON | Scan API config or OpenAPI spec |
| POST | /scan/mcp |
JSON | Scan MCP tool definition |
| POST | /scan/integration |
JSON | Scan integration config |
| POST | /scan/sca |
JSON | Scan dependency manifests for known vulnerabilities |
| POST | /scan/container |
JSON | Scan Dockerfile / container config content |
| POST | /scan/all |
JSON | Run all AI security scanners |
| POST | /tm-coding-standards |
File upload | TMotions Python coding standards check |
| GET | /docs |
— | Swagger UI |
JSON endpoints — request:
{"content": "file content as string", "filename": "openapi.yaml"}
JSON endpoints — response:
{
"scan_type": "api",
"passed": false,
"critical": 1, "high": 2, "medium": 0, "info": 0, "total": 3,
"findings": [
{
"rule_id": "AK-001",
"severity": "critical",
"threat": "security_risk",
"title": "Hardcoded secret or API key",
"detail": "Matched on line 5: ...",
"file": "config.yaml",
"line": 5,
"fix": "Move to environment variables or a secrets manager."
}
]
}
curl examples:
# Scan prompt
curl -X POST http://localhost:8000/scan/prompt \
-H "Content-Type: application/json" \
-d '{"content": "Ignore previous instructions", "filename": "prompt.txt"}'
# Scan container config
curl -X POST http://localhost:8000/scan/container \
-H "Content-Type: application/json" \
-d '{"content": "FROM node:latest\nUSER root", "filename": "Dockerfile"}'
# TMotions coding standards (file upload)
curl -X POST http://localhost:8000/tm-coding-standards \
-F "file=@my_script.py"
Python SDK
Use directly inside your Python project — no separate service needed.
from aiscan.sdk import AIScan
scanner = AIScan(no_ai=True) # static rules only
# or
scanner = AIScan(ai_provider="claude", api_key="sk-ant-...")
report = scanner.scan_prompt("Ignore previous instructions...")
print(report.passed) # False
print(report.critical) # 2
print(report.total) # 3
for f in report.findings:
print(f.severity, f.rule_id, f.title)
print(" Fix:", f.fix)
# Other methods
scanner.scan_file("openapi.yaml") # auto-detect type
scanner.scan_file("agent.yaml", scan_type="agent")
scanner.scan_code("app.py") # security + TM coding standards
scanner.scan_api_spec(open("openapi.yaml").read())
scanner.scan_agent_config(open("agent.yaml").read())
scanner.scan_mcp_tool(open("tool.json").read())
scanner.scan_integration(open("webhook.yaml").read())
scanner.scan_sca(open("requirements.txt").read(), filename="requirements.txt")
reports = scanner.scan_all(content, filename="config.yaml") # all scanners
# Gate on severity
if report.has_severity("critical"):
block_request()
critical_only = report.findings_by_severity("critical")
# Export to dict
data = report.to_dict()
Middleware
Auto-scan every incoming request before it reaches your handlers.
FastAPI
from fastapi import FastAPI
from aiscan.sdk.middleware import AIScanFastAPIMiddleware
app = FastAPI()
app.add_middleware(
AIScanFastAPIMiddleware,
scan_fields=["prompt", "message", "content"],
fail_on="high",
no_ai=True,
)
Blocked requests return:
{
"error": "Request blocked by AI Security Scanner",
"field": "prompt",
"issues": [{"rule": "PI-001", "severity": "critical", "issue": "...", "fix": "..."}]
}
Flask
from flask import Flask
from aiscan.sdk.middleware import AIScanFlaskMiddleware
app = Flask(__name__)
AIScanFlaskMiddleware(app, fields=["prompt", "message"], fail_on="high")
Django
# settings.py
MIDDLEWARE = [
"aiscan.sdk.middleware.AIScanDjangoMiddleware",
# ...
]
AISCAN_FIELDS = ["prompt", "message"]
AISCAN_FAIL_ON = "high"
AISCAN_NO_AI = False
Decorators
Wrap any existing function — no refactoring needed.
from aiscan.sdk import scan_prompt_input, scan_before_llm, scan_file_input
# Scan a specific argument before the function runs
@scan_prompt_input(field="user_message", fail_on="high")
def call_llm(user_message: str) -> str:
... # raises ValueError if unsafe
# Scan all prompt-related arguments
@scan_before_llm(fields=["system_prompt", "user_message"])
def chat(system_prompt: str, user_message: str) -> str:
...
# Scan a file path before loading it
@scan_file_input(field="config_path", fail_on="critical")
def load_agent_config(config_path: str) -> dict:
...
HTTP client (other languages)
Run aiscan serve as a separate service, call it from any language.
Python:
from aiscan.sdk import AIScanClient
client = AIScanClient("http://localhost:8000")
result = client.scan_prompt(user_input)
is_safe = client.is_safe(user_input, fail_on="high")
result = client.scan_file("openapi.yaml")
Node.js:
const res = await fetch("http://localhost:8000/scan/prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: userInput, filename: "prompt.txt" }),
});
const { passed, findings } = await res.json();
Go:
resp, _ := http.Post("http://localhost:8000/scan/prompt",
"application/json", bytes.NewBuffer(body))
TMotions coding standards from any language:
# multipart file upload — works from any HTTP client
curl -X POST http://localhost:8000/tm-coding-standards -F "file=@script.py"
CI/CD integration
GitHub Actions
name: AI Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -e .
- name: Scan prompts and configs
run: aiscan scan --type prompt prompts/ --fail-on high
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Scan Python code (TMotions standards)
run: aiscan scan --type code src/ --fail-on high
- name: Export SARIF
run: aiscan scan --output sarif --out results.sarif .
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: results.sarif }
Azure Pipelines
# AI-Security-Scan-Pipeline.yml
trigger:
branches:
include:
- '*'
pool:
name: 'LinuxAgentPool'
variables:
python_cmd: 'python3.12'
python_env_name: '.aiscanner'
project_name: $(Build.Repository.Name)
SMTP_SERVER: 'smtp-relay.brevo.com'
SMTP_PORT: '587'
SMTP_FROM: 'your-verified-sender@example.com'
SMTP_RECIPIENTS: 'team@example.com'
steps:
- checkout: self
displayName: 'Checkout Repository'
- script: |
set -e
rm -rf "$(python_env_name)"
$(python_cmd) -m venv "$(python_env_name)"
"$(python_env_name)/bin/python" -m pip install --upgrade pip
displayName: 'Create Python Virtual Environment'
- script: |
set -e
"$(python_env_name)/bin/python" -m pip install --upgrade ai-security-scanner
displayName: 'Install AI Security Scanner'
- script: |
set -e
export PATH="$(pwd)/$(python_env_name)/bin:$PATH"
aiscan generate-report-script --force
displayName: 'Generate report_generation.py'
- script: |
set -e
set -o pipefail
export PATH="$(pwd)/$(python_env_name)/bin:$PATH"
"$(python_env_name)/bin/python" -u report_generation.py 2>&1 | tee ai_security_scan.log
displayName: 'Run AI Security Scan and Send Email'
env:
SMTP_SERVER: $(SMTP_SERVER)
SMTP_PORT: $(SMTP_PORT)
SMTP_LOGIN: $(SMTP_LOGIN)
SMTP_PASSWORD: $(SMTP_PASSWORD)
SMTP_FROM: $(SMTP_FROM)
SMTP_RECIPIENTS: $(SMTP_RECIPIENTS)
BRANCH_NAME: $(Build.SourceBranchName)
PROJECT_NAME: $(project_name)
- script: |
set -e
rm -rf "$(python_env_name)"
rm -f *_report_*.pdf
displayName: 'Cleanup'
condition: always()
SMTP_LOGIN and SMTP_PASSWORD are set as secret pipeline variables (not shown in plaintext YAML) under Pipeline → Edit → Variables.
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: aiscan
name: AI Security Scanner
entry: aiscan scan --no-ai --fail-on critical
language: system
files: \.(txt|yaml|yml|json|py|js|ts)$
Makefile
make install # pip install -e ".[dev]"
make test # run pytest
make lint # ruff check
make scan-samples # scan all bad sample files
make rules # list all rules
make version # print version
Exit codes
| Code | Meaning |
|---|---|
0 |
Passed — no findings at or above --fail-on threshold |
1 |
Failed — findings found at or above threshold |
2 |
Tool error — bad arguments, missing file |
Generating CI/CD files with aiscan
Rather than hand-writing the report/email script and pipeline config, aiscan can generate both for you from bundled templates:
aiscan generate-report-script # → report_generation.py
aiscan generate-workflow # → .github/workflows/security-scan-report.yml
Typical setup in a CI pipeline: install aiscan, run aiscan generate-report-script --force to always get the current template (rather than committing a copy that can drift out of date), then run the generated script. This is the pattern used in both the GitHub Actions and Azure Pipelines examples above.
See aiscan generate-report-script and aiscan generate-workflow under CLI commands for the full option list and recognised environment variables.
AI provider configuration
Run aiscan init to create .env, then edit it:
# Claude (default)
AI_PROVIDER=claude
ANTHROPIC_API_KEY=sk-ant-...
SCANNER_MODEL=claude-sonnet-4-20250514
# OpenAI
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o
# Vertex AI (Gemini)
AI_PROVIDER=vertex
VERTEX_PROJECT=your-gcp-project-id
VERTEX_LOCATION=us-central1
VERTEX_MODEL=gemini-2.5-flash-001
# Shared
MAX_TOKENS=2000
LOG_LEVEL=INFO
If the primary provider fails (rate limit, auth error), aiscan automatically retries with the next available one. Use --no-ai to skip AI scanning entirely — static rules and TMotions coding standards still run.
Suppressing false positives
Add # noscan (Python/YAML) or // noscan (JS/TS) to the end of any line to skip it:
result = scanner.scan_prompt("test example string") # noscan
except Exception: # noscan — intentional catch-all in middleware
Comments are ignored
Text inside code comments is never scanned, so commented-out secrets, example emails and old code don't raise findings.
| Files | Comment styles ignored |
|---|---|
.py .rb .yaml .yml .env .ini .cfg .conf .properties |
# … |
.js .jsx .ts .tsx .java .kt .cs .go .c .cpp .rs .swift |
// … and /* … */ (may span lines) |
.php |
// …, # … and /* … */ |
.tf |
# …, // … and /* … */ |
- Only the comment part of a line is ignored. In
const key = "…"; // TODO, the code before//is still scanned, and the report shows the whole original line. - A
//or#inside a quoted string (such as"https://example.com") is not a comment, and neither is Python's//floor-division operator. - Line numbers in findings are unchanged.
- Every other file type (
.txt,.md,.json,.j2, Dockerfiles, …) is scanned as-is, so prompt text is never altered. - This applies to the static rules. The AI deep scan still receives the full file, comments included.
- Known gap: Python triple-quoted docstrings are treated as code, not as comments.
Names that only look like secrets
The hardcoded-credential rules (AK-001, CD-001, TF-001, KT-001, JV-001, PHP-006) flag a value only if it looks like a real secret. When the quoted value itself contains a label word such as token, secret or password, aiscan treats it as a name or placeholder rather than a credential:
public static final String COLUMNUSERTOKEN = "USERTOKEN"; // not flagged — a column name
static let deviceToken = "deviceToken" // not flagged — a label
String apiToken = "aF39xQ72zM01Plq"; // flagged — looks like a real secret
The trade-off: a genuine random secret that happens to contain one of those words would be missed.
Sample files
samples/
├── prompts/
│ ├── bad_system_prompt.txt — jailbreak + PII (9 findings)
│ ├── bad_user_template.txt — chat tokens + exfil URL (6 findings)
│ ├── bad_agent_instructions.txt — role override (4 findings)
│ ├── clean_customer_support.txt — safe (0 findings)
│ └── clean_coding_assistant.txt — safe (0 findings)
├── agents/
│ ├── bad_research_agent.yaml — wildcard tools + no HITL (8 findings)
│ ├── bad_finance_agent.yaml — auto-approves transactions (6 findings)
│ ├── clean_support_agent.yaml — safe (0 findings)
│ └── clean_data_agent.yaml — safe (0 findings)
├── apis/
│ ├── bad_config.yaml — 5 hardcoded keys + wildcard CORS (10 findings)
│ ├── bad_openapi.yaml — unauthenticated endpoints (3 findings)
│ ├── clean_config.yaml — safe (0 findings)
│ └── clean_openapi.yaml — safe (0 findings)
├── mcp/
│ ├── bad_code_executor.json — shell access + wildcard perms (10+ findings)
│ ├── bad_database_tool.json — no auth + raw SQL (5 findings)
│ ├── bad_web_search.json — injection in description (4 findings)
│ ├── clean_product_search.json — safe (0 findings)
│ └── clean_calendar_tool.json — safe (0 findings)
├── integrations/
│ ├── bad_slack_integration.yaml — default secret + SSL off (4 findings)
│ ├── bad_email_integration.yaml — multiple issues (5 findings)
│ └── clean_slack_integration.yaml — safe (0 findings)
└── code/
├── bad_app.py — hardcoded keys + SQL injection + eval (17 findings)
├── bad_app.js — hardcoded keys + console.log (4 findings)
├── bad_api.ts — SSL disabled + credentials (3 findings)
├── bad_settings.py — multiple hardcoded secrets (6 findings)
├── tmcs_bad_code.py — TMotions coding standards violations
└── clean_app.py — safe (0 findings)
# Test all bad samples
aiscan scan --no-ai samples/prompts/bad_system_prompt.txt
aiscan scan --no-ai samples/agents/bad_research_agent.yaml
aiscan scan --no-ai samples/apis/bad_config.yaml
aiscan scan --no-ai samples/mcp/bad_code_executor.json
aiscan scan --no-ai samples/integrations/bad_slack_integration.yaml
aiscan scan --no-ai samples/code/bad_app.py
aiscan scan --type code samples/code/tmcs_bad_code.py
Troubleshooting
aiscan: command not found
python -m aiscan.cli --help
pip install -e .
ImportError: cannot import name 'scan_tm_code'
The function in tm_coding_standards_scanner.py was named differently. Fix by adding an alias at the bottom of the file:
scan_tm_code = tm_coding_standards_scanner
__all__ = ["scan_tm_code", "tm_coding_standards_scanner"]
AI scan not running
aiscan scan --no-ai openapi.yaml # confirms static rules work
# check .env has a valid API key and AI_PROVIDER is set
False positives
aiscan scan --no-ai file.txt # isolate static rule hits
aiscan rules # see which pattern triggered
# add # noscan to suppress a specific line
BackendUnavailable: Cannot import setuptools.backends.legacy
pip install --upgrade setuptools wheel
pip install -e .
CVE lookup via OSV.dev failed
aiscan couldn't reach api.osv.dev after its retries (no network, a blocking firewall, or a proxy that isn't configured). Set HTTPS_PROXY if you're behind one. The first file takes about 35 seconds to give up; later files in the same run fail immediately. The scan's other results are unaffected.
A file passed to --only-files wasn't scanned
aiscan prints a warning naming any listed path it never found. Exact paths are resolved from the current directory, so check for a typo and run from the project root.
Commented-out code is still flagged
Comment handling depends on the file extension (see Comments are ignored). .txt, .md and .json files are scanned as-is, and Python docstrings count as code.
Version
0.2.5
License
MIT
Metadata
Release files for ai-security-scanner 0.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ai_security_scanner-0.2.5.tar.gz | 118.3 kB | Without provenance | Details for ai_security_scanner-0.2.5.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ai_security_scanner-0.2.5-py3-none-any.whl | Python 3 | none | any | Details for ai_security_scanner-0.2.5-py3-none-any.whl |
Total release size: 217.1 kB
Release files / ai_security_scanner-0.2.5.tar.gz
| Download URL | ai_security_scanner-0.2.5.tar.gz |
|---|---|
| Size | 118.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7a540583f6b61baaa7ce06fd4400d6adde773ffa25c36c2ad8a4090ac34c7e74
|
|
BLAKE2b-256 checksum How to use checksums |
6736c80f983a6f939f0eb97c7d0973ad98925ac2eac6b2ad4c563654db42ff3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / ai_security_scanner-0.2.5-py3-none-any.whl
| Download URL | ai_security_scanner-0.2.5-py3-none-any.whl |
|---|---|
| Size | 98.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a2c598242698a3bce960c2891018fe28a8d87a7019b3219506538fbf4e64862f
|
|
BLAKE2b-256 checksum How to use checksums |
dace7ef24a92122aabf25ba79dd1219366b3d1bda16b71570e006f3ce3d141b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for ai-security-scanner 0.2.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.