Nitesh-Reddy commited on
Commit
9b70424
Β·
verified Β·
1 Parent(s): 461b246

Add file upload + landing page

Browse files
Files changed (2) hide show
  1. app.py +113 -56
  2. requirements.txt +1 -0
app.py CHANGED
@@ -2,8 +2,8 @@
2
  SecureHeal Agent β€” HuggingFace Space FastAPI Server
3
  ────────────────────────────────────────────────────
4
  Loads the trained model at startup, caches it, and exposes a FastAPI
5
- endpoint that takes application code β†’ runs the SecureHeal agent β†’
6
- finds vulnerabilities β†’ suggests fixes β†’ returns structured response.
7
 
8
  Deploy to HF Spaces with GPU (T4).
9
  """
@@ -13,26 +13,24 @@ import json
13
  import re
14
  import torch
15
  from contextlib import asynccontextmanager
16
- from fastapi import FastAPI, HTTPException
17
  from fastapi.middleware.cors import CORSMiddleware
 
18
  from pydantic import BaseModel
19
  from typing import Optional, List
20
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
21
 
22
  # ────────────────────── Model Cache ──────────────────────────
23
 
24
  MODEL_ID = os.environ.get("MODEL_ID", "Nitesh-Reddy/secureheal-agent-v2")
25
- PIPE = None # Global pipeline β€” loaded once at startup
26
 
27
 
28
  @asynccontextmanager
29
  async def lifespan(app: FastAPI):
30
- """Load model at startup, keep in memory for all requests."""
31
  global PIPE
32
  print(f"πŸ”„ Loading model: {MODEL_ID}")
33
- print(" This takes ~2 min on first load, then cached...")
34
-
35
- PIPE = pipeline(
36
  "text-generation",
37
  model=MODEL_ID,
38
  torch_dtype=torch.float16,
@@ -40,7 +38,6 @@ async def lifespan(app: FastAPI):
40
  )
41
  print(f"βœ… Model loaded and cached!")
42
  yield
43
- print("πŸ›‘ Shutting down...")
44
 
45
 
46
  # ────────────────────── FastAPI App ──────────────────────────
@@ -60,7 +57,7 @@ app.add_middleware(
60
  )
61
 
62
 
63
- # ────────────────────── Request/Response Models ──────────────
64
 
65
  class ScanRequest(BaseModel):
66
  code: str
@@ -75,7 +72,7 @@ class VulnerabilityReport(BaseModel):
75
  vulnerabilities_found: bool
76
  tool_calls: List[ToolCall]
77
  analysis: str
78
- raw_output: str
79
 
80
  class AgentRequest(BaseModel):
81
  prompt: str
@@ -86,22 +83,18 @@ class AgentResponse(BaseModel):
86
  tool_calls: List[ToolCall]
87
 
88
 
89
- # ────────────────────── Helper: Parse Tool Calls ─────────────
90
 
91
  def parse_tool_calls(text: str) -> List[ToolCall]:
92
- """Extract <tool_call>tool_name({...})</tool_call> from model output."""
93
  calls = []
94
  pattern = r'<tool_call>\s*(\w+)\((\{.*?\})\)\s*</tool_call>'
95
  matches = re.findall(pattern, text, re.DOTALL)
96
-
97
  for tool_name, args_str in matches:
98
  try:
99
  args = json.loads(args_str)
100
  except json.JSONDecodeError:
101
  args = {"raw": args_str}
102
  calls.append(ToolCall(tool=tool_name, args=args))
103
-
104
- # Fallback: find tool mentions without proper wrapping
105
  if not calls:
106
  valid_tools = [
107
  "scan_code", "simulate_attack", "apply_patch", "run_tests",
@@ -110,83 +103,147 @@ def parse_tool_calls(text: str) -> List[ToolCall]:
110
  for tool in valid_tools:
111
  if tool in text.lower():
112
  calls.append(ToolCall(tool=tool, args={}))
113
-
114
  return calls
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  # ────────────────────── Endpoints ────────────────────────────
118
 
119
- @app.get("/")
120
  async def root():
121
- return {
122
- "service": "SecureHeal Agent",
123
- "model": MODEL_ID,
124
- "status": "ready" if PIPE else "loading",
125
- "endpoints": {
126
- "/scan": "POST β€” Scan code for vulnerabilities",
127
- "/agent": "POST β€” Free-form agent prompt",
128
- "/health": "GET β€” Health check",
129
- },
130
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
 
133
  @app.get("/health")
134
  async def health():
135
- return {"status": "healthy", "model_loaded": PIPE is not None}
136
 
137
 
138
  @app.post("/scan", response_model=VulnerabilityReport)
139
- async def scan_code(request: ScanRequest):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  """
141
- Scan application code for vulnerabilities.
142
- The agent analyzes the code and returns structured tool calls + fixes.
143
  """
144
  if not PIPE:
145
- raise HTTPException(503, "Model still loading, try again in ~2 min")
146
 
147
- prompt = (
148
- f"You are an autonomous SRE and Security agent. "
149
- f"Analyze the following {request.context} code for vulnerabilities. "
150
- f"Use scan_code, simulate_attack, apply_patch, run_tests to analyze and fix. "
151
- f"Output each action as <tool_call>tool_name({{\"param\": \"value\"}})</tool_call>. "
152
- f"End with DONE when finished.\n\n"
153
- f"Code to analyze:\n```\n{request.code}\n```"
154
- )
155
 
156
- messages = [{"role": "user", "content": prompt}]
157
- output = PIPE(messages, max_new_tokens=request.max_tokens, do_sample=True, temperature=0.7)
158
- response_text = output[0]["generated_text"][-1]["content"]
159
 
 
160
  tool_calls = parse_tool_calls(response_text)
161
 
162
  return VulnerabilityReport(
163
  vulnerabilities_found=len(tool_calls) > 0,
164
  tool_calls=tool_calls,
165
  analysis=response_text,
166
- raw_output=response_text,
167
  )
168
 
169
 
170
  @app.post("/agent", response_model=AgentResponse)
171
  async def agent_prompt(request: AgentRequest):
172
- """
173
- Send a free-form prompt to the SecureHeal agent.
174
- """
175
  if not PIPE:
176
- raise HTTPException(503, "Model still loading, try again in ~2 min")
177
 
178
  messages = [{"role": "user", "content": request.prompt}]
179
  output = PIPE(messages, max_new_tokens=request.max_tokens, do_sample=True, temperature=0.7)
180
  response_text = output[0]["generated_text"][-1]["content"]
181
  tool_calls = parse_tool_calls(response_text)
182
 
183
- return AgentResponse(
184
- response=response_text,
185
- tool_calls=tool_calls,
186
- )
187
-
188
 
189
- # ────────────────────── Run ──────────────────────────────────
190
 
191
  if __name__ == "__main__":
192
  import uvicorn
 
2
  SecureHeal Agent β€” HuggingFace Space FastAPI Server
3
  ────────────────────────────────────────────────────
4
  Loads the trained model at startup, caches it, and exposes a FastAPI
5
+ endpoint that takes application code (text or file upload) β†’ runs the
6
+ SecureHeal agent β†’ finds vulnerabilities β†’ suggests fixes.
7
 
8
  Deploy to HF Spaces with GPU (T4).
9
  """
 
13
  import re
14
  import torch
15
  from contextlib import asynccontextmanager
16
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
17
  from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.responses import HTMLResponse
19
  from pydantic import BaseModel
20
  from typing import Optional, List
21
+ from transformers import pipeline as hf_pipeline
22
 
23
  # ────────────────────── Model Cache ──────────────────────────
24
 
25
  MODEL_ID = os.environ.get("MODEL_ID", "Nitesh-Reddy/secureheal-agent-v2")
26
+ PIPE = None
27
 
28
 
29
  @asynccontextmanager
30
  async def lifespan(app: FastAPI):
 
31
  global PIPE
32
  print(f"πŸ”„ Loading model: {MODEL_ID}")
33
+ PIPE = hf_pipeline(
 
 
34
  "text-generation",
35
  model=MODEL_ID,
36
  torch_dtype=torch.float16,
 
38
  )
39
  print(f"βœ… Model loaded and cached!")
40
  yield
 
41
 
42
 
43
  # ────────────────────── FastAPI App ──────────────────────────
 
57
  )
58
 
59
 
60
+ # ────────────────────── Models ───────────────────────────────
61
 
62
  class ScanRequest(BaseModel):
63
  code: str
 
72
  vulnerabilities_found: bool
73
  tool_calls: List[ToolCall]
74
  analysis: str
75
+ filename: Optional[str] = None
76
 
77
  class AgentRequest(BaseModel):
78
  prompt: str
 
83
  tool_calls: List[ToolCall]
84
 
85
 
86
+ # ────────────────────── Helper ───────────────────────────────
87
 
88
  def parse_tool_calls(text: str) -> List[ToolCall]:
 
89
  calls = []
90
  pattern = r'<tool_call>\s*(\w+)\((\{.*?\})\)\s*</tool_call>'
91
  matches = re.findall(pattern, text, re.DOTALL)
 
92
  for tool_name, args_str in matches:
93
  try:
94
  args = json.loads(args_str)
95
  except json.JSONDecodeError:
96
  args = {"raw": args_str}
97
  calls.append(ToolCall(tool=tool_name, args=args))
 
 
98
  if not calls:
99
  valid_tools = [
100
  "scan_code", "simulate_attack", "apply_patch", "run_tests",
 
103
  for tool in valid_tools:
104
  if tool in text.lower():
105
  calls.append(ToolCall(tool=tool, args={}))
 
106
  return calls
107
 
108
 
109
+ def run_agent(code: str, context: str = "web application", max_tokens: int = 512) -> str:
110
+ """Run the SecureHeal agent on the given code."""
111
+ prompt = (
112
+ f"You are an autonomous SRE and Security agent. "
113
+ f"Analyze the following {context} code for vulnerabilities. "
114
+ f"Use scan_code, simulate_attack, apply_patch, run_tests to analyze and fix. "
115
+ f'Output each action as <tool_call>tool_name({{"param": "value"}})</tool_call>. '
116
+ f"End with DONE when finished.\n\n"
117
+ f"Code to analyze:\n```\n{code}\n```"
118
+ )
119
+ messages = [{"role": "user", "content": prompt}]
120
+ output = PIPE(messages, max_new_tokens=max_tokens, do_sample=True, temperature=0.7)
121
+ return output[0]["generated_text"][-1]["content"]
122
+
123
+
124
  # ────────────────────── Endpoints ────────────────────────────
125
 
126
+ @app.get("/", response_class=HTMLResponse)
127
  async def root():
128
+ """Landing page with usage instructions."""
129
+ return """
130
+ <html>
131
+ <head><title>SecureHeal Agent</title>
132
+ <style>
133
+ body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px;
134
+ background: #0d1117; color: #e6edf3; }
135
+ h1 { color: #58a6ff; }
136
+ code { background: #161b22; padding: 2px 6px; border-radius: 4px; color: #f0883e; }
137
+ pre { background: #161b22; padding: 16px; border-radius: 8px; overflow-x: auto; }
138
+ .endpoint { background: #161b22; padding: 12px 16px; border-radius: 8px;
139
+ border-left: 3px solid #58a6ff; margin: 12px 0; }
140
+ a { color: #58a6ff; }
141
+ </style></head>
142
+ <body>
143
+ <h1>πŸ›‘οΈ SecureHeal Agent API</h1>
144
+ <p>Autonomous SRE & Security agent β€” trained with GRPO on Llama 3 8B</p>
145
+
146
+ <h2>Endpoints</h2>
147
+
148
+ <div class="endpoint">
149
+ <strong>POST /scan</strong> β€” Scan code (JSON body)<br>
150
+ <code>{"code": "your code here", "context": "web app"}</code>
151
+ </div>
152
+
153
+ <div class="endpoint">
154
+ <strong>POST /scan/file</strong> β€” Upload a file to scan<br>
155
+ <code>curl -F "file=@app.py" -F "context=flask app" URL/scan/file</code>
156
+ </div>
157
+
158
+ <div class="endpoint">
159
+ <strong>POST /agent</strong> β€” Free-form agent prompt<br>
160
+ <code>{"prompt": "Find SQL injection in login function"}</code>
161
+ </div>
162
+
163
+ <div class="endpoint">
164
+ <strong>GET /health</strong> β€” Health check
165
+ </div>
166
+
167
+ <h2>Example</h2>
168
+ <pre>curl -X POST /scan/file \\
169
+ -F "file=@vulnerable_app.py" \\
170
+ -F "context=flask web application"</pre>
171
+
172
+ <p>Model: <a href="https://huggingface.co/Nitesh-Reddy/secureheal-agent-v2">Nitesh-Reddy/secureheal-agent-v2</a></p>
173
+ <p><a href="/docs">πŸ“– Interactive API Docs (Swagger)</a></p>
174
+ </body></html>
175
+ """
176
 
177
 
178
  @app.get("/health")
179
  async def health():
180
+ return {"status": "healthy", "model_loaded": PIPE is not None, "model": MODEL_ID}
181
 
182
 
183
  @app.post("/scan", response_model=VulnerabilityReport)
184
+ async def scan_code_json(request: ScanRequest):
185
+ """Scan code for vulnerabilities (JSON body with code string)."""
186
+ if not PIPE:
187
+ raise HTTPException(503, "Model still loading")
188
+
189
+ response_text = run_agent(request.code, request.context, request.max_tokens)
190
+ tool_calls = parse_tool_calls(response_text)
191
+
192
+ return VulnerabilityReport(
193
+ vulnerabilities_found=len(tool_calls) > 0,
194
+ tool_calls=tool_calls,
195
+ analysis=response_text,
196
+ )
197
+
198
+
199
+ @app.post("/scan/file", response_model=VulnerabilityReport)
200
+ async def scan_code_file(
201
+ file: UploadFile = File(..., description="Source code file to scan"),
202
+ context: str = Form("web application", description="What kind of app (e.g. flask, django, express)"),
203
+ max_tokens: int = Form(512, description="Max response tokens"),
204
+ ):
205
  """
206
+ Upload a source code file for vulnerability scanning.
207
+ Supports .py, .js, .ts, .java, .go, .rb, .php, etc.
208
  """
209
  if not PIPE:
210
+ raise HTTPException(503, "Model still loading")
211
 
212
+ # Read file content
213
+ content = await file.read()
214
+ try:
215
+ code = content.decode("utf-8")
216
+ except UnicodeDecodeError:
217
+ raise HTTPException(400, "File must be a text/source code file")
 
 
218
 
219
+ # Truncate very long files to fit model context
220
+ if len(code) > 8000:
221
+ code = code[:8000] + "\n\n# ... (truncated, file too large)"
222
 
223
+ response_text = run_agent(code, context, max_tokens)
224
  tool_calls = parse_tool_calls(response_text)
225
 
226
  return VulnerabilityReport(
227
  vulnerabilities_found=len(tool_calls) > 0,
228
  tool_calls=tool_calls,
229
  analysis=response_text,
230
+ filename=file.filename,
231
  )
232
 
233
 
234
  @app.post("/agent", response_model=AgentResponse)
235
  async def agent_prompt(request: AgentRequest):
236
+ """Send a free-form prompt to the SecureHeal agent."""
 
 
237
  if not PIPE:
238
+ raise HTTPException(503, "Model still loading")
239
 
240
  messages = [{"role": "user", "content": request.prompt}]
241
  output = PIPE(messages, max_new_tokens=request.max_tokens, do_sample=True, temperature=0.7)
242
  response_text = output[0]["generated_text"][-1]["content"]
243
  tool_calls = parse_tool_calls(response_text)
244
 
245
+ return AgentResponse(response=response_text, tool_calls=tool_calls)
 
 
 
 
246
 
 
247
 
248
  if __name__ == "__main__":
249
  import uvicorn
requirements.txt CHANGED
@@ -5,3 +5,4 @@ bitsandbytes
5
  fastapi
6
  uvicorn[standard]
7
  pydantic>=2.0
 
 
5
  fastapi
6
  uvicorn[standard]
7
  pydantic>=2.0
8
+ python-multipart