aigis-sast 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aigis_sast-0.2.0/LICENSE +21 -0
- aigis_sast-0.2.0/PKG-INFO +267 -0
- aigis_sast-0.2.0/README.md +242 -0
- aigis_sast-0.2.0/pyproject.toml +49 -0
- aigis_sast-0.2.0/setup.cfg +4 -0
- aigis_sast-0.2.0/src/aigis/__init__.py +1 -0
- aigis_sast-0.2.0/src/aigis/__main__.py +3 -0
- aigis_sast-0.2.0/src/aigis/ai.py +69 -0
- aigis_sast-0.2.0/src/aigis/badge.py +38 -0
- aigis_sast-0.2.0/src/aigis/checks/__init__.py +0 -0
- aigis_sast-0.2.0/src/aigis/checks/common.py +28 -0
- aigis_sast-0.2.0/src/aigis/checks/python_ast.py +288 -0
- aigis_sast-0.2.0/src/aigis/checks/text.py +166 -0
- aigis_sast-0.2.0/src/aigis/cli.py +213 -0
- aigis_sast-0.2.0/src/aigis/fix.py +317 -0
- aigis_sast-0.2.0/src/aigis/history.py +136 -0
- aigis_sast-0.2.0/src/aigis/models.py +50 -0
- aigis_sast-0.2.0/src/aigis/report.py +242 -0
- aigis_sast-0.2.0/src/aigis/rules.py +214 -0
- aigis_sast-0.2.0/src/aigis/scanner.py +202 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/PKG-INFO +267 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/SOURCES.txt +26 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/dependency_links.txt +1 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/entry_points.txt +2 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/requires.txt +4 -0
- aigis_sast-0.2.0/src/aigis_sast.egg-info/top_level.txt +1 -0
- aigis_sast-0.2.0/tests/test_scanner.py +175 -0
- aigis_sast-0.2.0/tests/test_tools.py +108 -0
aigis_sast-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jasperBLCK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aigis-sast
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AigisSAST: security scanner for AI-generated code. Finds what vibe coding leaves behind and explains how to fix it
|
|
5
|
+
Author: jasperBLCK
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jasperBLCK/AigisSAST
|
|
8
|
+
Project-URL: Source, https://github.com/jasperBLCK/AigisSAST
|
|
9
|
+
Project-URL: Issues, https://github.com/jasperBLCK/AigisSAST/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/jasperBLCK/AigisSAST/releases
|
|
11
|
+
Keywords: security,sast,linter,secrets,ai,vibe-coding,fastapi
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
|
|
28
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/header.svg" width="100%" alt="AigisSAST"/>
|
|
29
|
+
|
|
30
|
+
<h3>ИИ пишет код быстро. AigisSAST проверяет, что он не оставил дыр.</h3>
|
|
31
|
+
|
|
32
|
+
<p><i>Security scanner for AI-generated code: finds what vibe coding leaves behind and explains how to fix it.</i></p>
|
|
33
|
+
|
|
34
|
+
<a href="https://github.com/jasperBLCK/AigisSAST/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/jasperBLCK/AigisSAST/ci.yml?branch=main&style=for-the-badge&label=CI&labelColor=000000&color=333333&logo=githubactions&logoColor=white"/></a> <img src="https://img.shields.io/badge/python-3.9+-000000?style=for-the-badge&logo=python&logoColor=white"/> <img src="https://img.shields.io/badge/dependencies-0-000000?style=for-the-badge"/> <img src="https://img.shields.io/badge/SARIF-2.1.0-000000?style=for-the-badge&logo=github&logoColor=white"/> <img src="https://img.shields.io/badge/aigis-A%20100%2F100-000000?style=for-the-badge"/> <img src="https://img.shields.io/badge/license-MIT-000000?style=for-the-badge"/>
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/divider.svg" width="100%"/>
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/demo.svg" width="92%" alt="aigis scan demo"/>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
## Зачем
|
|
45
|
+
|
|
46
|
+
Cursor, Copilot и ChatGPT собирают рабочий бэкенд за вечер. Но по пути они почти всегда оставляют одно и то же:
|
|
47
|
+
токен бота прямо в коде, `f"SELECT ... {user}"`, `allow_origins=["*"]`, `debug=True`, открытый наружу Postgres.
|
|
48
|
+
Код работает, тесты зелёные, а сервис уже можно взломать.
|
|
49
|
+
|
|
50
|
+
AigisSAST ловит именно эти ошибки и объясняет каждую **простым языком**: чем она опасна и как исправить, с готовым кодом.
|
|
51
|
+
Без регистрации и облака, ничего не отправляет наружу (если сам не включишь `--ai`).
|
|
52
|
+
|
|
53
|
+
- **Ноль зависимостей.** Только стандартная библиотека Python, ставится за секунду.
|
|
54
|
+
- **AST, а не grep.** Python-код разбирается синтаксическим деревом, поэтому меньше ложных срабатываний.
|
|
55
|
+
- **Секреты маскируются** в отчёте: `sk-p************`, сам ключ в логах CI не светится.
|
|
56
|
+
- **Не только ругается, но и чинит.** `aigis fix` сам выносит ключи в `.env` и правит безопасные случаи.
|
|
57
|
+
- **Видит прошлое.** `aigis history` находит ключи, которые «удалили», но они остались в истории git.
|
|
58
|
+
- **CI-ready.** Exit-коды, SARIF для GitHub Code Scanning, JSON, Markdown, GitHub Action и pre-commit hook.
|
|
59
|
+
|
|
60
|
+
## Быстрый старт
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install git+https://github.com/jasperBLCK/AigisSAST
|
|
64
|
+
aigis scan . # найти проблемы
|
|
65
|
+
aigis fix --dry-run # посмотреть, что исправится автоматически
|
|
66
|
+
aigis fix # исправить
|
|
67
|
+
aigis history # проверить всю историю git на утёкшие ключи
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Попробуй на заведомо дырявом примере:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/jasperBLCK/AigisSAST && cd AigisSAST
|
|
74
|
+
pip install . && aigis scan examples
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Что ловит
|
|
78
|
+
|
|
79
|
+
| ID | Уровень | Проблема | Где |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `AIG001` | CRITICAL | Токены и API-ключи: OpenAI, Anthropic, Telegram, GitHub, AWS, Stripe, Slack, Google, Yandex Cloud | везде |
|
|
82
|
+
| `AIG004` | CRITICAL | Приватные ключи (RSA / EC / OpenSSH / PGP) | везде |
|
|
83
|
+
| `AIG002` | HIGH | Захардкоженные пароли и секреты | Python, JS/TS, YAML, Dockerfile, compose, .ini/.toml |
|
|
84
|
+
| `AIG003` | HIGH | Строка подключения к БД с паролем | везде, кроме документации |
|
|
85
|
+
| `AIG005` | HIGH | `.env` попадает в репозиторий | git / .gitignore |
|
|
86
|
+
| `AIG010` | HIGH | SQL-инъекция: f-string, `%`, `+`, `.format()` в `execute()` / `text()` | Python |
|
|
87
|
+
| `AIG011` | HIGH | Shell-инъекция: `shell=True`, `os.system` с переменной | Python |
|
|
88
|
+
| `AIG012` | HIGH | `eval` / `exec` над динамическими данными | Python |
|
|
89
|
+
| `AIG013` | HIGH | `pickle.loads`, `yaml.load` без SafeLoader | Python |
|
|
90
|
+
| `AIG017` | HIGH | JWT без проверки подписи, `algorithms=["none"]` | Python |
|
|
91
|
+
| `AIG030` | HIGH | `eval` / `new Function` | JS / TS |
|
|
92
|
+
| `AIG014` | MEDIUM | `debug=True`, `DEBUG = True` | Python |
|
|
93
|
+
| `AIG015` | MEDIUM → HIGH | CORS `*` (HIGH вместе с `allow_credentials=True`) | FastAPI, Express |
|
|
94
|
+
| `AIG016` | MEDIUM | `verify=False` в requests / httpx | Python |
|
|
95
|
+
| `AIG019` | MEDIUM | Токены и OTP-коды через `random` вместо `secrets` | Python |
|
|
96
|
+
| `AIG031` | MEDIUM | XSS: `innerHTML`, `dangerouslySetInnerHTML`, `v-html` | JS / TS / Vue |
|
|
97
|
+
| `AIG032` | MEDIUM | Секреты в `NEXT_PUBLIC_*` / `VITE_*` / `REACT_APP_*` | фронтенд |
|
|
98
|
+
| `AIG042` | MEDIUM | Порт БД/Redis опубликован на `0.0.0.0` | docker-compose |
|
|
99
|
+
| `AIG018` | LOW | MD5 / SHA-1 | Python |
|
|
100
|
+
| `AIG021` | LOW | POST/PUT/PATCH/DELETE без `Depends(...)` авторизации | FastAPI |
|
|
101
|
+
| `AIG040` | LOW | Контейнер работает от root | Dockerfile |
|
|
102
|
+
|
|
103
|
+
Подробно про любое правило: `aigis explain AIG010`, полный список: `aigis rules`.
|
|
104
|
+
|
|
105
|
+
## Использование
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
aigis scan . # текстовый отчёт с объяснениями
|
|
109
|
+
aigis scan src api --quiet # несколько путей, без блоков «чем опасно / как исправить»
|
|
110
|
+
aigis scan . --min-severity medium # скрыть LOW
|
|
111
|
+
aigis scan . --fail-on critical # падать только на критичных
|
|
112
|
+
aigis scan . -f sarif -o aigis.sarif # для GitHub Code Scanning
|
|
113
|
+
aigis scan . -f json | jq '.findings[]' # для своих скриптов
|
|
114
|
+
aigis scan . -f markdown > SECURITY.md # отчёт для PR / заказчика
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Exit code | Значение |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `0` | нет находок уровня `--fail-on` (по умолчанию `high`) и выше |
|
|
120
|
+
| `1` | найдены проблемы уровня `--fail-on` и выше |
|
|
121
|
+
| `2` | ошибка запуска (неверный путь, неизвестное правило) |
|
|
122
|
+
|
|
123
|
+
В конце отчёта выводится **security score** (0–100) и оценка от `A` до `F`.
|
|
124
|
+
|
|
125
|
+
### Исключения
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
eval(trusted_expr) # aigis: ignore
|
|
129
|
+
DEBUG = True # aigis: ignore[AIG014]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Файлы и папки исключаются через `.aigisignore` (glob-шаблоны) или флаг `--exclude`.
|
|
133
|
+
`node_modules`, `.venv`, `dist` и lock-файлы пропускаются автоматически, а в git-репозитории ещё и всё из `.gitignore`.
|
|
134
|
+
|
|
135
|
+
## Автоисправление: `aigis fix`
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
aigis fix --dry-run # только показать diff
|
|
139
|
+
aigis fix # применить
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Исправляет только то, что можно поменять без риска сломать логику:
|
|
143
|
+
|
|
144
|
+
| Было | Стало |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `BOT_TOKEN = "7312845567:AA..."` | `BOT_TOKEN = os.environ["BOT_TOKEN"]` + значение в `.env`, ключ в `.env.example` |
|
|
147
|
+
| `connect(password="Pa55word!")` | `connect(password=os.environ["PASSWORD"])` |
|
|
148
|
+
| `.env` не в `.gitignore` | `.env` добавлен в `.gitignore` |
|
|
149
|
+
| `requests.get(url, verify=False)` | `verify=True` |
|
|
150
|
+
| `yaml.load(data)` | `yaml.safe_load(data)` |
|
|
151
|
+
| `otp = random.randint(100000, 999999)` | `otp = secrets.randbelow(900000) + 100000` |
|
|
152
|
+
| `token = random.choice(alphabet)` | `secrets.choice(alphabet)` |
|
|
153
|
+
| `app.run(debug=True)` / `DEBUG = True` | `debug=False` / `DEBUG = os.getenv("DEBUG") == "1"` |
|
|
154
|
+
| `- "5432:5432"` в compose | `- "127.0.0.1:5432:5432"` |
|
|
155
|
+
|
|
156
|
+
Нужные `import os` / `import secrets` добавляются сами. SQL-инъекции, `eval`, `pickle`, JWT и CORS
|
|
157
|
+
автоматически не трогаются: там нужно понимать логику приложения, поэтому они остаются в списке «вручную».
|
|
158
|
+
|
|
159
|
+
## Утёкшие ключи в истории: `aigis history`
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
$ aigis history
|
|
163
|
+
aigis history: секретов в истории git: 1
|
|
164
|
+
|
|
165
|
+
[CRITICAL] AIG001 7312************ (Telegram bot token)
|
|
166
|
+
bot.py · коммит 402c92b от 2026-09-14 · удалён, но остался в истории
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Удалить ключ новым коммитом мало: старая версия файла остаётся в истории, и её видит любой, кто склонировал репо.
|
|
170
|
+
`aigis history` проходит по всем коммитам и веткам, показывает, где и когда ключ появился и есть ли он в коде сейчас.
|
|
171
|
+
Ключи в отчёте замаскированы. Для CI: `aigis history -f json`, exit code `1`, если что-то найдено.
|
|
172
|
+
|
|
173
|
+
## Бейдж для README
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
aigis badge # пишет aigis-badge.svg с оценкой, например «aigis | A 100/100»
|
|
177
|
+
aigis badge --url # ссылка shields.io, без файла
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## AI-режим
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export AIGIS_API_KEY=... # любой OpenAI-совместимый API
|
|
184
|
+
export AIGIS_BASE_URL=https://api.openai.com/v1 # или свой прокси / локальная LLM
|
|
185
|
+
export AIGIS_MODEL=gpt-4o-mini
|
|
186
|
+
aigis scan . --ai
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Для каждой находки модель получает фрагмент кода вокруг неё и возвращает исправление именно под твой код.
|
|
190
|
+
**Находки с секретами (`AIG001`–`AIG005`) в модель никогда не отправляются.** Если ключа нет или API недоступен,
|
|
191
|
+
сканер предупредит об этом и покажет стандартные советы.
|
|
192
|
+
|
|
193
|
+
## GitHub Actions
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
name: security
|
|
197
|
+
on: [push, pull_request]
|
|
198
|
+
|
|
199
|
+
jobs:
|
|
200
|
+
aigis:
|
|
201
|
+
runs-on: ubuntu-latest
|
|
202
|
+
permissions:
|
|
203
|
+
contents: read
|
|
204
|
+
security-events: write
|
|
205
|
+
steps:
|
|
206
|
+
- uses: actions/checkout@v4
|
|
207
|
+
- uses: jasperBLCK/AigisSAST@v0.2.0
|
|
208
|
+
with:
|
|
209
|
+
fail-on: high
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Находки появятся во вкладке **Security → Code scanning** и прямо в diff пулл-реквеста.
|
|
213
|
+
|
|
214
|
+
## pre-commit
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
repos:
|
|
218
|
+
- repo: https://github.com/jasperBLCK/AigisSAST
|
|
219
|
+
rev: v0.2.0
|
|
220
|
+
hooks:
|
|
221
|
+
- id: aigis
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Как устроено
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
aigis scan
|
|
228
|
+
│
|
|
229
|
+
├── scanner git ls-files → фильтры → .aigisignore → чтение файлов
|
|
230
|
+
├── checks/
|
|
231
|
+
│ ├── python_ast ast.NodeVisitor: SQL, shell, eval, pickle, JWT, CORS, debug, random, FastAPI auth
|
|
232
|
+
│ └── text regex: токены, ключи, DSN, конфиги, Dockerfile, compose, JS/TS
|
|
233
|
+
├── ai (опционально) OpenAI-совместимый API, секреты отфильтрованы
|
|
234
|
+
└── report text · json · sarif · markdown + score / grade
|
|
235
|
+
|
|
236
|
+
aigis fix AST-позиции находок → точечные правки → .env / .env.example / .gitignore
|
|
237
|
+
aigis history git log -p --all → добавленные строки → те же детекторы секретов
|
|
238
|
+
aigis badge score / grade → SVG или shields.io
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Разработка:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
python -m venv .venv && . .venv/bin/activate
|
|
245
|
+
pip install -e ".[dev]"
|
|
246
|
+
ruff check . && ruff format --check . && pytest -q
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Roadmap
|
|
250
|
+
|
|
251
|
+
- [x] Python AST-правила, секреты, конфиги, Dockerfile, compose, JS/TS
|
|
252
|
+
- [x] SARIF, JSON, Markdown, GitHub Action, pre-commit
|
|
253
|
+
- [x] AI-исправления через OpenAI-совместимый API
|
|
254
|
+
- [x] `aigis fix`: автоматическое исправление безопасных случаев (вынос секретов в `.env`)
|
|
255
|
+
- [x] `aigis history`: поиск секретов во всей истории git
|
|
256
|
+
- [x] `aigis badge`: бейдж с оценкой безопасности
|
|
257
|
+
- [ ] Проверка зависимостей на известные CVE и тайпсквоттинг
|
|
258
|
+
- [ ] Правила для Django и Flask, Go и PHP
|
|
259
|
+
- [ ] VS Code extension
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
[MIT](https://github.com/jasperBLCK/AigisSAST/blob/main/LICENSE)
|
|
264
|
+
|
|
265
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/divider.svg" width="100%"/>
|
|
266
|
+
|
|
267
|
+
<div align="center"><sub>built by <a href="https://github.com/jasperBLCK">jasperBLCK</a> · ship fast, ship secure</sub></div>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/header.svg" width="100%" alt="AigisSAST"/>
|
|
4
|
+
|
|
5
|
+
<h3>ИИ пишет код быстро. AigisSAST проверяет, что он не оставил дыр.</h3>
|
|
6
|
+
|
|
7
|
+
<p><i>Security scanner for AI-generated code: finds what vibe coding leaves behind and explains how to fix it.</i></p>
|
|
8
|
+
|
|
9
|
+
<a href="https://github.com/jasperBLCK/AigisSAST/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/jasperBLCK/AigisSAST/ci.yml?branch=main&style=for-the-badge&label=CI&labelColor=000000&color=333333&logo=githubactions&logoColor=white"/></a> <img src="https://img.shields.io/badge/python-3.9+-000000?style=for-the-badge&logo=python&logoColor=white"/> <img src="https://img.shields.io/badge/dependencies-0-000000?style=for-the-badge"/> <img src="https://img.shields.io/badge/SARIF-2.1.0-000000?style=for-the-badge&logo=github&logoColor=white"/> <img src="https://img.shields.io/badge/aigis-A%20100%2F100-000000?style=for-the-badge"/> <img src="https://img.shields.io/badge/license-MIT-000000?style=for-the-badge"/>
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/divider.svg" width="100%"/>
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/demo.svg" width="92%" alt="aigis scan demo"/>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## Зачем
|
|
20
|
+
|
|
21
|
+
Cursor, Copilot и ChatGPT собирают рабочий бэкенд за вечер. Но по пути они почти всегда оставляют одно и то же:
|
|
22
|
+
токен бота прямо в коде, `f"SELECT ... {user}"`, `allow_origins=["*"]`, `debug=True`, открытый наружу Postgres.
|
|
23
|
+
Код работает, тесты зелёные, а сервис уже можно взломать.
|
|
24
|
+
|
|
25
|
+
AigisSAST ловит именно эти ошибки и объясняет каждую **простым языком**: чем она опасна и как исправить, с готовым кодом.
|
|
26
|
+
Без регистрации и облака, ничего не отправляет наружу (если сам не включишь `--ai`).
|
|
27
|
+
|
|
28
|
+
- **Ноль зависимостей.** Только стандартная библиотека Python, ставится за секунду.
|
|
29
|
+
- **AST, а не grep.** Python-код разбирается синтаксическим деревом, поэтому меньше ложных срабатываний.
|
|
30
|
+
- **Секреты маскируются** в отчёте: `sk-p************`, сам ключ в логах CI не светится.
|
|
31
|
+
- **Не только ругается, но и чинит.** `aigis fix` сам выносит ключи в `.env` и правит безопасные случаи.
|
|
32
|
+
- **Видит прошлое.** `aigis history` находит ключи, которые «удалили», но они остались в истории git.
|
|
33
|
+
- **CI-ready.** Exit-коды, SARIF для GitHub Code Scanning, JSON, Markdown, GitHub Action и pre-commit hook.
|
|
34
|
+
|
|
35
|
+
## Быстрый старт
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install git+https://github.com/jasperBLCK/AigisSAST
|
|
39
|
+
aigis scan . # найти проблемы
|
|
40
|
+
aigis fix --dry-run # посмотреть, что исправится автоматически
|
|
41
|
+
aigis fix # исправить
|
|
42
|
+
aigis history # проверить всю историю git на утёкшие ключи
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Попробуй на заведомо дырявом примере:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/jasperBLCK/AigisSAST && cd AigisSAST
|
|
49
|
+
pip install . && aigis scan examples
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Что ловит
|
|
53
|
+
|
|
54
|
+
| ID | Уровень | Проблема | Где |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `AIG001` | CRITICAL | Токены и API-ключи: OpenAI, Anthropic, Telegram, GitHub, AWS, Stripe, Slack, Google, Yandex Cloud | везде |
|
|
57
|
+
| `AIG004` | CRITICAL | Приватные ключи (RSA / EC / OpenSSH / PGP) | везде |
|
|
58
|
+
| `AIG002` | HIGH | Захардкоженные пароли и секреты | Python, JS/TS, YAML, Dockerfile, compose, .ini/.toml |
|
|
59
|
+
| `AIG003` | HIGH | Строка подключения к БД с паролем | везде, кроме документации |
|
|
60
|
+
| `AIG005` | HIGH | `.env` попадает в репозиторий | git / .gitignore |
|
|
61
|
+
| `AIG010` | HIGH | SQL-инъекция: f-string, `%`, `+`, `.format()` в `execute()` / `text()` | Python |
|
|
62
|
+
| `AIG011` | HIGH | Shell-инъекция: `shell=True`, `os.system` с переменной | Python |
|
|
63
|
+
| `AIG012` | HIGH | `eval` / `exec` над динамическими данными | Python |
|
|
64
|
+
| `AIG013` | HIGH | `pickle.loads`, `yaml.load` без SafeLoader | Python |
|
|
65
|
+
| `AIG017` | HIGH | JWT без проверки подписи, `algorithms=["none"]` | Python |
|
|
66
|
+
| `AIG030` | HIGH | `eval` / `new Function` | JS / TS |
|
|
67
|
+
| `AIG014` | MEDIUM | `debug=True`, `DEBUG = True` | Python |
|
|
68
|
+
| `AIG015` | MEDIUM → HIGH | CORS `*` (HIGH вместе с `allow_credentials=True`) | FastAPI, Express |
|
|
69
|
+
| `AIG016` | MEDIUM | `verify=False` в requests / httpx | Python |
|
|
70
|
+
| `AIG019` | MEDIUM | Токены и OTP-коды через `random` вместо `secrets` | Python |
|
|
71
|
+
| `AIG031` | MEDIUM | XSS: `innerHTML`, `dangerouslySetInnerHTML`, `v-html` | JS / TS / Vue |
|
|
72
|
+
| `AIG032` | MEDIUM | Секреты в `NEXT_PUBLIC_*` / `VITE_*` / `REACT_APP_*` | фронтенд |
|
|
73
|
+
| `AIG042` | MEDIUM | Порт БД/Redis опубликован на `0.0.0.0` | docker-compose |
|
|
74
|
+
| `AIG018` | LOW | MD5 / SHA-1 | Python |
|
|
75
|
+
| `AIG021` | LOW | POST/PUT/PATCH/DELETE без `Depends(...)` авторизации | FastAPI |
|
|
76
|
+
| `AIG040` | LOW | Контейнер работает от root | Dockerfile |
|
|
77
|
+
|
|
78
|
+
Подробно про любое правило: `aigis explain AIG010`, полный список: `aigis rules`.
|
|
79
|
+
|
|
80
|
+
## Использование
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
aigis scan . # текстовый отчёт с объяснениями
|
|
84
|
+
aigis scan src api --quiet # несколько путей, без блоков «чем опасно / как исправить»
|
|
85
|
+
aigis scan . --min-severity medium # скрыть LOW
|
|
86
|
+
aigis scan . --fail-on critical # падать только на критичных
|
|
87
|
+
aigis scan . -f sarif -o aigis.sarif # для GitHub Code Scanning
|
|
88
|
+
aigis scan . -f json | jq '.findings[]' # для своих скриптов
|
|
89
|
+
aigis scan . -f markdown > SECURITY.md # отчёт для PR / заказчика
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Exit code | Значение |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `0` | нет находок уровня `--fail-on` (по умолчанию `high`) и выше |
|
|
95
|
+
| `1` | найдены проблемы уровня `--fail-on` и выше |
|
|
96
|
+
| `2` | ошибка запуска (неверный путь, неизвестное правило) |
|
|
97
|
+
|
|
98
|
+
В конце отчёта выводится **security score** (0–100) и оценка от `A` до `F`.
|
|
99
|
+
|
|
100
|
+
### Исключения
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
eval(trusted_expr) # aigis: ignore
|
|
104
|
+
DEBUG = True # aigis: ignore[AIG014]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Файлы и папки исключаются через `.aigisignore` (glob-шаблоны) или флаг `--exclude`.
|
|
108
|
+
`node_modules`, `.venv`, `dist` и lock-файлы пропускаются автоматически, а в git-репозитории ещё и всё из `.gitignore`.
|
|
109
|
+
|
|
110
|
+
## Автоисправление: `aigis fix`
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
aigis fix --dry-run # только показать diff
|
|
114
|
+
aigis fix # применить
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Исправляет только то, что можно поменять без риска сломать логику:
|
|
118
|
+
|
|
119
|
+
| Было | Стало |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `BOT_TOKEN = "7312845567:AA..."` | `BOT_TOKEN = os.environ["BOT_TOKEN"]` + значение в `.env`, ключ в `.env.example` |
|
|
122
|
+
| `connect(password="Pa55word!")` | `connect(password=os.environ["PASSWORD"])` |
|
|
123
|
+
| `.env` не в `.gitignore` | `.env` добавлен в `.gitignore` |
|
|
124
|
+
| `requests.get(url, verify=False)` | `verify=True` |
|
|
125
|
+
| `yaml.load(data)` | `yaml.safe_load(data)` |
|
|
126
|
+
| `otp = random.randint(100000, 999999)` | `otp = secrets.randbelow(900000) + 100000` |
|
|
127
|
+
| `token = random.choice(alphabet)` | `secrets.choice(alphabet)` |
|
|
128
|
+
| `app.run(debug=True)` / `DEBUG = True` | `debug=False` / `DEBUG = os.getenv("DEBUG") == "1"` |
|
|
129
|
+
| `- "5432:5432"` в compose | `- "127.0.0.1:5432:5432"` |
|
|
130
|
+
|
|
131
|
+
Нужные `import os` / `import secrets` добавляются сами. SQL-инъекции, `eval`, `pickle`, JWT и CORS
|
|
132
|
+
автоматически не трогаются: там нужно понимать логику приложения, поэтому они остаются в списке «вручную».
|
|
133
|
+
|
|
134
|
+
## Утёкшие ключи в истории: `aigis history`
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
$ aigis history
|
|
138
|
+
aigis history: секретов в истории git: 1
|
|
139
|
+
|
|
140
|
+
[CRITICAL] AIG001 7312************ (Telegram bot token)
|
|
141
|
+
bot.py · коммит 402c92b от 2026-09-14 · удалён, но остался в истории
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Удалить ключ новым коммитом мало: старая версия файла остаётся в истории, и её видит любой, кто склонировал репо.
|
|
145
|
+
`aigis history` проходит по всем коммитам и веткам, показывает, где и когда ключ появился и есть ли он в коде сейчас.
|
|
146
|
+
Ключи в отчёте замаскированы. Для CI: `aigis history -f json`, exit code `1`, если что-то найдено.
|
|
147
|
+
|
|
148
|
+
## Бейдж для README
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
aigis badge # пишет aigis-badge.svg с оценкой, например «aigis | A 100/100»
|
|
152
|
+
aigis badge --url # ссылка shields.io, без файла
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## AI-режим
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
export AIGIS_API_KEY=... # любой OpenAI-совместимый API
|
|
159
|
+
export AIGIS_BASE_URL=https://api.openai.com/v1 # или свой прокси / локальная LLM
|
|
160
|
+
export AIGIS_MODEL=gpt-4o-mini
|
|
161
|
+
aigis scan . --ai
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Для каждой находки модель получает фрагмент кода вокруг неё и возвращает исправление именно под твой код.
|
|
165
|
+
**Находки с секретами (`AIG001`–`AIG005`) в модель никогда не отправляются.** Если ключа нет или API недоступен,
|
|
166
|
+
сканер предупредит об этом и покажет стандартные советы.
|
|
167
|
+
|
|
168
|
+
## GitHub Actions
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
name: security
|
|
172
|
+
on: [push, pull_request]
|
|
173
|
+
|
|
174
|
+
jobs:
|
|
175
|
+
aigis:
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
permissions:
|
|
178
|
+
contents: read
|
|
179
|
+
security-events: write
|
|
180
|
+
steps:
|
|
181
|
+
- uses: actions/checkout@v4
|
|
182
|
+
- uses: jasperBLCK/AigisSAST@v0.2.0
|
|
183
|
+
with:
|
|
184
|
+
fail-on: high
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Находки появятся во вкладке **Security → Code scanning** и прямо в diff пулл-реквеста.
|
|
188
|
+
|
|
189
|
+
## pre-commit
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
repos:
|
|
193
|
+
- repo: https://github.com/jasperBLCK/AigisSAST
|
|
194
|
+
rev: v0.2.0
|
|
195
|
+
hooks:
|
|
196
|
+
- id: aigis
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Как устроено
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
aigis scan
|
|
203
|
+
│
|
|
204
|
+
├── scanner git ls-files → фильтры → .aigisignore → чтение файлов
|
|
205
|
+
├── checks/
|
|
206
|
+
│ ├── python_ast ast.NodeVisitor: SQL, shell, eval, pickle, JWT, CORS, debug, random, FastAPI auth
|
|
207
|
+
│ └── text regex: токены, ключи, DSN, конфиги, Dockerfile, compose, JS/TS
|
|
208
|
+
├── ai (опционально) OpenAI-совместимый API, секреты отфильтрованы
|
|
209
|
+
└── report text · json · sarif · markdown + score / grade
|
|
210
|
+
|
|
211
|
+
aigis fix AST-позиции находок → точечные правки → .env / .env.example / .gitignore
|
|
212
|
+
aigis history git log -p --all → добавленные строки → те же детекторы секретов
|
|
213
|
+
aigis badge score / grade → SVG или shields.io
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Разработка:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m venv .venv && . .venv/bin/activate
|
|
220
|
+
pip install -e ".[dev]"
|
|
221
|
+
ruff check . && ruff format --check . && pytest -q
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Roadmap
|
|
225
|
+
|
|
226
|
+
- [x] Python AST-правила, секреты, конфиги, Dockerfile, compose, JS/TS
|
|
227
|
+
- [x] SARIF, JSON, Markdown, GitHub Action, pre-commit
|
|
228
|
+
- [x] AI-исправления через OpenAI-совместимый API
|
|
229
|
+
- [x] `aigis fix`: автоматическое исправление безопасных случаев (вынос секретов в `.env`)
|
|
230
|
+
- [x] `aigis history`: поиск секретов во всей истории git
|
|
231
|
+
- [x] `aigis badge`: бейдж с оценкой безопасности
|
|
232
|
+
- [ ] Проверка зависимостей на известные CVE и тайпсквоттинг
|
|
233
|
+
- [ ] Правила для Django и Flask, Go и PHP
|
|
234
|
+
- [ ] VS Code extension
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
[MIT](https://github.com/jasperBLCK/AigisSAST/blob/main/LICENSE)
|
|
239
|
+
|
|
240
|
+
<img src="https://raw.githubusercontent.com/jasperBLCK/AigisSAST/main/assets/divider.svg" width="100%"/>
|
|
241
|
+
|
|
242
|
+
<div align="center"><sub>built by <a href="https://github.com/jasperBLCK">jasperBLCK</a> · ship fast, ship secure</sub></div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aigis-sast"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "AigisSAST: security scanner for AI-generated code. Finds what vibe coding leaves behind and explains how to fix it"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "jasperBLCK" }]
|
|
14
|
+
keywords = ["security", "sast", "linter", "secrets", "ai", "vibe-coding", "fastapi"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
aigis = "aigis.cli:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/jasperBLCK/AigisSAST"
|
|
33
|
+
Source = "https://github.com/jasperBLCK/AigisSAST"
|
|
34
|
+
Issues = "https://github.com/jasperBLCK/AigisSAST/issues"
|
|
35
|
+
Changelog = "https://github.com/jasperBLCK/AigisSAST/releases"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 120
|
|
42
|
+
target-version = "py39"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from aigis.models import Finding
|
|
10
|
+
|
|
11
|
+
SECRET_RULES = {"AIG001", "AIG002", "AIG003", "AIG004", "AIG005"}
|
|
12
|
+
PROMPT = (
|
|
13
|
+
"Ты senior security-инженер. Ниже уязвимый фрагмент кода и найденная проблема. "
|
|
14
|
+
"Ответь по-русски, коротко и простыми словами: 1) одной фразой, как это можно эксплуатировать "
|
|
15
|
+
"именно здесь; 2) исправленный код этого фрагмента. Без вступлений."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AIConfig:
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.api_key = os.environ.get("AIGIS_API_KEY", "")
|
|
22
|
+
self.base_url = os.environ.get("AIGIS_BASE_URL", "https://api.openai.com/v1").rstrip("/")
|
|
23
|
+
self.model = os.environ.get("AIGIS_MODEL", "gpt-4o-mini")
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def enabled(self) -> bool:
|
|
27
|
+
return bool(self.api_key)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _context(root: Path, finding: Finding, radius: int = 6) -> str:
|
|
31
|
+
try:
|
|
32
|
+
lines = (root / finding.path).read_text("utf-8", "replace").splitlines()
|
|
33
|
+
except OSError:
|
|
34
|
+
return finding.snippet
|
|
35
|
+
start = max(0, finding.line - 1 - radius)
|
|
36
|
+
chunk = lines[start : finding.line + radius]
|
|
37
|
+
return "\n".join(f"{start + i + 1:>4} {ln}" for i, ln in enumerate(chunk))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ask(cfg: AIConfig, content: str, timeout: float) -> str:
|
|
41
|
+
body = json.dumps(
|
|
42
|
+
{
|
|
43
|
+
"model": cfg.model,
|
|
44
|
+
"temperature": 0.2,
|
|
45
|
+
"messages": [{"role": "system", "content": PROMPT}, {"role": "user", "content": content}],
|
|
46
|
+
}
|
|
47
|
+
).encode()
|
|
48
|
+
req = urllib.request.Request(
|
|
49
|
+
f"{cfg.base_url}/chat/completions",
|
|
50
|
+
data=body,
|
|
51
|
+
headers={"Authorization": f"Bearer {cfg.api_key}", "Content-Type": "application/json"},
|
|
52
|
+
)
|
|
53
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
54
|
+
data = json.loads(resp.read())
|
|
55
|
+
return str(data["choices"][0]["message"]["content"]).strip()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def enrich(root: Path, findings: list[Finding], *, limit: int = 15, timeout: float = 30.0) -> str | None:
|
|
59
|
+
"""Add AI-written, context-specific fixes. Secret findings are never sent to the model."""
|
|
60
|
+
cfg = AIConfig()
|
|
61
|
+
if not cfg.enabled:
|
|
62
|
+
return "AIGIS_API_KEY не задан: AI-режим пропущен, показаны стандартные советы."
|
|
63
|
+
for f in [f for f in findings if f.rule.id not in SECRET_RULES][:limit]:
|
|
64
|
+
content = f"Проблема: {f.rule.id} {f.rule.title}\nФайл: {f.path}:{f.line}\n\n{_context(root, f)}"
|
|
65
|
+
try:
|
|
66
|
+
f.ai_fix = _ask(cfg, content, timeout)
|
|
67
|
+
except (urllib.error.URLError, TimeoutError, KeyError, ValueError) as exc:
|
|
68
|
+
return f"AI-режим недоступен ({exc}); показаны стандартные советы."
|
|
69
|
+
return None
|