Spaces:
Sleeping
Sleeping
Deploy code agent from GitHub Actions
Browse files
app.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
-
from pydantic import BaseModel
|
| 3 |
from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig
|
|
|
|
| 4 |
import os
|
| 5 |
import torch
|
| 6 |
import re
|
|
|
|
| 7 |
|
| 8 |
HF_MODEL = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-1.5B-Instruct")
|
| 9 |
|
|
@@ -26,6 +28,56 @@ class CodeRepairRequest(BaseModel):
|
|
| 26 |
exit_code: int | None = None
|
| 27 |
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
@app.get("/")
|
| 30 |
def health_check():
|
| 31 |
return {
|
|
@@ -33,7 +85,9 @@ def health_check():
|
|
| 33 |
"status": "running",
|
| 34 |
"llm_enabled": True,
|
| 35 |
"llm_mode": "local-transformers",
|
| 36 |
-
"model": HF_MODEL
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
|
|
@@ -383,25 +437,30 @@ Keep the answer concise.
|
|
| 383 |
"""
|
| 384 |
|
| 385 |
|
| 386 |
-
def call_llm(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
active_tokenizer, active_model = load_model()
|
| 388 |
|
| 389 |
messages = [
|
| 390 |
{
|
| 391 |
"role": "system",
|
| 392 |
-
"content":
|
| 393 |
},
|
| 394 |
{
|
| 395 |
"role": "user",
|
| 396 |
-
"content": prompt
|
| 397 |
-
}
|
| 398 |
]
|
| 399 |
|
| 400 |
if hasattr(active_tokenizer, "apply_chat_template"):
|
| 401 |
formatted_prompt = active_tokenizer.apply_chat_template(
|
| 402 |
messages,
|
| 403 |
tokenize=False,
|
| 404 |
-
add_generation_prompt=True
|
| 405 |
)
|
| 406 |
else:
|
| 407 |
formatted_prompt = prompt
|
|
@@ -410,14 +469,14 @@ def call_llm(prompt: str):
|
|
| 410 |
formatted_prompt,
|
| 411 |
return_tensors="pt",
|
| 412 |
truncation=True,
|
| 413 |
-
max_length=
|
| 414 |
)
|
| 415 |
|
| 416 |
outputs = active_model.generate(
|
| 417 |
**inputs,
|
| 418 |
-
max_new_tokens=
|
| 419 |
do_sample=False,
|
| 420 |
-
pad_token_id=active_tokenizer.eos_token_id
|
| 421 |
)
|
| 422 |
|
| 423 |
generated_text = active_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
@@ -542,6 +601,809 @@ def clean_output(text: str, request: CodeRepairRequest):
|
|
| 542 |
|
| 543 |
return cleaned
|
| 544 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
|
| 546 |
@app.post("/suggest-code-fix")
|
| 547 |
def suggest_code_fix(request: CodeRepairRequest):
|
|
@@ -582,4 +1444,4 @@ def suggest_code_fix(request: CodeRepairRequest):
|
|
| 582 |
|
| 583 |
except Exception as error:
|
| 584 |
fallback_result["llm_error"] = repr(error)
|
| 585 |
-
return fallback_result
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig
|
| 4 |
+
import ast
|
| 5 |
import os
|
| 6 |
import torch
|
| 7 |
import re
|
| 8 |
+
from collections import Counter
|
| 9 |
|
| 10 |
HF_MODEL = os.getenv("HF_MODEL", "Qwen/Qwen2.5-Coder-1.5B-Instruct")
|
| 11 |
|
|
|
|
| 28 |
exit_code: int | None = None
|
| 29 |
|
| 30 |
|
| 31 |
+
class SourceFileInput(BaseModel):
|
| 32 |
+
path: str
|
| 33 |
+
content: str
|
| 34 |
+
truncated: bool = False
|
| 35 |
+
original_chars: int = 0
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class SourceReviewRequest(BaseModel):
|
| 39 |
+
project_type: str
|
| 40 |
+
has_tests: bool = False
|
| 41 |
+
files: list[SourceFileInput] = Field(default_factory=list)
|
| 42 |
+
discovered_files_count: int = 0
|
| 43 |
+
submitted_files_count: int = 0
|
| 44 |
+
submitted_chars: int = 0
|
| 45 |
+
truncated_files_count: int = 0
|
| 46 |
+
omitted_files_count: int = 0
|
| 47 |
+
read_error_files_count: int = 0
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class SourceFinding(BaseModel):
|
| 51 |
+
id: str
|
| 52 |
+
file_path: str | None = None
|
| 53 |
+
line: int | None = None
|
| 54 |
+
category: str
|
| 55 |
+
severity: str
|
| 56 |
+
confidence: str
|
| 57 |
+
title: str
|
| 58 |
+
evidence: str
|
| 59 |
+
impact: str
|
| 60 |
+
recommendation: str
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class SourceReviewResponse(BaseModel):
|
| 64 |
+
agent: str
|
| 65 |
+
mode: str
|
| 66 |
+
status: str
|
| 67 |
+
summary: str
|
| 68 |
+
risk_level: str
|
| 69 |
+
release_recommendation: str
|
| 70 |
+
reviewed_files_count: int
|
| 71 |
+
findings_count: int
|
| 72 |
+
severity_summary: dict[str, int]
|
| 73 |
+
category_summary: dict[str, int]
|
| 74 |
+
findings: list[SourceFinding] = Field(default_factory=list)
|
| 75 |
+
warnings: list[str] = Field(default_factory=list)
|
| 76 |
+
limitations: list[str] = Field(default_factory=list)
|
| 77 |
+
verification: str
|
| 78 |
+
llm_error: str | None = None
|
| 79 |
+
|
| 80 |
+
|
| 81 |
@app.get("/")
|
| 82 |
def health_check():
|
| 83 |
return {
|
|
|
|
| 85 |
"status": "running",
|
| 86 |
"llm_enabled": True,
|
| 87 |
"llm_mode": "local-transformers",
|
| 88 |
+
"model": HF_MODEL,
|
| 89 |
+
"capabilities": ["source-review", "code-repair"],
|
| 90 |
+
"endpoints": ["/review-source", "/suggest-code-fix"],
|
| 91 |
}
|
| 92 |
|
| 93 |
|
|
|
|
| 437 |
"""
|
| 438 |
|
| 439 |
|
| 440 |
+
def call_llm(
|
| 441 |
+
prompt: str,
|
| 442 |
+
system_prompt: str = "You are a careful code repair assistant. Return only the final repair guidance.",
|
| 443 |
+
max_input_tokens: int = 1024,
|
| 444 |
+
max_new_tokens: int = 256,
|
| 445 |
+
):
|
| 446 |
active_tokenizer, active_model = load_model()
|
| 447 |
|
| 448 |
messages = [
|
| 449 |
{
|
| 450 |
"role": "system",
|
| 451 |
+
"content": system_prompt,
|
| 452 |
},
|
| 453 |
{
|
| 454 |
"role": "user",
|
| 455 |
+
"content": prompt,
|
| 456 |
+
},
|
| 457 |
]
|
| 458 |
|
| 459 |
if hasattr(active_tokenizer, "apply_chat_template"):
|
| 460 |
formatted_prompt = active_tokenizer.apply_chat_template(
|
| 461 |
messages,
|
| 462 |
tokenize=False,
|
| 463 |
+
add_generation_prompt=True,
|
| 464 |
)
|
| 465 |
else:
|
| 466 |
formatted_prompt = prompt
|
|
|
|
| 469 |
formatted_prompt,
|
| 470 |
return_tensors="pt",
|
| 471 |
truncation=True,
|
| 472 |
+
max_length=max_input_tokens,
|
| 473 |
)
|
| 474 |
|
| 475 |
outputs = active_model.generate(
|
| 476 |
**inputs,
|
| 477 |
+
max_new_tokens=max_new_tokens,
|
| 478 |
do_sample=False,
|
| 479 |
+
pad_token_id=active_tokenizer.eos_token_id,
|
| 480 |
)
|
| 481 |
|
| 482 |
generated_text = active_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
|
|
| 601 |
|
| 602 |
return cleaned
|
| 603 |
|
| 604 |
+
SEVERITY_ORDER = {
|
| 605 |
+
"CRITICAL": 0,
|
| 606 |
+
"HIGH": 1,
|
| 607 |
+
"MEDIUM": 2,
|
| 608 |
+
"LOW": 3,
|
| 609 |
+
"INFO": 4,
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
SECRET_NAME_PATTERN = re.compile(
|
| 613 |
+
r"(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|access[_-]?key)",
|
| 614 |
+
re.IGNORECASE,
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
PLACEHOLDER_SECRET_VALUES = {
|
| 618 |
+
"",
|
| 619 |
+
"change-me",
|
| 620 |
+
"changeme",
|
| 621 |
+
"example",
|
| 622 |
+
"placeholder",
|
| 623 |
+
"secret",
|
| 624 |
+
"password",
|
| 625 |
+
"token",
|
| 626 |
+
"your-secret",
|
| 627 |
+
"your-token",
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def dotted_name(node):
|
| 632 |
+
if isinstance(node, ast.Name):
|
| 633 |
+
return node.id
|
| 634 |
+
|
| 635 |
+
if isinstance(node, ast.Attribute):
|
| 636 |
+
parent = dotted_name(node.value)
|
| 637 |
+
return f"{parent}.{node.attr}" if parent else node.attr
|
| 638 |
+
|
| 639 |
+
return ""
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
def line_evidence(content, line_number, fallback):
|
| 643 |
+
lines = content.splitlines()
|
| 644 |
+
|
| 645 |
+
if line_number and 0 < line_number <= len(lines):
|
| 646 |
+
value = lines[line_number - 1].strip()
|
| 647 |
+
if value:
|
| 648 |
+
return value[:240]
|
| 649 |
+
|
| 650 |
+
return fallback
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def is_placeholder_secret(value):
|
| 654 |
+
normalized = str(value).strip().lower()
|
| 655 |
+
|
| 656 |
+
if normalized in PLACEHOLDER_SECRET_VALUES:
|
| 657 |
+
return True
|
| 658 |
+
|
| 659 |
+
return any(
|
| 660 |
+
marker in normalized
|
| 661 |
+
for marker in ("${", "{{", "env.", "process.env", "os.getenv", "system.getenv")
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def assignment_target_names(node):
|
| 666 |
+
names = []
|
| 667 |
+
|
| 668 |
+
if isinstance(node, ast.Name):
|
| 669 |
+
names.append(node.id)
|
| 670 |
+
elif isinstance(node, (ast.Tuple, ast.List)):
|
| 671 |
+
for item in node.elts:
|
| 672 |
+
names.extend(assignment_target_names(item))
|
| 673 |
+
elif isinstance(node, ast.Attribute):
|
| 674 |
+
names.append(node.attr)
|
| 675 |
+
|
| 676 |
+
return names
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
def keyword_value(call, keyword_name):
|
| 680 |
+
for keyword in call.keywords:
|
| 681 |
+
if keyword.arg == keyword_name:
|
| 682 |
+
return keyword.value
|
| 683 |
+
|
| 684 |
+
return None
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
def is_true_literal(node):
|
| 688 |
+
return isinstance(node, ast.Constant) and node.value is True
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def is_false_literal(node):
|
| 692 |
+
return isinstance(node, ast.Constant) and node.value is False
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
def add_finding(
|
| 696 |
+
findings,
|
| 697 |
+
file_path,
|
| 698 |
+
line,
|
| 699 |
+
category,
|
| 700 |
+
severity,
|
| 701 |
+
confidence,
|
| 702 |
+
title,
|
| 703 |
+
evidence,
|
| 704 |
+
impact,
|
| 705 |
+
recommendation,
|
| 706 |
+
):
|
| 707 |
+
findings.append(
|
| 708 |
+
{
|
| 709 |
+
"file_path": file_path,
|
| 710 |
+
"line": line,
|
| 711 |
+
"category": category,
|
| 712 |
+
"severity": severity,
|
| 713 |
+
"confidence": confidence,
|
| 714 |
+
"title": title,
|
| 715 |
+
"evidence": evidence,
|
| 716 |
+
"impact": impact,
|
| 717 |
+
"recommendation": recommendation,
|
| 718 |
+
}
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def analyze_python_source(source_file, findings, warnings):
|
| 723 |
+
path = source_file.path
|
| 724 |
+
content = source_file.content
|
| 725 |
+
|
| 726 |
+
try:
|
| 727 |
+
tree = ast.parse(content, filename=path)
|
| 728 |
+
except SyntaxError as error:
|
| 729 |
+
if source_file.truncated:
|
| 730 |
+
warnings.append(
|
| 731 |
+
f"{path} could not be fully parsed because the submitted content was truncated."
|
| 732 |
+
)
|
| 733 |
+
return
|
| 734 |
+
|
| 735 |
+
add_finding(
|
| 736 |
+
findings,
|
| 737 |
+
path,
|
| 738 |
+
error.lineno,
|
| 739 |
+
"correctness",
|
| 740 |
+
"HIGH",
|
| 741 |
+
"HIGH",
|
| 742 |
+
"Python syntax error",
|
| 743 |
+
error.msg,
|
| 744 |
+
"The module cannot be imported or executed successfully.",
|
| 745 |
+
"Correct the syntax error and rerun Stitch QA before deployment.",
|
| 746 |
+
)
|
| 747 |
+
return
|
| 748 |
+
|
| 749 |
+
for node in ast.walk(tree):
|
| 750 |
+
if isinstance(node, (ast.Assign, ast.AnnAssign)):
|
| 751 |
+
value = node.value
|
| 752 |
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
| 753 |
+
names = []
|
| 754 |
+
|
| 755 |
+
for target in targets:
|
| 756 |
+
names.extend(assignment_target_names(target))
|
| 757 |
+
|
| 758 |
+
if (
|
| 759 |
+
isinstance(value, ast.Constant)
|
| 760 |
+
and isinstance(value.value, str)
|
| 761 |
+
and len(value.value.strip()) >= 6
|
| 762 |
+
and any(SECRET_NAME_PATTERN.search(name) for name in names)
|
| 763 |
+
and not is_placeholder_secret(value.value)
|
| 764 |
+
):
|
| 765 |
+
add_finding(
|
| 766 |
+
findings,
|
| 767 |
+
path,
|
| 768 |
+
getattr(node, "lineno", None),
|
| 769 |
+
"security",
|
| 770 |
+
"HIGH",
|
| 771 |
+
"HIGH",
|
| 772 |
+
"Possible hardcoded credential",
|
| 773 |
+
"A credential-like variable is assigned a literal string value.",
|
| 774 |
+
"Hardcoded credentials can be exposed through source control, logs, or package distribution.",
|
| 775 |
+
"Move the value to a secret manager or environment variable and rotate any exposed credential.",
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 779 |
+
defaults = list(node.args.defaults) + [
|
| 780 |
+
item for item in node.args.kw_defaults if item is not None
|
| 781 |
+
]
|
| 782 |
+
|
| 783 |
+
if any(isinstance(default, (ast.List, ast.Dict, ast.Set)) for default in defaults):
|
| 784 |
+
add_finding(
|
| 785 |
+
findings,
|
| 786 |
+
path,
|
| 787 |
+
getattr(node, "lineno", None),
|
| 788 |
+
"reliability",
|
| 789 |
+
"MEDIUM",
|
| 790 |
+
"HIGH",
|
| 791 |
+
"Mutable function default",
|
| 792 |
+
f"Function `{node.name}` uses a mutable default argument.",
|
| 793 |
+
"State can leak between calls and create difficult-to-reproduce defects.",
|
| 794 |
+
"Use None as the default and create the mutable object inside the function.",
|
| 795 |
+
)
|
| 796 |
+
|
| 797 |
+
if isinstance(node, ast.ExceptHandler):
|
| 798 |
+
if node.type is None:
|
| 799 |
+
add_finding(
|
| 800 |
+
findings,
|
| 801 |
+
path,
|
| 802 |
+
getattr(node, "lineno", None),
|
| 803 |
+
"reliability",
|
| 804 |
+
"MEDIUM",
|
| 805 |
+
"HIGH",
|
| 806 |
+
"Bare exception handler",
|
| 807 |
+
line_evidence(content, getattr(node, "lineno", None), "A bare except handler was detected."),
|
| 808 |
+
"SystemExit, KeyboardInterrupt, and unexpected defects can be swallowed.",
|
| 809 |
+
"Catch only the expected exception types and preserve actionable error context.",
|
| 810 |
+
)
|
| 811 |
+
|
| 812 |
+
if node.body and all(isinstance(item, ast.Pass) for item in node.body):
|
| 813 |
+
add_finding(
|
| 814 |
+
findings,
|
| 815 |
+
path,
|
| 816 |
+
getattr(node, "lineno", None),
|
| 817 |
+
"error-handling",
|
| 818 |
+
"MEDIUM",
|
| 819 |
+
"HIGH",
|
| 820 |
+
"Exception silently ignored",
|
| 821 |
+
line_evidence(content, getattr(node, "lineno", None), "An exception handler contains only pass."),
|
| 822 |
+
"Failures can be hidden and leave the application in an unknown state.",
|
| 823 |
+
"Handle the exception explicitly or log and re-raise it with safe context.",
|
| 824 |
+
)
|
| 825 |
+
|
| 826 |
+
if not isinstance(node, ast.Call):
|
| 827 |
+
continue
|
| 828 |
+
|
| 829 |
+
function_name = dotted_name(node.func)
|
| 830 |
+
line = getattr(node, "lineno", None)
|
| 831 |
+
evidence = line_evidence(content, line, function_name)
|
| 832 |
+
|
| 833 |
+
if function_name in {"eval", "exec", "builtins.eval", "builtins.exec"}:
|
| 834 |
+
add_finding(
|
| 835 |
+
findings,
|
| 836 |
+
path,
|
| 837 |
+
line,
|
| 838 |
+
"security",
|
| 839 |
+
"HIGH",
|
| 840 |
+
"HIGH",
|
| 841 |
+
"Dynamic code execution",
|
| 842 |
+
evidence,
|
| 843 |
+
"Untrusted input can lead to arbitrary code execution.",
|
| 844 |
+
"Remove dynamic execution or replace it with a strict parser and an allowlisted operation set.",
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
if function_name == "os.system":
|
| 848 |
+
add_finding(
|
| 849 |
+
findings,
|
| 850 |
+
path,
|
| 851 |
+
line,
|
| 852 |
+
"security",
|
| 853 |
+
"HIGH",
|
| 854 |
+
"HIGH",
|
| 855 |
+
"Shell command execution through os.system",
|
| 856 |
+
evidence,
|
| 857 |
+
"User-controlled values can create operating-system command injection.",
|
| 858 |
+
"Use subprocess with an argument list, shell disabled, validated inputs, and least privilege.",
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
if function_name.startswith("subprocess.") and is_true_literal(
|
| 862 |
+
keyword_value(node, "shell")
|
| 863 |
+
):
|
| 864 |
+
add_finding(
|
| 865 |
+
findings,
|
| 866 |
+
path,
|
| 867 |
+
line,
|
| 868 |
+
"security",
|
| 869 |
+
"HIGH",
|
| 870 |
+
"HIGH",
|
| 871 |
+
"Subprocess executed with shell enabled",
|
| 872 |
+
evidence,
|
| 873 |
+
"Shell interpretation increases command-injection risk.",
|
| 874 |
+
"Disable shell execution and pass validated command arguments as a list.",
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
if function_name in {"pickle.load", "pickle.loads"}:
|
| 878 |
+
add_finding(
|
| 879 |
+
findings,
|
| 880 |
+
path,
|
| 881 |
+
line,
|
| 882 |
+
"security",
|
| 883 |
+
"HIGH",
|
| 884 |
+
"HIGH",
|
| 885 |
+
"Unsafe pickle deserialization",
|
| 886 |
+
evidence,
|
| 887 |
+
"Loading attacker-controlled pickle data can execute arbitrary code.",
|
| 888 |
+
"Use a non-executable serialization format and validate data before processing it.",
|
| 889 |
+
)
|
| 890 |
+
|
| 891 |
+
if function_name == "yaml.load":
|
| 892 |
+
loader = keyword_value(node, "Loader")
|
| 893 |
+
loader_name = dotted_name(loader) if loader is not None else ""
|
| 894 |
+
|
| 895 |
+
if loader_name not in {"yaml.SafeLoader", "yaml.CSafeLoader", "SafeLoader", "CSafeLoader"}:
|
| 896 |
+
add_finding(
|
| 897 |
+
findings,
|
| 898 |
+
path,
|
| 899 |
+
line,
|
| 900 |
+
"security",
|
| 901 |
+
"HIGH",
|
| 902 |
+
"HIGH",
|
| 903 |
+
"Unsafe YAML loading",
|
| 904 |
+
evidence,
|
| 905 |
+
"Unsafe YAML constructors can instantiate arbitrary Python objects.",
|
| 906 |
+
"Use yaml.safe_load or explicitly select SafeLoader.",
|
| 907 |
+
)
|
| 908 |
+
|
| 909 |
+
if function_name.startswith("requests.") and is_false_literal(
|
| 910 |
+
keyword_value(node, "verify")
|
| 911 |
+
):
|
| 912 |
+
add_finding(
|
| 913 |
+
findings,
|
| 914 |
+
path,
|
| 915 |
+
line,
|
| 916 |
+
"security",
|
| 917 |
+
"HIGH",
|
| 918 |
+
"HIGH",
|
| 919 |
+
"TLS certificate verification disabled",
|
| 920 |
+
evidence,
|
| 921 |
+
"Network traffic can be intercepted by an attacker presenting an untrusted certificate.",
|
| 922 |
+
"Enable certificate verification and configure a trusted CA bundle when required.",
|
| 923 |
+
)
|
| 924 |
+
|
| 925 |
+
if function_name in {"hashlib.md5", "hashlib.sha1"}:
|
| 926 |
+
add_finding(
|
| 927 |
+
findings,
|
| 928 |
+
path,
|
| 929 |
+
line,
|
| 930 |
+
"security",
|
| 931 |
+
"MEDIUM",
|
| 932 |
+
"MEDIUM",
|
| 933 |
+
"Weak cryptographic hash",
|
| 934 |
+
evidence,
|
| 935 |
+
"MD5 and SHA-1 are unsuitable for security-sensitive integrity or credential protection.",
|
| 936 |
+
"Use a modern hash such as SHA-256, or a password-specific algorithm such as Argon2 or bcrypt.",
|
| 937 |
+
)
|
| 938 |
+
|
| 939 |
+
if function_name.endswith(".run") and is_true_literal(keyword_value(node, "debug")):
|
| 940 |
+
add_finding(
|
| 941 |
+
findings,
|
| 942 |
+
path,
|
| 943 |
+
line,
|
| 944 |
+
"security",
|
| 945 |
+
"MEDIUM",
|
| 946 |
+
"MEDIUM",
|
| 947 |
+
"Debug mode explicitly enabled",
|
| 948 |
+
evidence,
|
| 949 |
+
"Production debug mode can expose sensitive stack traces or interactive debugging features.",
|
| 950 |
+
"Disable debug mode in deployment and control it through environment-specific configuration.",
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
if len(content.splitlines()) > 800:
|
| 954 |
+
add_finding(
|
| 955 |
+
findings,
|
| 956 |
+
path,
|
| 957 |
+
1,
|
| 958 |
+
"maintainability",
|
| 959 |
+
"LOW",
|
| 960 |
+
"MEDIUM",
|
| 961 |
+
"Large source file",
|
| 962 |
+
f"The submitted file contains {len(content.splitlines())} lines.",
|
| 963 |
+
"Very large modules are harder to review, test, and maintain safely.",
|
| 964 |
+
"Split unrelated responsibilities into smaller cohesive modules while preserving behavior.",
|
| 965 |
+
)
|
| 966 |
+
|
| 967 |
+
|
| 968 |
+
def analyze_java_source(source_file, findings):
|
| 969 |
+
path = source_file.path
|
| 970 |
+
content = source_file.content
|
| 971 |
+
lines = content.splitlines()
|
| 972 |
+
|
| 973 |
+
for index, raw_line in enumerate(lines, start=1):
|
| 974 |
+
line = raw_line.strip()
|
| 975 |
+
secret_match = re.search(
|
| 976 |
+
r'(?i)\b(?:String\s+)?([A-Za-z0-9_]*(?:password|passwd|secret|token|apiKey|api_key|privateKey|accessKey)[A-Za-z0-9_]*)\s*=\s*"([^"]{6,})"',
|
| 977 |
+
line,
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
+
if secret_match and not is_placeholder_secret(secret_match.group(2)):
|
| 981 |
+
add_finding(
|
| 982 |
+
findings,
|
| 983 |
+
path,
|
| 984 |
+
index,
|
| 985 |
+
"security",
|
| 986 |
+
"HIGH",
|
| 987 |
+
"HIGH",
|
| 988 |
+
"Possible hardcoded credential",
|
| 989 |
+
"A credential-like Java variable is assigned a literal string value.",
|
| 990 |
+
"The credential can be exposed through source control, build artifacts, or logs.",
|
| 991 |
+
"Load the value from a protected secret store or environment variable and rotate exposed credentials.",
|
| 992 |
+
)
|
| 993 |
+
|
| 994 |
+
if "Runtime.getRuntime().exec" in line:
|
| 995 |
+
add_finding(
|
| 996 |
+
findings,
|
| 997 |
+
path,
|
| 998 |
+
index,
|
| 999 |
+
"security",
|
| 1000 |
+
"HIGH",
|
| 1001 |
+
"HIGH",
|
| 1002 |
+
"Runtime command execution",
|
| 1003 |
+
line[:240],
|
| 1004 |
+
"Unvalidated command content can lead to operating-system command injection.",
|
| 1005 |
+
"Use a fixed command with validated arguments and avoid passing user-controlled shell content.",
|
| 1006 |
+
)
|
| 1007 |
+
|
| 1008 |
+
if "new ProcessBuilder" in line:
|
| 1009 |
+
add_finding(
|
| 1010 |
+
findings,
|
| 1011 |
+
path,
|
| 1012 |
+
index,
|
| 1013 |
+
"security",
|
| 1014 |
+
"MEDIUM",
|
| 1015 |
+
"MEDIUM",
|
| 1016 |
+
"External process creation requires validation",
|
| 1017 |
+
line[:240],
|
| 1018 |
+
"Untrusted process arguments can create command execution or privilege risks.",
|
| 1019 |
+
"Use allowlisted commands, validated argument arrays, controlled working directories, and least privilege.",
|
| 1020 |
+
)
|
| 1021 |
+
|
| 1022 |
+
if re.search(r"\bexecute(?:Query|Update)?\s*\([^)]*\+", line):
|
| 1023 |
+
add_finding(
|
| 1024 |
+
findings,
|
| 1025 |
+
path,
|
| 1026 |
+
index,
|
| 1027 |
+
"security",
|
| 1028 |
+
"HIGH",
|
| 1029 |
+
"MEDIUM",
|
| 1030 |
+
"Possible SQL query concatenation",
|
| 1031 |
+
line[:240],
|
| 1032 |
+
"Concatenated untrusted values can allow SQL injection.",
|
| 1033 |
+
"Use PreparedStatement parameters or an ORM parameter-binding API.",
|
| 1034 |
+
)
|
| 1035 |
+
|
| 1036 |
+
if re.search(r'MessageDigest\.getInstance\(\s*"(?:MD5|SHA-1)"', line, re.IGNORECASE):
|
| 1037 |
+
add_finding(
|
| 1038 |
+
findings,
|
| 1039 |
+
path,
|
| 1040 |
+
index,
|
| 1041 |
+
"security",
|
| 1042 |
+
"MEDIUM",
|
| 1043 |
+
"HIGH",
|
| 1044 |
+
"Weak cryptographic hash",
|
| 1045 |
+
line[:240],
|
| 1046 |
+
"MD5 and SHA-1 are unsuitable for security-sensitive integrity or credential protection.",
|
| 1047 |
+
"Use SHA-256 or stronger, and use a password-specific algorithm for password storage.",
|
| 1048 |
+
)
|
| 1049 |
+
|
| 1050 |
+
if ".printStackTrace()" in line:
|
| 1051 |
+
add_finding(
|
| 1052 |
+
findings,
|
| 1053 |
+
path,
|
| 1054 |
+
index,
|
| 1055 |
+
"error-handling",
|
| 1056 |
+
"LOW",
|
| 1057 |
+
"HIGH",
|
| 1058 |
+
"Stack trace written directly",
|
| 1059 |
+
line[:240],
|
| 1060 |
+
"Raw stack traces can expose implementation details and bypass structured logging.",
|
| 1061 |
+
"Use the project logger with controlled context and avoid exposing sensitive details to clients.",
|
| 1062 |
+
)
|
| 1063 |
+
|
| 1064 |
+
if "System.out.print" in line or "System.err.print" in line:
|
| 1065 |
+
add_finding(
|
| 1066 |
+
findings,
|
| 1067 |
+
path,
|
| 1068 |
+
index,
|
| 1069 |
+
"maintainability",
|
| 1070 |
+
"LOW",
|
| 1071 |
+
"HIGH",
|
| 1072 |
+
"Direct console output in application code",
|
| 1073 |
+
line[:240],
|
| 1074 |
+
"Console output is difficult to classify, filter, and correlate in production.",
|
| 1075 |
+
"Use structured application logging with an appropriate log level.",
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
if re.search(r"@CrossOrigin\s*\([^)]*\*", line) or re.search(
|
| 1079 |
+
r"allowedOrigins?\s*\([^)]*\*", line, re.IGNORECASE
|
| 1080 |
+
):
|
| 1081 |
+
add_finding(
|
| 1082 |
+
findings,
|
| 1083 |
+
path,
|
| 1084 |
+
index,
|
| 1085 |
+
"security",
|
| 1086 |
+
"MEDIUM",
|
| 1087 |
+
"HIGH",
|
| 1088 |
+
"Wildcard cross-origin access",
|
| 1089 |
+
line[:240],
|
| 1090 |
+
"Unrestricted origins can expose APIs to untrusted web applications.",
|
| 1091 |
+
"Allow only explicitly trusted origins and review credential-sharing settings.",
|
| 1092 |
+
)
|
| 1093 |
+
|
| 1094 |
+
if re.search(r"HostnameVerifier.*->\s*true", line) or "ALLOW_ALL_HOSTNAME_VERIFIER" in line:
|
| 1095 |
+
add_finding(
|
| 1096 |
+
findings,
|
| 1097 |
+
path,
|
| 1098 |
+
index,
|
| 1099 |
+
"security",
|
| 1100 |
+
"CRITICAL",
|
| 1101 |
+
"HIGH",
|
| 1102 |
+
"TLS hostname verification bypass",
|
| 1103 |
+
line[:240],
|
| 1104 |
+
"An attacker can impersonate a remote service even when TLS is used.",
|
| 1105 |
+
"Remove the permissive verifier and use the platform default hostname and certificate validation.",
|
| 1106 |
+
)
|
| 1107 |
+
|
| 1108 |
+
for match in re.finditer(r"catch\s*\([^)]*\)\s*\{\s*\}", content, re.DOTALL):
|
| 1109 |
+
line = content.count("\n", 0, match.start()) + 1
|
| 1110 |
+
add_finding(
|
| 1111 |
+
findings,
|
| 1112 |
+
path,
|
| 1113 |
+
line,
|
| 1114 |
+
"error-handling",
|
| 1115 |
+
"MEDIUM",
|
| 1116 |
+
"HIGH",
|
| 1117 |
+
"Empty Java catch block",
|
| 1118 |
+
"A catch block contains no handling logic.",
|
| 1119 |
+
"The application can hide failures and continue with invalid state.",
|
| 1120 |
+
"Handle the expected exception, preserve safe diagnostic context, or rethrow it appropriately.",
|
| 1121 |
+
)
|
| 1122 |
+
|
| 1123 |
+
if len(lines) > 800:
|
| 1124 |
+
add_finding(
|
| 1125 |
+
findings,
|
| 1126 |
+
path,
|
| 1127 |
+
1,
|
| 1128 |
+
"maintainability",
|
| 1129 |
+
"LOW",
|
| 1130 |
+
"MEDIUM",
|
| 1131 |
+
"Large source file",
|
| 1132 |
+
f"The submitted file contains {len(lines)} lines.",
|
| 1133 |
+
"Very large classes are harder to review, test, and maintain safely.",
|
| 1134 |
+
"Separate unrelated responsibilities into smaller cohesive classes while preserving behavior.",
|
| 1135 |
+
)
|
| 1136 |
+
|
| 1137 |
+
|
| 1138 |
+
def deduplicate_findings(findings):
|
| 1139 |
+
unique = {}
|
| 1140 |
+
|
| 1141 |
+
for finding in findings:
|
| 1142 |
+
key = (
|
| 1143 |
+
finding.get("file_path"),
|
| 1144 |
+
finding.get("line"),
|
| 1145 |
+
finding.get("title"),
|
| 1146 |
+
)
|
| 1147 |
+
unique[key] = finding
|
| 1148 |
+
|
| 1149 |
+
ordered = sorted(
|
| 1150 |
+
unique.values(),
|
| 1151 |
+
key=lambda item: (
|
| 1152 |
+
SEVERITY_ORDER.get(item.get("severity"), 99),
|
| 1153 |
+
str(item.get("file_path") or ""),
|
| 1154 |
+
item.get("line") or 0,
|
| 1155 |
+
item.get("title") or "",
|
| 1156 |
+
),
|
| 1157 |
+
)
|
| 1158 |
+
|
| 1159 |
+
for index, finding in enumerate(ordered, start=1):
|
| 1160 |
+
finding["id"] = f"SQ-SRC-{index:03d}"
|
| 1161 |
+
|
| 1162 |
+
return ordered
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
def build_severity_summary(findings):
|
| 1166 |
+
summary = {
|
| 1167 |
+
"CRITICAL": 0,
|
| 1168 |
+
"HIGH": 0,
|
| 1169 |
+
"MEDIUM": 0,
|
| 1170 |
+
"LOW": 0,
|
| 1171 |
+
"INFO": 0,
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
for finding in findings:
|
| 1175 |
+
severity = finding.get("severity", "INFO")
|
| 1176 |
+
summary[severity] = summary.get(severity, 0) + 1
|
| 1177 |
+
|
| 1178 |
+
return summary
|
| 1179 |
+
|
| 1180 |
+
|
| 1181 |
+
def build_category_summary(findings):
|
| 1182 |
+
return dict(sorted(Counter(finding.get("category", "other") for finding in findings).items()))
|
| 1183 |
+
|
| 1184 |
+
|
| 1185 |
+
def build_risk_level(severity_summary):
|
| 1186 |
+
for severity in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
|
| 1187 |
+
if severity_summary.get(severity, 0):
|
| 1188 |
+
return severity
|
| 1189 |
+
|
| 1190 |
+
return "LOW"
|
| 1191 |
+
|
| 1192 |
+
|
| 1193 |
+
def build_release_recommendation(risk_level, partial):
|
| 1194 |
+
if risk_level in {"CRITICAL", "HIGH"}:
|
| 1195 |
+
return "BLOCK_RELEASE"
|
| 1196 |
+
|
| 1197 |
+
if risk_level == "MEDIUM":
|
| 1198 |
+
return "REVIEW_REQUIRED"
|
| 1199 |
+
|
| 1200 |
+
if partial:
|
| 1201 |
+
return "MANUAL_REVIEW_REQUIRED"
|
| 1202 |
+
|
| 1203 |
+
return "READY_WITH_CAUTION"
|
| 1204 |
+
|
| 1205 |
+
|
| 1206 |
+
def fallback_source_summary(request, findings, risk_level, release_recommendation):
|
| 1207 |
+
if not findings:
|
| 1208 |
+
return (
|
| 1209 |
+
f"Agent 3 reviewed {len(request.files)} application source files and did not identify "
|
| 1210 |
+
"a blocking issue with the implemented checks. This result does not replace manual, "
|
| 1211 |
+
"dependency, architecture, or runtime security review."
|
| 1212 |
+
)
|
| 1213 |
+
|
| 1214 |
+
severity_summary = build_severity_summary(findings)
|
| 1215 |
+
return (
|
| 1216 |
+
f"Agent 3 reviewed {len(request.files)} application source files and reported "
|
| 1217 |
+
f"{len(findings)} findings. Highest risk is {risk_level}. "
|
| 1218 |
+
f"Critical: {severity_summary.get('CRITICAL', 0)}, High: {severity_summary.get('HIGH', 0)}, "
|
| 1219 |
+
f"Medium: {severity_summary.get('MEDIUM', 0)}, Low: {severity_summary.get('LOW', 0)}. "
|
| 1220 |
+
f"Release recommendation: {release_recommendation}."
|
| 1221 |
+
)
|
| 1222 |
+
|
| 1223 |
+
|
| 1224 |
+
def build_source_summary_prompt(request, findings, risk_level, release_recommendation):
|
| 1225 |
+
finding_lines = []
|
| 1226 |
+
|
| 1227 |
+
for finding in findings[:20]:
|
| 1228 |
+
location = finding.get("file_path") or "Project-level"
|
| 1229 |
+
if finding.get("line"):
|
| 1230 |
+
location = f"{location}:{finding.get('line')}"
|
| 1231 |
+
finding_lines.append(
|
| 1232 |
+
f"- [{finding.get('severity')}] {finding.get('title')} at {location}: "
|
| 1233 |
+
f"{finding.get('impact')}"
|
| 1234 |
+
)
|
| 1235 |
+
|
| 1236 |
+
findings_text = "\n".join(finding_lines) or "- No findings from the implemented checks."
|
| 1237 |
+
|
| 1238 |
+
return f"""
|
| 1239 |
+
Create a concise professional pre-release source-code QA summary using only the supplied facts.
|
| 1240 |
+
|
| 1241 |
+
Project type: {request.project_type}
|
| 1242 |
+
Reviewed files: {len(request.files)}
|
| 1243 |
+
Has detected tests: {request.has_tests}
|
| 1244 |
+
Highest risk: {risk_level}
|
| 1245 |
+
Release recommendation: {release_recommendation}
|
| 1246 |
+
|
| 1247 |
+
Confirmed findings:
|
| 1248 |
+
{findings_text}
|
| 1249 |
+
|
| 1250 |
+
State what was reviewed, the most important confirmed risks, and the required next action.
|
| 1251 |
+
Do not invent findings, files, runtime results, or successful deployment claims.
|
| 1252 |
+
Return one short paragraph only.
|
| 1253 |
+
"""
|
| 1254 |
+
|
| 1255 |
+
|
| 1256 |
+
def clean_source_summary(text):
|
| 1257 |
+
cleaned = remove_prompt_leak(text)
|
| 1258 |
+
|
| 1259 |
+
if not cleaned:
|
| 1260 |
+
return None
|
| 1261 |
+
|
| 1262 |
+
cleaned = re.sub(r"\n{2,}", " ", cleaned).strip()
|
| 1263 |
+
|
| 1264 |
+
if len(cleaned) < 40:
|
| 1265 |
+
return None
|
| 1266 |
+
|
| 1267 |
+
blocked_phrases = [
|
| 1268 |
+
"create a concise professional",
|
| 1269 |
+
"confirmed findings:",
|
| 1270 |
+
"do not invent findings",
|
| 1271 |
+
"return one short paragraph",
|
| 1272 |
+
"system you are",
|
| 1273 |
+
"user you are",
|
| 1274 |
+
]
|
| 1275 |
+
|
| 1276 |
+
if any(phrase in cleaned.lower() for phrase in blocked_phrases):
|
| 1277 |
+
return None
|
| 1278 |
+
|
| 1279 |
+
return cleaned[:1600]
|
| 1280 |
+
|
| 1281 |
+
|
| 1282 |
+
def review_source_request(request: SourceReviewRequest):
|
| 1283 |
+
findings = []
|
| 1284 |
+
warnings = []
|
| 1285 |
+
limitations = [
|
| 1286 |
+
"This review uses targeted static checks and an AI summary; it does not prove the absence of defects or vulnerabilities.",
|
| 1287 |
+
"Dependency vulnerabilities, infrastructure configuration, business-rule correctness, and runtime behavior require separate evidence.",
|
| 1288 |
+
]
|
| 1289 |
+
|
| 1290 |
+
for source_file in request.files:
|
| 1291 |
+
suffix = os.path.splitext(source_file.path.lower())[1]
|
| 1292 |
+
|
| 1293 |
+
if suffix == ".py":
|
| 1294 |
+
analyze_python_source(source_file, findings, warnings)
|
| 1295 |
+
elif suffix == ".java":
|
| 1296 |
+
analyze_java_source(source_file, findings)
|
| 1297 |
+
else:
|
| 1298 |
+
warnings.append(f"Unsupported source extension skipped by Agent 3: {source_file.path}")
|
| 1299 |
+
|
| 1300 |
+
if source_file.truncated:
|
| 1301 |
+
warnings.append(
|
| 1302 |
+
f"{source_file.path} was truncated before review; findings may not cover the entire file."
|
| 1303 |
+
)
|
| 1304 |
+
|
| 1305 |
+
if not request.has_tests:
|
| 1306 |
+
add_finding(
|
| 1307 |
+
findings,
|
| 1308 |
+
None,
|
| 1309 |
+
None,
|
| 1310 |
+
"testing",
|
| 1311 |
+
"MEDIUM",
|
| 1312 |
+
"HIGH",
|
| 1313 |
+
"No automated tests detected",
|
| 1314 |
+
"The project scan did not detect a compatible automated test suite.",
|
| 1315 |
+
"Source review alone cannot verify runtime behavior, integrations, regressions, or business rules.",
|
| 1316 |
+
"Add focused automated tests for critical flows, validation, error paths, and security-sensitive behavior.",
|
| 1317 |
+
)
|
| 1318 |
+
|
| 1319 |
+
findings = deduplicate_findings(findings)
|
| 1320 |
+
severity_summary = build_severity_summary(findings)
|
| 1321 |
+
category_summary = build_category_summary(findings)
|
| 1322 |
+
risk_level = build_risk_level(severity_summary)
|
| 1323 |
+
partial = any(
|
| 1324 |
+
(
|
| 1325 |
+
request.truncated_files_count,
|
| 1326 |
+
request.omitted_files_count,
|
| 1327 |
+
request.read_error_files_count,
|
| 1328 |
+
)
|
| 1329 |
+
)
|
| 1330 |
+
release_recommendation = build_release_recommendation(risk_level, partial)
|
| 1331 |
+
|
| 1332 |
+
if request.omitted_files_count:
|
| 1333 |
+
limitations.append(
|
| 1334 |
+
f"{request.omitted_files_count} discovered source files were omitted by client-side review limits."
|
| 1335 |
+
)
|
| 1336 |
+
|
| 1337 |
+
if request.read_error_files_count:
|
| 1338 |
+
limitations.append(
|
| 1339 |
+
f"{request.read_error_files_count} source files could not be read by the CLI."
|
| 1340 |
+
)
|
| 1341 |
+
|
| 1342 |
+
if request.truncated_files_count:
|
| 1343 |
+
limitations.append(
|
| 1344 |
+
f"{request.truncated_files_count} source files were partially reviewed because of size limits."
|
| 1345 |
+
)
|
| 1346 |
+
|
| 1347 |
+
status = "PARTIAL" if partial else "COMPLETED"
|
| 1348 |
+
mode = "rule-based"
|
| 1349 |
+
llm_error = None
|
| 1350 |
+
summary = fallback_source_summary(
|
| 1351 |
+
request,
|
| 1352 |
+
findings,
|
| 1353 |
+
risk_level,
|
| 1354 |
+
release_recommendation,
|
| 1355 |
+
)
|
| 1356 |
+
|
| 1357 |
+
try:
|
| 1358 |
+
prompt = build_source_summary_prompt(
|
| 1359 |
+
request,
|
| 1360 |
+
findings,
|
| 1361 |
+
risk_level,
|
| 1362 |
+
release_recommendation,
|
| 1363 |
+
)
|
| 1364 |
+
llm_text = call_llm(
|
| 1365 |
+
prompt,
|
| 1366 |
+
system_prompt=(
|
| 1367 |
+
"You are Agent 3 of Stitch QA, a careful pre-release source-code QA reviewer. "
|
| 1368 |
+
"Summarize only confirmed supplied findings and never claim complete safety."
|
| 1369 |
+
),
|
| 1370 |
+
max_input_tokens=1536,
|
| 1371 |
+
max_new_tokens=220,
|
| 1372 |
+
)
|
| 1373 |
+
cleaned_summary = clean_source_summary(llm_text)
|
| 1374 |
+
|
| 1375 |
+
if cleaned_summary:
|
| 1376 |
+
summary = cleaned_summary
|
| 1377 |
+
mode = "hybrid"
|
| 1378 |
+
except Exception as error:
|
| 1379 |
+
llm_error = repr(error)
|
| 1380 |
+
|
| 1381 |
+
return {
|
| 1382 |
+
"agent": "code-agent",
|
| 1383 |
+
"mode": mode,
|
| 1384 |
+
"status": status,
|
| 1385 |
+
"summary": summary,
|
| 1386 |
+
"risk_level": risk_level,
|
| 1387 |
+
"release_recommendation": release_recommendation,
|
| 1388 |
+
"reviewed_files_count": len(request.files),
|
| 1389 |
+
"findings_count": len(findings),
|
| 1390 |
+
"severity_summary": severity_summary,
|
| 1391 |
+
"category_summary": category_summary,
|
| 1392 |
+
"findings": findings,
|
| 1393 |
+
"warnings": list(dict.fromkeys(warnings)),
|
| 1394 |
+
"limitations": limitations,
|
| 1395 |
+
"verification": (
|
| 1396 |
+
"Manually validate confirmed findings, apply the smallest safe fixes, add or run tests, "
|
| 1397 |
+
"and rerun Stitch QA before deployment."
|
| 1398 |
+
),
|
| 1399 |
+
"llm_error": llm_error,
|
| 1400 |
+
}
|
| 1401 |
+
|
| 1402 |
+
|
| 1403 |
+
@app.post("/review-source", response_model=SourceReviewResponse)
|
| 1404 |
+
def review_source(request: SourceReviewRequest):
|
| 1405 |
+
return review_source_request(request)
|
| 1406 |
+
|
| 1407 |
|
| 1408 |
@app.post("/suggest-code-fix")
|
| 1409 |
def suggest_code_fix(request: CodeRepairRequest):
|
|
|
|
| 1444 |
|
| 1445 |
except Exception as error:
|
| 1446 |
fallback_result["llm_error"] = repr(error)
|
| 1447 |
+
return fallback_result
|