MartyNattakit commited on
Commit
ef0d2f0
Β·
1 Parent(s): 9613bf1

add api, frontend, requirements

Browse files
Files changed (4) hide show
  1. api/__init__.py +7 -0
  2. api/main.py +218 -0
  3. frontend/index.html +855 -0
  4. requirements.txt +16 -0
api/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ api/
3
+ FastAPI application exposing the vulnerability classification pipeline.
4
+
5
+ Entry point: api/main.py
6
+ Run: uvicorn api.main:app --host 0.0.0.0 --port 7860
7
+ """
api/main.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ api/main.py
3
+ FastAPI application β€” exposes the vulnerability classification pipeline
4
+ as an HTTP API. Single endpoint: POST /classify
5
+
6
+ Run locally:
7
+ uvicorn api.main:app --reload --port 8000
8
+
9
+ Run in HF Spaces:
10
+ uvicorn api.main:app --host 0.0.0.0 --port 7860
11
+ """
12
+
13
+ from __future__ import annotations
14
+ import time
15
+ from contextlib import asynccontextmanager
16
+ from typing import Optional
17
+
18
+ from fastapi import FastAPI, HTTPException, Request
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.responses import JSONResponse
21
+ from pydantic import BaseModel, Field
22
+
23
+ from pipeline.router import get_router
24
+
25
+ # ── Lifespan β€” warm up classifier on startup ──────────────────────────────────
26
+ # RoBERTa (125MB) loads fast β€” warm it up at startup so first request isn't slow
27
+ # Qwen (5GB) is lazy-loaded on first code input β€” too large to preload
28
+
29
+ @asynccontextmanager
30
+ async def lifespan(app: FastAPI):
31
+ print("[API] Warming up RoBERTa classifier...")
32
+ router = get_router()
33
+ router._get_classifier() # preload RoBERTa only
34
+ print("[API] Ready.")
35
+ yield
36
+ print("[API] Shutting down.")
37
+
38
+
39
+ # ── App ───────────────────────────────────────────────────────────────────────
40
+
41
+ app = FastAPI(
42
+ title="CodeSentinel API",
43
+ description=(
44
+ "Vulnerability classification API. "
45
+ "Classifies code snippets and CVE descriptions into CWE categories, "
46
+ "with optional ATLAS AI/ML attack pattern matching."
47
+ ),
48
+ version="0.1.0",
49
+ lifespan=lifespan,
50
+ )
51
+
52
+ # Allow frontend (HF Spaces, localhost) to call the API
53
+ app.add_middleware(
54
+ CORSMiddleware,
55
+ allow_origins=["*"], # tighten this in production
56
+ allow_methods=["POST", "GET"],
57
+ allow_headers=["*"],
58
+ )
59
+
60
+
61
+ # ── Schemas ───────────────────────────────────────────────────────────────────
62
+
63
+ class ClassifyRequest(BaseModel):
64
+ input: str = Field(
65
+ ...,
66
+ min_length=10,
67
+ max_length=8000,
68
+ description="Raw input β€” code snippet, CVE description, or bug report.",
69
+ examples=[
70
+ "def get_user(name): return db.execute('SELECT * FROM users WHERE name=' + name)"
71
+ ],
72
+ )
73
+
74
+
75
+ class CWEPrediction(BaseModel):
76
+ cwe_id: str
77
+ cwe_name: str
78
+ severity: str
79
+ confidence: float
80
+
81
+
82
+ class ATLASMatch(BaseModel):
83
+ atlas_id: str
84
+ technique: str
85
+ tactic: str
86
+ confidence: str
87
+ matched_signals: list[str]
88
+ description: str
89
+ mitigations: list[str]
90
+ real_world: Optional[str]
91
+ reasoning: str
92
+
93
+
94
+ class ClassifyResponse(BaseModel):
95
+ # Primary result
96
+ cwe_id: str
97
+ cwe_name: str
98
+ severity: str
99
+ confidence: float
100
+
101
+ # Explanation (Qwen's structured description or original input)
102
+ description: str
103
+
104
+ # Top-3 alternatives (excluding top-1)
105
+ alternatives: list[CWEPrediction]
106
+
107
+ # ATLAS match β€” None if no AI/ML signals detected
108
+ atlas_match: Optional[ATLASMatch]
109
+
110
+ # Metadata
111
+ input_type: str # "code" or "text"
112
+ warning: Optional[str]
113
+ elapsed_s: float
114
+
115
+
116
+ class HealthResponse(BaseModel):
117
+ status: str
118
+ version: str
119
+ models: dict
120
+
121
+
122
+ class ErrorResponse(BaseModel):
123
+ error: str
124
+ detail: Optional[str]
125
+
126
+
127
+ # ── Routes ────────────────────────────────────────────────────────────────────
128
+
129
+ @app.get("/", include_in_schema=False)
130
+ async def root():
131
+ return {"message": "CodeSentinel API v0.1 β€” POST /classify to classify input."}
132
+
133
+
134
+ @app.get("/health", response_model=HealthResponse)
135
+ async def health():
136
+ """Health check β€” returns model load status."""
137
+ router = get_router()
138
+ return {
139
+ "status": "ok",
140
+ "version": "0.1.0",
141
+ "models": {
142
+ "roberta": "loaded" if router._classifier is not None else "not loaded",
143
+ "qwen": "loaded" if router._code_analyzer is not None else "not loaded (lazy)",
144
+ "atlas_matcher": "loaded" if router._atlas_matcher is not None else "not loaded (lazy)",
145
+ }
146
+ }
147
+
148
+
149
+ @app.post(
150
+ "/classify",
151
+ response_model=ClassifyResponse,
152
+ responses={
153
+ 400: {"model": ErrorResponse, "description": "Invalid input"},
154
+ 500: {"model": ErrorResponse, "description": "Internal classification error"},
155
+ },
156
+ )
157
+ async def classify(request: ClassifyRequest):
158
+ """
159
+ Classify a vulnerability input.
160
+
161
+ - **Code snippets** β†’ Qwen analyzes β†’ RoBERTa classifies β†’ CWE result
162
+ - **CVE/text descriptions** β†’ RoBERTa classifies directly β†’ CWE result
163
+ - **AI/ML-related inputs** β†’ also runs ATLAS pattern matcher
164
+
165
+ Returns a unified output card with CWE ID, severity, explanation,
166
+ remediation hints, and optional ATLAS technique match.
167
+ """
168
+ try:
169
+ router = get_router()
170
+ result = router.run(request.input)
171
+ except ValueError as e:
172
+ raise HTTPException(status_code=400, detail=str(e))
173
+ except Exception as e:
174
+ raise HTTPException(
175
+ status_code=500,
176
+ detail=f"Classification failed: {str(e)}"
177
+ )
178
+
179
+ # Build alternatives list
180
+ alternatives = [
181
+ CWEPrediction(**alt) for alt in result.get("alternatives", [])
182
+ ]
183
+
184
+ # Build ATLAS match if present
185
+ atlas_match = None
186
+ if result.get("atlas_match"):
187
+ atlas_match = ATLASMatch(**result["atlas_match"])
188
+
189
+ return ClassifyResponse(
190
+ cwe_id=result["cwe_id"],
191
+ cwe_name=result["cwe_name"],
192
+ severity=result["severity"],
193
+ confidence=result["confidence"],
194
+ description=result["description"],
195
+ alternatives=alternatives,
196
+ atlas_match=atlas_match,
197
+ input_type=result["input_type"],
198
+ warning=result.get("warning"),
199
+ elapsed_s=result["elapsed_s"],
200
+ )
201
+
202
+
203
+ # ── Error handlers ────────────────────────────────────────────────────────────
204
+
205
+ @app.exception_handler(404)
206
+ async def not_found_handler(request: Request, exc):
207
+ return JSONResponse(
208
+ status_code=404,
209
+ content={"error": "Not found", "detail": str(request.url)}
210
+ )
211
+
212
+
213
+ @app.exception_handler(500)
214
+ async def server_error_handler(request: Request, exc):
215
+ return JSONResponse(
216
+ status_code=500,
217
+ content={"error": "Internal server error", "detail": "Check server logs."}
218
+ )
frontend/index.html ADDED
@@ -0,0 +1,855 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>CodeSentinel</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
9
+ <style>
10
+ :root {
11
+ --bg: #0a0a0f;
12
+ --surface: #111118;
13
+ --border: #1e1e2e;
14
+ --accent: #00ff9d;
15
+ --accent2: #ff4d6d;
16
+ --amber: #ffb347;
17
+ --text: #e8e8f0;
18
+ --muted: #6b6b80;
19
+ --mono: 'Space Mono', monospace;
20
+ --sans: 'DM Sans', sans-serif;
21
+
22
+ --sev-critical: #ff4d6d;
23
+ --sev-high: #ff7043;
24
+ --sev-medium: #ffb347;
25
+ --sev-low: #00ff9d;
26
+ }
27
+
28
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
29
+
30
+ body {
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ font-family: var(--sans);
34
+ min-height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow-x: hidden;
38
+ }
39
+
40
+ /* ── Noise overlay ── */
41
+ body::before {
42
+ content: '';
43
+ position: fixed;
44
+ inset: 0;
45
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
46
+ pointer-events: none;
47
+ z-index: 0;
48
+ opacity: 0.4;
49
+ }
50
+
51
+ /* ── Grid lines ── */
52
+ body::after {
53
+ content: '';
54
+ position: fixed;
55
+ inset: 0;
56
+ background-image:
57
+ linear-gradient(rgba(0,255,157,0.03) 1px, transparent 1px),
58
+ linear-gradient(90deg, rgba(0,255,157,0.03) 1px, transparent 1px);
59
+ background-size: 40px 40px;
60
+ pointer-events: none;
61
+ z-index: 0;
62
+ }
63
+
64
+ /* ── Header ── */
65
+ header {
66
+ position: relative;
67
+ z-index: 10;
68
+ padding: 2rem 2.5rem 1.5rem;
69
+ border-bottom: 1px solid var(--border);
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ animation: fadeDown 0.6s ease both;
74
+ }
75
+
76
+ .logo {
77
+ display: flex;
78
+ align-items: baseline;
79
+ gap: 0.5rem;
80
+ }
81
+
82
+ .logo-mark {
83
+ font-family: var(--mono);
84
+ font-size: 1.4rem;
85
+ font-weight: 700;
86
+ color: var(--accent);
87
+ letter-spacing: -0.02em;
88
+ }
89
+
90
+ .logo-sub {
91
+ font-family: var(--mono);
92
+ font-size: 0.65rem;
93
+ color: var(--muted);
94
+ letter-spacing: 0.15em;
95
+ text-transform: uppercase;
96
+ }
97
+
98
+ .badge {
99
+ font-family: var(--mono);
100
+ font-size: 0.65rem;
101
+ color: var(--muted);
102
+ border: 1px solid var(--border);
103
+ padding: 0.25rem 0.6rem;
104
+ letter-spacing: 0.1em;
105
+ }
106
+
107
+ /* ── Main layout ── */
108
+ main {
109
+ position: relative;
110
+ z-index: 10;
111
+ flex: 1;
112
+ display: grid;
113
+ grid-template-columns: 1fr 1fr;
114
+ gap: 0;
115
+ max-width: 1400px;
116
+ margin: 0 auto;
117
+ width: 100%;
118
+ padding: 2.5rem;
119
+ gap: 2rem;
120
+ }
121
+
122
+ /* ── Input panel ── */
123
+ .input-panel {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 1rem;
127
+ animation: fadeUp 0.5s 0.1s ease both;
128
+ }
129
+
130
+ .panel-label {
131
+ font-family: var(--mono);
132
+ font-size: 0.65rem;
133
+ color: var(--muted);
134
+ letter-spacing: 0.15em;
135
+ text-transform: uppercase;
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 0.5rem;
139
+ }
140
+
141
+ .panel-label::before {
142
+ content: '';
143
+ display: inline-block;
144
+ width: 6px;
145
+ height: 6px;
146
+ background: var(--accent);
147
+ border-radius: 50%;
148
+ }
149
+
150
+ textarea {
151
+ flex: 1;
152
+ min-height: 420px;
153
+ background: var(--surface);
154
+ border: 1px solid var(--border);
155
+ color: var(--text);
156
+ font-family: var(--mono);
157
+ font-size: 0.8rem;
158
+ line-height: 1.7;
159
+ padding: 1.25rem;
160
+ resize: vertical;
161
+ outline: none;
162
+ transition: border-color 0.2s;
163
+ caret-color: var(--accent);
164
+ }
165
+
166
+ textarea::placeholder { color: var(--muted); }
167
+
168
+ textarea:focus {
169
+ border-color: rgba(0,255,157,0.3);
170
+ box-shadow: 0 0 0 1px rgba(0,255,157,0.1) inset;
171
+ }
172
+
173
+ .input-meta {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: space-between;
177
+ }
178
+
179
+ .char-count {
180
+ font-family: var(--mono);
181
+ font-size: 0.65rem;
182
+ color: var(--muted);
183
+ }
184
+
185
+ .classify-btn {
186
+ font-family: var(--mono);
187
+ font-size: 0.8rem;
188
+ font-weight: 700;
189
+ letter-spacing: 0.08em;
190
+ color: var(--bg);
191
+ background: var(--accent);
192
+ border: none;
193
+ padding: 0.75rem 2rem;
194
+ cursor: pointer;
195
+ transition: all 0.15s;
196
+ position: relative;
197
+ overflow: hidden;
198
+ }
199
+
200
+ .classify-btn:hover {
201
+ background: #00e68a;
202
+ transform: translateY(-1px);
203
+ }
204
+
205
+ .classify-btn:active { transform: translateY(0); }
206
+
207
+ .classify-btn:disabled {
208
+ opacity: 0.4;
209
+ cursor: not-allowed;
210
+ transform: none;
211
+ }
212
+
213
+ .classify-btn.loading::after {
214
+ content: '';
215
+ position: absolute;
216
+ bottom: 0;
217
+ left: -100%;
218
+ width: 100%;
219
+ height: 2px;
220
+ background: rgba(0,0,0,0.3);
221
+ animation: progress 1.5s linear infinite;
222
+ }
223
+
224
+ /* ── Output panel ── */
225
+ .output-panel {
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 1rem;
229
+ animation: fadeUp 0.5s 0.2s ease both;
230
+ }
231
+
232
+ .output-card {
233
+ background: var(--surface);
234
+ border: 1px solid var(--border);
235
+ flex: 1;
236
+ display: flex;
237
+ flex-direction: column;
238
+ overflow: hidden;
239
+ }
240
+
241
+ /* Empty state */
242
+ .empty-state {
243
+ flex: 1;
244
+ display: flex;
245
+ flex-direction: column;
246
+ align-items: center;
247
+ justify-content: center;
248
+ gap: 1rem;
249
+ padding: 3rem;
250
+ text-align: center;
251
+ }
252
+
253
+ .empty-icon {
254
+ font-size: 2rem;
255
+ opacity: 0.2;
256
+ }
257
+
258
+ .empty-text {
259
+ font-family: var(--mono);
260
+ font-size: 0.7rem;
261
+ color: var(--muted);
262
+ letter-spacing: 0.1em;
263
+ line-height: 1.8;
264
+ }
265
+
266
+ /* Result state */
267
+ .result {
268
+ display: none;
269
+ flex-direction: column;
270
+ flex: 1;
271
+ }
272
+
273
+ .result.visible { display: flex; }
274
+
275
+ /* CWE header block */
276
+ .cwe-header {
277
+ padding: 1.5rem;
278
+ border-bottom: 1px solid var(--border);
279
+ display: flex;
280
+ align-items: flex-start;
281
+ justify-content: space-between;
282
+ gap: 1rem;
283
+ }
284
+
285
+ .cwe-id {
286
+ font-family: var(--mono);
287
+ font-size: 1.8rem;
288
+ font-weight: 700;
289
+ color: var(--accent);
290
+ letter-spacing: -0.02em;
291
+ line-height: 1;
292
+ }
293
+
294
+ .cwe-name {
295
+ font-size: 0.85rem;
296
+ color: var(--muted);
297
+ margin-top: 0.4rem;
298
+ font-weight: 300;
299
+ }
300
+
301
+ .severity-badge {
302
+ font-family: var(--mono);
303
+ font-size: 0.65rem;
304
+ font-weight: 700;
305
+ letter-spacing: 0.12em;
306
+ padding: 0.3rem 0.7rem;
307
+ border: 1px solid currentColor;
308
+ white-space: nowrap;
309
+ margin-top: 0.2rem;
310
+ }
311
+
312
+ .severity-CRITICAL { color: var(--sev-critical); }
313
+ .severity-HIGH { color: var(--sev-high); }
314
+ .severity-MEDIUM { color: var(--sev-medium); }
315
+ .severity-LOW { color: var(--sev-low); }
316
+
317
+ /* Confidence bar */
318
+ .confidence-row {
319
+ padding: 1rem 1.5rem;
320
+ border-bottom: 1px solid var(--border);
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 1rem;
324
+ }
325
+
326
+ .conf-label {
327
+ font-family: var(--mono);
328
+ font-size: 0.65rem;
329
+ color: var(--muted);
330
+ letter-spacing: 0.1em;
331
+ white-space: nowrap;
332
+ }
333
+
334
+ .conf-bar-track {
335
+ flex: 1;
336
+ height: 3px;
337
+ background: var(--border);
338
+ position: relative;
339
+ overflow: hidden;
340
+ }
341
+
342
+ .conf-bar-fill {
343
+ height: 100%;
344
+ background: var(--accent);
345
+ transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
346
+ width: 0%;
347
+ }
348
+
349
+ .conf-value {
350
+ font-family: var(--mono);
351
+ font-size: 0.75rem;
352
+ color: var(--text);
353
+ min-width: 3rem;
354
+ text-align: right;
355
+ }
356
+
357
+ /* Description */
358
+ .description-block {
359
+ padding: 1.25rem 1.5rem;
360
+ border-bottom: 1px solid var(--border);
361
+ }
362
+
363
+ .block-label {
364
+ font-family: var(--mono);
365
+ font-size: 0.6rem;
366
+ color: var(--muted);
367
+ letter-spacing: 0.15em;
368
+ text-transform: uppercase;
369
+ margin-bottom: 0.6rem;
370
+ }
371
+
372
+ .description-text {
373
+ font-size: 0.85rem;
374
+ line-height: 1.7;
375
+ color: var(--text);
376
+ font-weight: 300;
377
+ }
378
+
379
+ /* Alternatives */
380
+ .alternatives-block {
381
+ padding: 1.25rem 1.5rem;
382
+ border-bottom: 1px solid var(--border);
383
+ }
384
+
385
+ .alt-list {
386
+ display: flex;
387
+ flex-direction: column;
388
+ gap: 0.5rem;
389
+ margin-top: 0.6rem;
390
+ }
391
+
392
+ .alt-item {
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 0.75rem;
396
+ }
397
+
398
+ .alt-cwe {
399
+ font-family: var(--mono);
400
+ font-size: 0.75rem;
401
+ color: var(--muted);
402
+ min-width: 6rem;
403
+ }
404
+
405
+ .alt-bar-track {
406
+ flex: 1;
407
+ height: 2px;
408
+ background: var(--border);
409
+ }
410
+
411
+ .alt-bar-fill {
412
+ height: 100%;
413
+ background: var(--muted);
414
+ transition: width 0.6s 0.2s cubic-bezier(0.16, 1, 0.3, 1);
415
+ width: 0%;
416
+ }
417
+
418
+ .alt-score {
419
+ font-family: var(--mono);
420
+ font-size: 0.65rem;
421
+ color: var(--muted);
422
+ min-width: 3rem;
423
+ text-align: right;
424
+ }
425
+
426
+ /* ATLAS block */
427
+ .atlas-block {
428
+ padding: 1.25rem 1.5rem;
429
+ border-bottom: 1px solid var(--border);
430
+ border-left: 2px solid var(--accent2);
431
+ display: none;
432
+ }
433
+
434
+ .atlas-block.visible { display: block; }
435
+
436
+ .atlas-id {
437
+ font-family: var(--mono);
438
+ font-size: 0.75rem;
439
+ color: var(--accent2);
440
+ margin-bottom: 0.3rem;
441
+ }
442
+
443
+ .atlas-technique {
444
+ font-size: 0.9rem;
445
+ font-weight: 500;
446
+ margin-bottom: 0.5rem;
447
+ }
448
+
449
+ .atlas-reasoning {
450
+ font-size: 0.8rem;
451
+ color: var(--muted);
452
+ line-height: 1.6;
453
+ font-weight: 300;
454
+ }
455
+
456
+ .atlas-conf {
457
+ font-family: var(--mono);
458
+ font-size: 0.6rem;
459
+ color: var(--accent2);
460
+ letter-spacing: 0.1em;
461
+ margin-top: 0.5rem;
462
+ }
463
+
464
+ /* Warning */
465
+ .warning-block {
466
+ padding: 0.75rem 1.5rem;
467
+ background: rgba(255,179,71,0.06);
468
+ border-left: 2px solid var(--amber);
469
+ display: none;
470
+ margin: 0;
471
+ }
472
+
473
+ .warning-block.visible { display: block; }
474
+
475
+ .warning-text {
476
+ font-family: var(--mono);
477
+ font-size: 0.7rem;
478
+ color: var(--amber);
479
+ line-height: 1.6;
480
+ }
481
+
482
+ /* Input type tag */
483
+ .meta-row {
484
+ padding: 0.75rem 1.5rem;
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 1rem;
488
+ margin-top: auto;
489
+ }
490
+
491
+ .meta-tag {
492
+ font-family: var(--mono);
493
+ font-size: 0.6rem;
494
+ color: var(--muted);
495
+ letter-spacing: 0.1em;
496
+ border: 1px solid var(--border);
497
+ padding: 0.2rem 0.5rem;
498
+ }
499
+
500
+ .meta-time {
501
+ font-family: var(--mono);
502
+ font-size: 0.6rem;
503
+ color: var(--muted);
504
+ margin-left: auto;
505
+ }
506
+
507
+ /* Error state */
508
+ .error-block {
509
+ padding: 1.5rem;
510
+ display: none;
511
+ flex-direction: column;
512
+ gap: 0.5rem;
513
+ }
514
+
515
+ .error-block.visible { display: flex; }
516
+
517
+ .error-title {
518
+ font-family: var(--mono);
519
+ font-size: 0.75rem;
520
+ color: var(--accent2);
521
+ }
522
+
523
+ .error-msg {
524
+ font-size: 0.8rem;
525
+ color: var(--muted);
526
+ }
527
+
528
+ /* ── Footer ── */
529
+ footer {
530
+ position: relative;
531
+ z-index: 10;
532
+ padding: 1rem 2.5rem;
533
+ border-top: 1px solid var(--border);
534
+ display: flex;
535
+ align-items: center;
536
+ justify-content: space-between;
537
+ }
538
+
539
+ .footer-note {
540
+ font-family: var(--mono);
541
+ font-size: 0.6rem;
542
+ color: var(--muted);
543
+ letter-spacing: 0.08em;
544
+ }
545
+
546
+ .footer-links {
547
+ display: flex;
548
+ gap: 1.5rem;
549
+ }
550
+
551
+ .footer-links a {
552
+ font-family: var(--mono);
553
+ font-size: 0.6rem;
554
+ color: var(--muted);
555
+ text-decoration: none;
556
+ letter-spacing: 0.08em;
557
+ transition: color 0.15s;
558
+ }
559
+
560
+ .footer-links a:hover { color: var(--accent); }
561
+
562
+ /* ── Animations ── */
563
+ @keyframes fadeUp {
564
+ from { opacity: 0; transform: translateY(12px); }
565
+ to { opacity: 1; transform: translateY(0); }
566
+ }
567
+
568
+ @keyframes fadeDown {
569
+ from { opacity: 0; transform: translateY(-8px); }
570
+ to { opacity: 1; transform: translateY(0); }
571
+ }
572
+
573
+ @keyframes progress {
574
+ from { left: -100%; }
575
+ to { left: 100%; }
576
+ }
577
+
578
+ @keyframes pulse {
579
+ 0%, 100% { opacity: 1; }
580
+ 50% { opacity: 0.3; }
581
+ }
582
+
583
+ .scanning {
584
+ font-family: var(--mono);
585
+ font-size: 0.7rem;
586
+ color: var(--accent);
587
+ animation: pulse 1.2s ease infinite;
588
+ padding: 1.5rem;
589
+ text-align: center;
590
+ }
591
+
592
+ /* ── Responsive ── */
593
+ @media (max-width: 900px) {
594
+ main {
595
+ grid-template-columns: 1fr;
596
+ padding: 1.5rem;
597
+ }
598
+ textarea { min-height: 280px; }
599
+ }
600
+ </style>
601
+ </head>
602
+ <body>
603
+
604
+ <header>
605
+ <div class="logo">
606
+ <span class="logo-mark">CodeSentinel</span>
607
+ <span class="logo-sub">v0.1</span>
608
+ </div>
609
+ <span class="badge">CWE Β· ATLAS Β· AI/ML</span>
610
+ </header>
611
+
612
+ <main>
613
+ <!-- Input -->
614
+ <div class="input-panel">
615
+ <div class="panel-label">Input β€” paste code, CVE description, or bug report</div>
616
+ <textarea
617
+ id="input"
618
+ placeholder="# Paste anything here&#10;def get_user(name):&#10; return db.execute('SELECT * FROM users WHERE name=' + name)&#10;&#10;# or a CVE description:&#10;# The login form passes user input directly into SQL queries without sanitization..."
619
+ spellcheck="false"
620
+ ></textarea>
621
+ <div class="input-meta">
622
+ <span class="char-count" id="charCount">0 / 8000</span>
623
+ <button class="classify-btn" id="classifyBtn" onclick="classify()">
624
+ CLASSIFY β†’
625
+ </button>
626
+ </div>
627
+ </div>
628
+
629
+ <!-- Output -->
630
+ <div class="output-panel">
631
+ <div class="panel-label">Analysis</div>
632
+ <div class="output-card">
633
+
634
+ <!-- Empty state -->
635
+ <div class="empty-state" id="emptyState">
636
+ <div class="empty-icon">⬑</div>
637
+ <div class="empty-text">
638
+ AWAITING INPUT<br/>
639
+ paste code or vulnerability description<br/>
640
+ and hit classify
641
+ </div>
642
+ </div>
643
+
644
+ <!-- Scanning state -->
645
+ <div class="scanning" id="scanningState" style="display:none;">
646
+ β–Ά SCANNING INPUT...
647
+ </div>
648
+
649
+ <!-- Error state -->
650
+ <div class="error-block" id="errorBlock">
651
+ <div class="error-title">⚠ CLASSIFICATION FAILED</div>
652
+ <div class="error-msg" id="errorMsg"></div>
653
+ </div>
654
+
655
+ <!-- Result -->
656
+ <div class="result" id="resultBlock">
657
+
658
+ <div class="cwe-header">
659
+ <div>
660
+ <div class="cwe-id" id="cweId">β€”</div>
661
+ <div class="cwe-name" id="cweName">β€”</div>
662
+ </div>
663
+ <div class="severity-badge" id="severityBadge">β€”</div>
664
+ </div>
665
+
666
+ <div class="confidence-row">
667
+ <span class="conf-label">CONFIDENCE</span>
668
+ <div class="conf-bar-track">
669
+ <div class="conf-bar-fill" id="confBar"></div>
670
+ </div>
671
+ <span class="conf-value" id="confValue">β€”</span>
672
+ </div>
673
+
674
+ <div class="description-block">
675
+ <div class="block-label">Description</div>
676
+ <div class="description-text" id="descText">β€”</div>
677
+ </div>
678
+
679
+ <div class="alternatives-block">
680
+ <div class="block-label">Alternatives</div>
681
+ <div class="alt-list" id="altList"></div>
682
+ </div>
683
+
684
+ <div class="atlas-block" id="atlasBlock">
685
+ <div class="block-label">ATLAS Match</div>
686
+ <div class="atlas-id" id="atlasId">β€”</div>
687
+ <div class="atlas-technique" id="atlasTechnique">β€”</div>
688
+ <div class="atlas-reasoning" id="atlasReasoning">β€”</div>
689
+ <div class="atlas-conf" id="atlasConf">β€”</div>
690
+ </div>
691
+
692
+ <div class="warning-block" id="warningBlock">
693
+ <div class="warning-text" id="warningText">β€”</div>
694
+ </div>
695
+
696
+ <div class="meta-row">
697
+ <span class="meta-tag" id="inputTypeTag">β€”</span>
698
+ <span class="meta-time" id="metaTime">β€”</span>
699
+ </div>
700
+
701
+ </div>
702
+ </div>
703
+ </div>
704
+ </main>
705
+
706
+ <footer>
707
+ <span class="footer-note">CWE Top 25 Β· MITRE ATLAS Β· RoBERTa + Qwen2.5-Coder</span>
708
+ <div class="footer-links">
709
+ <a href="https://github.com/martynattakit/AIB5-CodeSentinel" target="_blank">GitHub</a>
710
+ <a href="https://huggingface.co/martynattakit" target="_blank">HF Hub</a>
711
+ <a href="https://atlas.mitre.org" target="_blank">MITRE ATLAS</a>
712
+ </div>
713
+ </footer>
714
+
715
+ <script>
716
+ // ── Config ──────────────────────────────────────────────────────────────────
717
+ // Update this to your API URL when deployed on HF Spaces
718
+ const API_BASE = window.location.origin;
719
+
720
+ // ── Char counter ────────────────────────────────────────────────────────────
721
+ const input = document.getElementById('input');
722
+ const charCount = document.getElementById('charCount');
723
+
724
+ input.addEventListener('input', () => {
725
+ const n = input.value.length;
726
+ charCount.textContent = `${n.toLocaleString()} / 8000`;
727
+ charCount.style.color = n > 7000 ? 'var(--accent2)' : 'var(--muted)';
728
+ });
729
+
730
+ // ── Keyboard shortcut: Cmd/Ctrl+Enter ───────────────────────────────────────
731
+ input.addEventListener('keydown', e => {
732
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') classify();
733
+ });
734
+
735
+ // ── State helpers ────────────────────────────────────────────────────────────
736
+ function showState(state) {
737
+ document.getElementById('emptyState').style.display = state === 'empty' ? 'flex' : 'none';
738
+ document.getElementById('scanningState').style.display = state === 'scanning' ? 'block' : 'none';
739
+ document.getElementById('errorBlock').classList.toggle('visible', state === 'error');
740
+ document.getElementById('resultBlock').classList.toggle('visible', state === 'result');
741
+ }
742
+
743
+ // ── Classify ─────────────────────────────────────────────────────────────────
744
+ async function classify() {
745
+ const text = input.value.trim();
746
+ if (!text) return;
747
+
748
+ const btn = document.getElementById('classifyBtn');
749
+ btn.disabled = true;
750
+ btn.classList.add('loading');
751
+ btn.textContent = 'SCANNING...';
752
+ showState('scanning');
753
+
754
+ try {
755
+ const res = await fetch(`${API_BASE}/classify`, {
756
+ method: 'POST',
757
+ headers: { 'Content-Type': 'application/json' },
758
+ body: JSON.stringify({ input: text }),
759
+ });
760
+
761
+ if (!res.ok) {
762
+ const err = await res.json().catch(() => ({ detail: res.statusText }));
763
+ throw new Error(err.detail || 'Classification failed');
764
+ }
765
+
766
+ const data = await res.json();
767
+ renderResult(data);
768
+
769
+ } catch (err) {
770
+ document.getElementById('errorMsg').textContent = err.message;
771
+ showState('error');
772
+ } finally {
773
+ btn.disabled = false;
774
+ btn.classList.remove('loading');
775
+ btn.textContent = 'CLASSIFY β†’';
776
+ }
777
+ }
778
+
779
+ // ── Render result ────────────────────────────────────────────────────────────
780
+ function renderResult(d) {
781
+ // CWE header
782
+ document.getElementById('cweId').textContent = d.cwe_id;
783
+ document.getElementById('cweName').textContent = d.cwe_name;
784
+
785
+ const sevBadge = document.getElementById('severityBadge');
786
+ sevBadge.textContent = d.severity;
787
+ sevBadge.className = `severity-badge severity-${d.severity}`;
788
+
789
+ // Confidence bar β€” animate after paint
790
+ const confBar = document.getElementById('confBar');
791
+ const pct = Math.round(d.confidence * 100);
792
+ document.getElementById('confValue').textContent = `${pct}%`;
793
+ confBar.style.width = '0%';
794
+ requestAnimationFrame(() => {
795
+ setTimeout(() => { confBar.style.width = `${pct}%`; }, 50);
796
+ });
797
+
798
+ // Description
799
+ document.getElementById('descText').textContent = d.description;
800
+
801
+ // Alternatives
802
+ const altList = document.getElementById('altList');
803
+ altList.innerHTML = '';
804
+ (d.alternatives || []).forEach(alt => {
805
+ const pct = Math.round(alt.confidence * 100);
806
+ const item = document.createElement('div');
807
+ item.className = 'alt-item';
808
+ item.innerHTML = `
809
+ <span class="alt-cwe">${alt.cwe_id}</span>
810
+ <div class="alt-bar-track">
811
+ <div class="alt-bar-fill" style="width:0%" data-target="${pct}"></div>
812
+ </div>
813
+ <span class="alt-score">${pct}%</span>
814
+ `;
815
+ altList.appendChild(item);
816
+ });
817
+ // Animate alt bars
818
+ requestAnimationFrame(() => {
819
+ setTimeout(() => {
820
+ document.querySelectorAll('.alt-bar-fill').forEach(el => {
821
+ el.style.width = el.dataset.target + '%';
822
+ });
823
+ }, 100);
824
+ });
825
+
826
+ // ATLAS
827
+ const atlasBlock = document.getElementById('atlasBlock');
828
+ if (d.atlas_match) {
829
+ atlasBlock.classList.add('visible');
830
+ document.getElementById('atlasId').textContent = d.atlas_match.atlas_id;
831
+ document.getElementById('atlasTechnique').textContent = d.atlas_match.technique;
832
+ document.getElementById('atlasReasoning').textContent = d.atlas_match.reasoning;
833
+ document.getElementById('atlasConf').textContent = `CONFIDENCE: ${d.atlas_match.confidence}`;
834
+ } else {
835
+ atlasBlock.classList.remove('visible');
836
+ }
837
+
838
+ // Warning
839
+ const warnBlock = document.getElementById('warningBlock');
840
+ if (d.warning) {
841
+ warnBlock.classList.add('visible');
842
+ document.getElementById('warningText').textContent = `⚠ ${d.warning}`;
843
+ } else {
844
+ warnBlock.classList.remove('visible');
845
+ }
846
+
847
+ // Meta
848
+ document.getElementById('inputTypeTag').textContent = `INPUT: ${d.input_type.toUpperCase()}`;
849
+ document.getElementById('metaTime').textContent = `${d.elapsed_s}s`;
850
+
851
+ showState('result');
852
+ }
853
+ </script>
854
+ </body>
855
+ </html>
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API
2
+ fastapi==0.115.0
3
+ uvicorn[standard]==0.30.6
4
+ pydantic==2.9.2
5
+
6
+ # ML
7
+ torch==2.4.1
8
+ transformers==4.46.0
9
+ peft==0.13.0
10
+ bitsandbytes==0.44.1
11
+ accelerate==1.0.1
12
+ datasets==3.0.1
13
+ sentence-transformers==3.1.1
14
+
15
+ # Utils
16
+ huggingface_hub==0.25.2