Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[run]
branch = True
source =
services
tenet_plugin
scripts
omit =
*/tests/*
*/test_*.py
*/__pycache__/*
*/site-packages/*
setup.py
*/migrations/*

[report]
precision = 2
show_missing = True
skip_covered = False
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
@abc.abstractmethod
if sys.version_info
if platform.system
except ImportError:
except ModuleNotFoundError:
@overload
if typing.TYPE_CHECKING:

[html]
directory = htmlcov

[xml]
output = coverage.xml

[paths]
source =
services
tenet_plugin
scripts
19 changes: 14 additions & 5 deletions .github/tenet_agent/tenet_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,23 @@

# Allowed source file extensions for LLM-proposed paths
_ALLOWED_EXTENSIONS = {
".py", ".ts", ".tsx", ".js", ".jsx",
".json", ".yaml", ".yml", ".md", ".txt", ".env.example",
".py",
".ts",
".tsx",
".js",
".jsx",
".json",
".yaml",
".yml",
".md",
".txt",
".env.example",
}


# ─── Parsing helpers ──────────────────────────────────────────────────────────


def _safe_filepath(filepath: str, repo_root: Path) -> str | None:
"""
Validate and normalise a filepath proposed by the LLM.
Expand Down Expand Up @@ -145,6 +155,7 @@ def extract_commit_message(llm_output: str, fallback: str) -> str:

# ─── Main flow ────────────────────────────────────────────────────────────────


def main():
"""Run the TENET Agent issue-solver workflow."""
print("🛡️ TENET Agent - Issue Solver starting...")
Expand Down Expand Up @@ -212,9 +223,7 @@ def main():

if file_changes is None:
# LLM said it cannot fix this issue
cannot_fix_reason = re.sub(
r".*### CANNOT_FIX\s*", "", code_output, flags=re.DOTALL
).strip()
cannot_fix_reason = re.sub(r".*### CANNOT_FIX\s*", "", code_output, flags=re.DOTALL).strip()
comment = (
f"## 🤖 TENET Agent - Cannot Auto-Fix\n\n"
f"After analyzing issue #{issue_number}, TENET Agent determined it cannot "
Expand Down
84 changes: 72 additions & 12 deletions .github/tenet_agent/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import google.generativeai as genai
from github import Github, GithubException


# ─── GitHub client ────────────────────────────────────────────────────────────


def get_github_client() -> Github:
"""Create and return an authenticated GitHub client."""
token = os.environ.get("GITHUB_TOKEN")
Expand All @@ -33,11 +33,14 @@ def get_repo(g: Github):

# ─── LLM client ───────────────────────────────────────────────────────────────


def get_llm_client():
"""Configure Gemini and return a GenerativeModel instance."""
api_key = os.environ.get("TENET_AI_KEY")
if not api_key:
print("❌ TENET_AI_KEY secret is not set. Please add it in repo Settings → Secrets → Actions.")
print(
"❌ TENET_AI_KEY secret is not set. Please add it in repo Settings → Secrets → Actions."
)
sys.exit(1)
genai.configure(api_key=api_key)
return genai.GenerativeModel(
Expand Down Expand Up @@ -72,6 +75,7 @@ def call_llm(model, prompt: str) -> str | None:

# ─── PR utilities ─────────────────────────────────────────────────────────────


def get_pr_diff(repo_name: str, pr_number: int, token: str) -> str:
"""Fetch the unified diff for a PR via GitHub API."""
url = f"https://api.github.com/repos/{repo_name}/pulls/{pr_number}"
Expand Down Expand Up @@ -102,6 +106,7 @@ def post_pr_comment(repo, pr_number: int, body: str) -> None:

# ─── Issue utilities ──────────────────────────────────────────────────────────


def post_issue_comment(repo, issue_number: int, body: str) -> None:
"""Post a comment on an issue."""
issue = repo.get_issue(issue_number)
Expand All @@ -112,12 +117,27 @@ def post_issue_comment(repo, issue_number: int, body: str) -> None:
def get_repo_structure(base_path: str = ".", max_files: int = 120) -> str:
"""Walk the repo and return a file tree string (excludes hidden dirs and common noise)."""
skip_dirs = {
".git", "__pycache__", "node_modules", ".venv",
"venv", "dist", "build", ".mypy_cache",
".git",
"__pycache__",
"node_modules",
".venv",
"venv",
"dist",
"build",
".mypy_cache",
}
skip_exts = {
".pyc", ".pyo", ".so", ".egg-info", ".lock", ".log",
".png", ".jpg", ".jpeg", ".svg", ".ico",
".pyc",
".pyo",
".so",
".egg-info",
".lock",
".log",
".png",
".jpg",
".jpeg",
".svg",
".ico",
}
lines = []
count = 0
Expand Down Expand Up @@ -186,21 +206,61 @@ def extract_keywords(text: str) -> list[str]:
"""Extract meaningful keywords from issue text."""
text = re.sub(r"[`*#\[\]()>]+", " ", text)
stop_words = {
"the", "a", "an", "is", "in", "on", "at", "to", "for", "of",
"and", "or", "but", "not", "with", "as", "it", "its", "this",
"that", "be", "was", "are", "have", "has", "do", "does", "i",
"we", "you", "should", "would", "could", "when", "how", "what",
"need", "want", "make", "add", "remove", "fix", "update", "change",
"the",
"a",
"an",
"is",
"in",
"on",
"at",
"to",
"for",
"of",
"and",
"or",
"but",
"not",
"with",
"as",
"it",
"its",
"this",
"that",
"be",
"was",
"are",
"have",
"has",
"do",
"does",
"i",
"we",
"you",
"should",
"would",
"could",
"when",
"how",
"what",
"need",
"want",
"make",
"add",
"remove",
"fix",
"update",
"change",
}
words = re.findall(r"[a-zA-Z_]\w+", text)
return [w for w in words if w.lower() not in stop_words and len(w) > 2]


# ─── Git helpers ──────────────────────────────────────────────────────────────


def _validate_branch_name(name: str) -> bool:
"""Ensure branch name contains only safe characters."""
return bool(re.match(r'^[a-zA-Z0-9._/-]+$', name)) and '..' not in name
return bool(re.match(r"^[a-zA-Z0-9._/-]+$", name)) and ".." not in name


def _validate_filepath(filepath: str, base_path: str = ".") -> bool:
Expand Down
114 changes: 93 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,37 @@ on:
branches: [ main ]

jobs:
test:
lint:
runs-on: ubuntu-latest
name: Lint & Format Check

steps:
- uses: actions/checkout@v6
Comment thread
agsaru marked this conversation as resolved.
Comment thread
agsaru marked this conversation as resolved.

services:
redis:
image: redis
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Run Ruff linter
run: |
ruff check services/ tenet_plugin/ scripts/ tests/
echo "Ruff check completed"

- name: Run Black formatter check
run: |
black --check services/ tenet_plugin/ scripts/ tests/

security:
runs-on: ubuntu-latest
name: Security Scanning

steps:
- uses: actions/checkout@v6

Expand All @@ -37,20 +54,75 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Run unit tests
- name: Run Bandit security scan
run: |
pytest tests/unit/ -v
bandit -r services/ tenet_plugin/ scripts/ -v

- name: Run training script check
- name: Run pip-audit for dependency vulnerabilities
run: |
python scripts/train_model.py --test-only
pip-audit
test:
runs-on: ubuntu-latest
name: Tests & Coverage

steps:
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Run unit tests with coverage
run: |
pytest tests/unit/ -v --cov=services --cov=tenet_plugin --cov-report=xml --cov-report=term-missing

- name: Check coverage threshold
run: |
coverage report --fail-under=50
Comment thread
agsaru marked this conversation as resolved.

- name: Upload coverage to artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage.xml
retention-days: 30

- name: Start complete infrastructure via Docker Compose
env:
POSTGRES_DB: tenet_test
POSTGRES_USER: tenet_user
POSTGRES_PASSWORD: tenet_password
MINIO_USER: tenet-ci
MINIO_PASSWORD: tenet-ci-minio-secret
API_KEY: tenet-dev-key-change-in-production
CORS_ORIGINS: "*"
run: |
# Builds and starts Redis, Postgres, MinIO, Ingest, and Analyzer
docker compose up -d --build
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Wait for services to initialize
run: |
echo "Waiting for health checks to pass..."
# Sleep gives the containers time to boot and run their internal health checks
sleep 15
Comment thread
agsaru marked this conversation as resolved.
Comment thread
agsaru marked this conversation as resolved.

- name: Run integration tests
env:
REDIS_HOST: localhost
REDIS_PORT: 6379
API_URL: http://localhost:8000
ANALYZER_URL: http://localhost:8100
run: |
pytest tests/integration/test_e2e.py -v

- name: Tear down infrastructure
if: always()
run: |
# Only run if services can be mocked or local redis is enough
# Current test_e2e requires full services, so might fail without them
# pytest tests/integration/test_e2e.py -v
echo "Skipping E2E in CI for now - requires analyzer/ingest containers"
# Clean up containers, networks, and volumes
docker compose down -v
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

**Defensive Security Middleware for LLM Applications**

[![CI/CD Pipeline](https://github.com/TENET-DEV-AI/TENET-AI/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TENET-DEV-AI/TENET-AI/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
![Security: Active](https://img.shields.io/badge/security-active-brightgreen.svg)
![Contributors](https://img.shields.io/github/contributors/S3DFX-CYBER/AI-Cyber-Defender)
[![Linting: Ruff](https://img.shields.io/badge/linting-ruff-4B8BBE.svg)](https://github.com/astral-sh/ruff)
[![Security: Active](https://img.shields.io/badge/security-active-brightgreen.svg)](https://github.com/TENET-DEV-AI/TENET-AI/actions/workflows/ci.yml)[![Code Quality: Bandit](https://img.shields.io/badge/security%20scanning-bandit-informational.svg)](https://github.com/PyCQA/bandit)
![Contributors](https://img.shields.io/github/contributors/TENET-DEV-AI/TENET-AI)
> **TENET AI is a security plugin layer for LLM-powered applications that detects, blocks, and reports adversarial prompts, jailbreaks, and abuse patterns with SOC-style visibility.**

---
Expand Down
Loading
Loading