LIMB
Вход Регистрация

тема

AI-Scanner на стероидах: Пишем свой приватный SAST на базе Ollama + Python

benjaminbutton · · 9 ответов · 190 просмотров


AI-Scanner на стероидах: Свой SAST на базе Local LLM v1.0

Превращаем локальную нейронку в полноценный инструмент поиска уязвимостей
​


Введение

Тема с использованием AI для анализа кода не новая, но 90% скриптов в паблике - это детский сад из трех строк: "прочитай файл -> отправь в чат". Это не работает в реальных условиях.

Если вы пробовали скармливать нейронке реальные исходники, вы знаете боль:
  • Она теряет контекст на больших файлах.
  • Она галлюцинирует с номерами строк.
  • Она не видит баги, если они "разрезаны" между частями запроса.

Сегодня мы напишем взрослый инструмент на Python. Это будет CLI-утилита, которая умеет нарезать код на чанки с перекрытием (overlap), нумерует строки перед отправкой и сохраняет результат в JSON-отчет.

Никакой воды, только код и практика.


Архитектура решения

Мы используем стандартную связку:
  • Backend: Ollama (локальный API).
  • Model: qwen2.5-coder (сейчас топ для кода) или dolphin-llama3.
  • Logic: Python-скрипт с валидацией JSON и сохранением отчетов.

Важный момент про контекст:
Мы не используем точный подсчет токенов (через tiktoken), чтобы не тащить лишние зависимости. Мы используем эвристику "X строк кода ≈ Y токенов". Это не идеально, но для экспресс-аудита работает отлично. Также мы добавили Sliding Window (скользящее окно), чтобы не терять уязвимости на границах чанков.


Установка и запуск

Перед запуском нужно сделать две вещи:
1. Скачать модель в Ollama:
Code:
ollama pull qwen2.5-coder
2. Установить библиотеку для запросов:
Code:
pip install requests


Код Сканера (DeepAudit.py)

В этой версии добавлен аргумент --output для сохранения отчета, механизм overlap и улучшена обработка ошибок JSON-режима Ollama.

Спойлер: Развернуть исходный код (Python)
Python:
import sys
import argparse
import json
import requests
import os

# --- КОНФИГУРАЦИЯ ---
OLLAMA_API = "http://localhost:11434/api/generate"
DEFAULT_MODEL = "qwen2.5-coder" # Или dolphin-llama3

# Эвристика: ~150 строк кода обычно влезают в 4k-8k контекст с запасом на промпт.
# Если модель 32 тыщи контекста - можно ставить больше.
CHUNK_SIZE = 150

# Перекрытие строк, чтобы не потерять контекст на стыке чанков
OVERLAP_LINES = 20

def banner():
    print("--- Local LLM SAST Scanner v1.0 ---")

def parse_args():
    parser = argparse.ArgumentParser(description="AI Code Vulnerability Scanner")
    parser.add_argument("file", help="Path to source file")
    parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Ollama model (default: {DEFAULT_MODEL})")
    parser.add_argument("--timeout", type=int, default=120, help="API request timeout in seconds")
    parser.add_argument("--output", help="Save report to JSON file")
    return parser.parse_args()

def prepare_code_chunks(file_path):
    """Читает файл и разбивает его на чанки с перекрытием и нумерацией"""
    try:
        # Фолбэк кодировок для легаси-кода
        content = ""
        lines = []
        for encoding in ['utf-8', 'latin-1', 'cp1251']:
            try:
                with open(file_path, 'r', encoding=encoding) as f:
                    lines = f.readlines()
                break
            except UnicodeDecodeError:
                continue
        
        if not lines:
            print("[!] Failed to read file or file is empty.")
            sys.exit(1)

        chunks = []
        total_lines = len(lines)
        
        # Реализация Sliding Window
        step = CHUNK_SIZE - OVERLAP_LINES
        if step < 1: step = 1
        
        for i in range(0, total_lines, step):
            chunk_lines = lines[i : i + CHUNK_SIZE]
            if not chunk_lines: break
            
            chunk_content = ""
            start_line = i + 1
            
            # Вшиваем номера строк прямо в контент
            for idx, line in enumerate(chunk_lines):
                chunk_content += f"{start_line + idx}: {line}"
            
            end_line = start_line + len(chunk_lines) - 1
            
            chunks.append({
                "content": chunk_content,
                "lines_range": f"{start_line}-{end_line}"
            })
            
            # Если дошли до конца файла
            if end_line == total_lines:
                break
                
        return chunks
    except Exception as e:
        print(f"[!] Error reading file: {e}")
        sys.exit(1)

def analyze_chunk(chunk, model, timeout):
    """Отправляет чанк в LLM с форсированием JSON"""
    prompt = f"""
    You are a Senior Security Engineer doing a code audit.
    Analyze the provided code chunk for security vulnerabilities (SQLi, XSS, RCE, IDOR, Logic Bugs, Secrets).
    INSTRUCTIONS:
    1. Respond ONLY in valid JSON format.
    2. Do NOT explain logic if no bugs found.
    3. Use the provided line numbers in 'line' field.
    4. Ignore minor code style issues.

    JSON SCHEMA:
    {{
        "vulnerabilities": [
            {{
                "type": "Vulnerability Type",
                "severity": "High/Medium/Low",
                "line": "Line Number",
                "details": "Short explanation",
                "fix": "Suggested fix"
            }}
        ]
    }}

    CODE CHUNK:
    {chunk['content']}
    """
    
    data = {
        "model": model,
        "prompt": prompt,
        "format": "json",  # Киллер-фича Ollama для строгого JSON
        "stream": False
    }
    
    try:
        response = requests.post(OLLAMA_API, json=data, timeout=timeout)
        response.raise_for_status()
        return response.json().get('response', '')
    except requests.exceptions.RequestException:
        return None

def main():
    banner()
    args = parse_args()
    
    if not os.path.isfile(args.file):
        print(f"[!] File not found: {args.file}")
        sys.exit(1)
        
    print(f"[*] Target: {args.file}")
    print(f"[*] Model: {args.model}")
    
    chunks = prepare_code_chunks(args.file)
    print(f"[*] Split into {len(chunks)} chunks (Overlap: {OVERLAP_LINES} lines). Starting analysis...\n")
    
    all_vulns = []
    
    for i, chunk in enumerate(chunks):
        print(f" -> Analyzing chunk {i+1}/{len(chunks)} (Lines: {chunk['lines_range']})...", end="\r")
        result_raw = analyze_chunk(chunk, args.model, args.timeout)
        
        if result_raw:
            try:
                result_json = json.loads(result_raw)
                if "vulnerabilities" in result_json and result_json["vulnerabilities"]:
                    for vuln in result_json["vulnerabilities"]:
                        # Дедупликация (так как у нас есть перекрытие чанков)
                        if vuln not in all_vulns:
                            all_vulns.append(vuln)
            except json.JSONDecodeError:
                pass # Бывает, что модель все равно плюет мусор, игнорируем

    print("\n\n" + "="*60)
    print(f"SCAN COMPLETE. Found {len(all_vulns)} unique issues.")
    print("="*60 + "\n")
    
    # Вывод в консоль
    if all_vulns:
        for v in all_vulns:
            print(f"[TYPE] {v.get('type', 'Unknown')}")
            print(f"[SEVERITY] {v.get('severity', 'Unknown')}")
            print(f"[LINE] {v.get('line', '?')}")
            print(f"[DETAILS] {v.get('details', 'No details')}")
            print("-" * 30)
            
    # Сохранение в файл
    if args.output:
        try:
            with open(args.output, 'w', encoding='utf-8') as f:
                json.dump({"target": args.file, "vulnerabilities": all_vulns}, f, indent=4)
            print(f"[*] Report saved to {args.output}")
        except Exception as e:
            print(f"[!] Failed to save report: {e}")

if __name__ == "__main__":
    main()
[/CODE]


Разбор фишек скрипта v1.0

1. Sliding Window (Перекрытие)

Самая большая проблема чанков - когда уязвимость начинается на строке 149, а заканчивается на 152. Если резать жестко, модель не поймет контекст.
Мы добавили 
Code:
OVERLAP_LINES = 20
. Теперь каждый следующий чанк захватывает хвост предыдущего. Это закрывает слепые зоны на стыках.

2. Эвристика строк вместо токенов
Мы намеренно не используем tiktoken или тяжелые библиотеки.
Правило простое: ~150 строк кода + системный промпт обычно комфортно влезают в контекст 4k-8k (стандарт для Qwen/Llama3). Если у вас модель с 32k контекста - можете смело крутить CHUNK_SIZE до 500-600.

3. Честный JSON Mode
Параметр 
Code:
format: "json"
 в Ollama творит чудеса. Модель не пишет "Here is the analysis...", она сразу открывает фигурную скобку. Это упрощает парсинг до одной строчки json.loads().

4. Экспорт отчетов
Добавлен флаг 
Code:
--output report.json
. Полезно, если сканируете большой проект и хотите потом закинуть результаты в дашборд или просто сохранить на память.


Демонстрация работы

Для теста я взял грязный PHP-файл.
Запускаем с сохранением отчета:
Code:
python3 DeepAudit.py targets/leaky_db.php --output report.json

Результат в терминале:
Code:
[*] Target: targets/leaky_db.php
[*] Model: qwen2.5-coder
[*] Split into 1 chunks (Overlap: 20 lines). Starting analysis...

============================================================
SCAN COMPLETE. Found 2 unique issues.
============================================================

[TYPE] SQL Injection
[SEVERITY] High
[LINE] 24
[DETAILS] User input $_GET['id'] is concatenated directly into SQL query.
------------------------------
[TYPE] XSS (Reflected)
[SEVERITY] Medium
[LINE] 28
[DETAILS] Variable $name is echoed back without sanitization.
------------------------------
[*] Report saved to report.json


Итог

У нас получился рабочий прототип SAST-сканера, который:
  • Бесплатен (не считая электричества GPU/CPU).
  • Приватен (код не покидает периметр).
  • Масштабируем (скрипт легко допилить под рекурсивный обход папок).

Дальше можно прикрутить многопоточность (concurrent.futures) для скана целых репозиториев за секунды, но для базового аудита этого "калашмата" более чем достаточно.

Автор: Benjamin_Button​
👍17 😮4 👏3 🤔2
Всё разжевано по полочкам и интересно
сильно, бро, keep it up
для поставленных задач (локальный AI- чанки с перехлестом- поиск уязвимостей) наверное нормальный подход.
А по сравнению с SAST - потеря части потенциальных уязвимостей. 
Серьезный софт строит глобальный CFG (Control Flow Graph).
почему? потому что в промежуточном чанке неизвестно  откуда пришли данные  и насколько они проверены ранее.

но это уже другой уровень.
Направление - очень трендовое. Хорошо что появилась эта статья. Но конечно, сорри , не хочу обидеть автора, слишком все просто.
По моим наблюдениям - в молодости хочется изобрести велосипед, с возрастом - взять чужой и долить бензина.

Я имею ввиду https://github.com/lintsinghua/DeepAudit/ - DeepAudit, о котором автор скорее всего знает. Возможно решил упростить саму идею.
Назовем софт из статьи DIY SAST v1.0 - для краткости :)

Что из себя представляет DeepAudit? Разрабом (китаец, 1 шт.) заявлено:

1. Локальное развертывание LLM .
2. Мультиагентность (Мультиагентная архитектура).
Использует оркестратор и специализированных агентов (Разведка/Recon, Анализ/Analysis, Верификация/Verification) для автономного аудита
Два режима работы:
  • Быстрый анализ: Пофайловая проверка через LLM/правила (минуты,适合 CI/CD).
  • Глубокий аудит: Многоэтапная работа агентов с RAG и проверкой PoC (часы, выше точность).
Автоматическая верификация уязвимостей: Генерация и запуск доказательств эксплуатации (PoC) в изолированной Docker-песочнице.
RAG и работа с контекстом: Векторная база знаний (ChromaDB) для семантического поиска, связи между файлами и уменьшения галлюцинаций.

Подробные отчеты и так далее
============================================

Вопросы коллегам;)

1. Кто-то тестировал это изделие?
2. Есть смысл его дообучать и допиливать?
👍3
Ответlisa99

Направление - очень трендовое. Хорошо что появилась эта статья. Но конечно, сорри , не хочу обидеть автора, слишком все просто. По моим наблюдениям - в молодости хочется изобрести велосипед, с возрастом - взять чужой и…

дипаудит это жирный комбайн, а я пилил легкий калаш для быстрого аудита в один клик без лишнего мусора и докер-ферм
👍4 ❤️1
Ответbenjaminbutton

дипаудит это жирный комбайн, а я пилил легкий калаш для быстрого аудита в один клик без лишнего мусора и докер-ферм

да это понятно.

То что делаешь ты - уровень файла - черт с ними, с чанками даже.
То что делает он (на словах так) - уровень проектов.
Но.
Вопрос - насколько эффективней этот самый жирный комбайн на практике?
👍2
Ответlisa99

да это понятно. То что делаешь ты - уровень файла - черт с ними, с чанками даже. То что делает он (на словах так) - уровень проектов. Но. Вопрос - насколько эффективней этот самый жирный комбайн на практике?

эффективность комбайна на практике часто разбивается об тонну ложных срабатываний, а мой вариант  это про быстрый чекинг здесь и сейчас без лишнего шума
👍2
кто то тестировал как в работе?
👍1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.