Spaces:
Configuration error
Configuration error
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -31,6 +31,9 @@ except ImportError:
|
|
| 31 |
import json
|
| 32 |
import logging
|
| 33 |
import os
|
|
|
|
|
|
|
|
|
|
| 34 |
from importlib import metadata, util
|
| 35 |
from typing import Any
|
| 36 |
|
|
@@ -41,6 +44,7 @@ from src.veris_classifier.classifier import (
|
|
| 41 |
answer_question,
|
| 42 |
classify_incident,
|
| 43 |
)
|
|
|
|
| 44 |
|
| 45 |
load_dotenv()
|
| 46 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -276,6 +280,17 @@ textarea:focus {
|
|
| 276 |
text-decoration: none;
|
| 277 |
}
|
| 278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
/* Mobile */
|
| 280 |
@media (max-width: 900px) {
|
| 281 |
.hero-section {
|
|
@@ -299,6 +314,12 @@ textarea:focus {
|
|
| 299 |
.hero-badges {
|
| 300 |
gap: 8px;
|
| 301 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
"""
|
| 304 |
|
|
@@ -330,6 +351,8 @@ EXAMPLES_QA = [
|
|
| 330 |
ZEROGPU_QUEUE_HINT = "No GPU was available after"
|
| 331 |
SPACES_PAGE_URL = "https://huggingface.co/spaces/vibesecurityguy/veris-classifier"
|
| 332 |
SPACE_HOST_URL = "https://vibesecurityguy-veris-classifier.hf.space"
|
|
|
|
|
|
|
| 333 |
|
| 334 |
|
| 335 |
def _is_zerogpu_queue_timeout(err: Exception) -> bool:
|
|
@@ -355,6 +378,57 @@ def _spaces_user_logged_in(
|
|
| 355 |
return False
|
| 356 |
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
def _use_hf_model() -> bool:
|
| 360 |
"""Check if we should use the fine-tuned HF model."""
|
|
@@ -393,7 +467,7 @@ def classify(
|
|
| 393 |
|
| 394 |
if use_hf:
|
| 395 |
try:
|
| 396 |
-
result = _classify_gpu(description)
|
| 397 |
return json.dumps(result, indent=2)
|
| 398 |
except Exception as e:
|
| 399 |
logger.error(f"HF model error: {e}")
|
|
@@ -445,7 +519,7 @@ def ask(
|
|
| 445 |
|
| 446 |
if use_hf:
|
| 447 |
try:
|
| 448 |
-
return _ask_gpu(question)
|
| 449 |
except Exception as e:
|
| 450 |
logger.error(f"HF model error: {e}")
|
| 451 |
if _is_zerogpu_queue_timeout(e):
|
|
@@ -515,31 +589,150 @@ def _classification_rows_from_json(raw_json: str) -> list[list[str]]:
|
|
| 515 |
return rows
|
| 516 |
|
| 517 |
|
| 518 |
-
def
|
| 519 |
-
"""
|
| 520 |
-
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
return (
|
| 523 |
gr.update(value=raw_json, visible=False),
|
| 524 |
-
gr.update(value=
|
|
|
|
|
|
|
| 525 |
)
|
|
|
|
| 526 |
return (
|
| 527 |
gr.update(value=raw_json, visible=True),
|
| 528 |
gr.update(value=[], visible=False),
|
|
|
|
|
|
|
| 529 |
)
|
| 530 |
|
| 531 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
def classify_and_render(
|
| 533 |
description: str,
|
| 534 |
api_key: str,
|
| 535 |
output_format: str,
|
|
|
|
|
|
|
| 536 |
request: gr.Request | None = None,
|
| 537 |
profile: gr.OAuthProfile | None = None,
|
| 538 |
):
|
| 539 |
"""Run classification and return display-ready outputs."""
|
| 540 |
raw_json = classify(description, api_key, request=request, profile=profile)
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
|
| 545 |
# ---------------------------------------------------------------------------
|
|
@@ -622,6 +815,7 @@ def build_app() -> gr.Blocks:
|
|
| 622 |
theme=THEME,
|
| 623 |
css=CUSTOM_CSS,
|
| 624 |
) as app:
|
|
|
|
| 625 |
|
| 626 |
# --- Hero Header ---
|
| 627 |
gr.HTML("""
|
|
@@ -702,6 +896,10 @@ def build_app() -> gr.Blocks:
|
|
| 702 |
'border:1px solid #334155;color:#cbd5e1;text-decoration:none;font-weight:600;">'
|
| 703 |
"Direct sign-in (if button refreshes)</a>"
|
| 704 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
else:
|
| 706 |
gr.HTML("""
|
| 707 |
<div class="model-banner">
|
|
@@ -760,12 +958,38 @@ def build_app() -> gr.Blocks:
|
|
| 760 |
info="Switch between raw JSON and a flattened table view.",
|
| 761 |
)
|
| 762 |
last_classification_raw = gr.State("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
classification_output = gr.Code(
|
| 764 |
label="VERIS Classification (JSON)",
|
| 765 |
language="json",
|
| 766 |
lines=20,
|
| 767 |
elem_classes=["code-output"],
|
| 768 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
classification_table = gr.Dataframe(
|
| 770 |
headers=["Dimension", "Field", "Value"],
|
| 771 |
datatype=["str", "str", "str"],
|
|
@@ -774,6 +998,7 @@ def build_app() -> gr.Blocks:
|
|
| 774 |
visible=False,
|
| 775 |
interactive=False,
|
| 776 |
wrap=True,
|
|
|
|
| 777 |
label="VERIS Classification (Table)",
|
| 778 |
)
|
| 779 |
|
|
@@ -787,13 +1012,43 @@ def build_app() -> gr.Blocks:
|
|
| 787 |
|
| 788 |
classify_btn.click(
|
| 789 |
fn=classify_and_render,
|
| 790 |
-
inputs=[incident_input, api_key, output_format],
|
| 791 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
)
|
| 793 |
output_format.change(
|
| 794 |
fn=_render_classification_output,
|
| 795 |
-
inputs=[
|
| 796 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
)
|
| 798 |
|
| 799 |
# ---- TAB 2: Q&A ----
|
|
@@ -946,6 +1201,13 @@ def build_app() -> gr.Blocks:
|
|
| 946 |
</div>
|
| 947 |
""")
|
| 948 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
return app
|
| 950 |
|
| 951 |
|
|
|
|
| 31 |
import json
|
| 32 |
import logging
|
| 33 |
import os
|
| 34 |
+
import csv
|
| 35 |
+
import tempfile
|
| 36 |
+
import time
|
| 37 |
from importlib import metadata, util
|
| 38 |
from typing import Any
|
| 39 |
|
|
|
|
| 44 |
answer_question,
|
| 45 |
classify_incident,
|
| 46 |
)
|
| 47 |
+
from src.veris_classifier.validator import validate_classification
|
| 48 |
|
| 49 |
load_dotenv()
|
| 50 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 280 |
text-decoration: none;
|
| 281 |
}
|
| 282 |
|
| 283 |
+
.status-card {
|
| 284 |
+
border: 1px solid #334155;
|
| 285 |
+
background: rgba(15, 23, 42, 0.6);
|
| 286 |
+
border-radius: 10px;
|
| 287 |
+
padding: 8px 12px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
#table-controls .wrap {
|
| 291 |
+
align-items: end;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
/* Mobile */
|
| 295 |
@media (max-width: 900px) {
|
| 296 |
.hero-section {
|
|
|
|
| 314 |
.hero-badges {
|
| 315 |
gap: 8px;
|
| 316 |
}
|
| 317 |
+
.primary-btn {
|
| 318 |
+
width: 100% !important;
|
| 319 |
+
}
|
| 320 |
+
#table-controls .wrap {
|
| 321 |
+
gap: 8px !important;
|
| 322 |
+
}
|
| 323 |
}
|
| 324 |
"""
|
| 325 |
|
|
|
|
| 351 |
ZEROGPU_QUEUE_HINT = "No GPU was available after"
|
| 352 |
SPACES_PAGE_URL = "https://huggingface.co/spaces/vibesecurityguy/veris-classifier"
|
| 353 |
SPACE_HOST_URL = "https://vibesecurityguy-veris-classifier.hf.space"
|
| 354 |
+
ZEROGPU_RETRY_ATTEMPTS = 2
|
| 355 |
+
ZEROGPU_RETRY_DELAY_SECONDS = 3
|
| 356 |
|
| 357 |
|
| 358 |
def _is_zerogpu_queue_timeout(err: Exception) -> bool:
|
|
|
|
| 378 |
return False
|
| 379 |
|
| 380 |
|
| 381 |
+
def _session_status_markdown(
|
| 382 |
+
request: gr.Request | None = None,
|
| 383 |
+
profile: gr.OAuthProfile | None = None,
|
| 384 |
+
) -> str:
|
| 385 |
+
"""Render current Spaces auth status for the user."""
|
| 386 |
+
if not IS_SPACES:
|
| 387 |
+
return ""
|
| 388 |
+
|
| 389 |
+
if _spaces_user_logged_in(request, profile):
|
| 390 |
+
username = None
|
| 391 |
+
if profile is not None:
|
| 392 |
+
username = (
|
| 393 |
+
getattr(profile, "preferred_username", None)
|
| 394 |
+
or getattr(profile, "name", None)
|
| 395 |
+
)
|
| 396 |
+
if not username and request is not None:
|
| 397 |
+
username = getattr(request, "username", None)
|
| 398 |
+
if username:
|
| 399 |
+
return (
|
| 400 |
+
f"**Session status:** Logged in as `{username}`. "
|
| 401 |
+
"ZeroGPU requests will use your account quota."
|
| 402 |
+
)
|
| 403 |
+
return "**Session status:** Logged in. ZeroGPU requests will use your account quota."
|
| 404 |
+
|
| 405 |
+
return (
|
| 406 |
+
"**Session status:** Not logged in. Click sign in to attach this browser session "
|
| 407 |
+
"to your Hugging Face quota."
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def _run_with_zerogpu_retry(call):
|
| 412 |
+
"""Retry queue-timeout failures once before returning an error."""
|
| 413 |
+
last_error = None
|
| 414 |
+
for attempt in range(1, ZEROGPU_RETRY_ATTEMPTS + 1):
|
| 415 |
+
try:
|
| 416 |
+
return call()
|
| 417 |
+
except Exception as e:
|
| 418 |
+
last_error = e
|
| 419 |
+
if _is_zerogpu_queue_timeout(e) and attempt < ZEROGPU_RETRY_ATTEMPTS:
|
| 420 |
+
logger.warning(
|
| 421 |
+
"ZeroGPU queue timeout (attempt %d/%d). Retrying in %ss.",
|
| 422 |
+
attempt,
|
| 423 |
+
ZEROGPU_RETRY_ATTEMPTS,
|
| 424 |
+
ZEROGPU_RETRY_DELAY_SECONDS,
|
| 425 |
+
)
|
| 426 |
+
time.sleep(ZEROGPU_RETRY_DELAY_SECONDS)
|
| 427 |
+
continue
|
| 428 |
+
raise
|
| 429 |
+
raise last_error
|
| 430 |
+
|
| 431 |
+
|
| 432 |
|
| 433 |
def _use_hf_model() -> bool:
|
| 434 |
"""Check if we should use the fine-tuned HF model."""
|
|
|
|
| 467 |
|
| 468 |
if use_hf:
|
| 469 |
try:
|
| 470 |
+
result = _run_with_zerogpu_retry(lambda: _classify_gpu(description))
|
| 471 |
return json.dumps(result, indent=2)
|
| 472 |
except Exception as e:
|
| 473 |
logger.error(f"HF model error: {e}")
|
|
|
|
| 519 |
|
| 520 |
if use_hf:
|
| 521 |
try:
|
| 522 |
+
return _run_with_zerogpu_retry(lambda: _ask_gpu(question))
|
| 523 |
except Exception as e:
|
| 524 |
logger.error(f"HF model error: {e}")
|
| 525 |
if _is_zerogpu_queue_timeout(e):
|
|
|
|
| 589 |
return rows
|
| 590 |
|
| 591 |
|
| 592 |
+
def _validation_summary_markdown(raw_json: str) -> str:
|
| 593 |
+
"""Build validation summary for the classification output."""
|
| 594 |
+
try:
|
| 595 |
+
parsed = json.loads(raw_json)
|
| 596 |
+
except Exception:
|
| 597 |
+
return ""
|
| 598 |
+
|
| 599 |
+
if not isinstance(parsed, dict) or parsed.get("error"):
|
| 600 |
+
return "**Validation:** Skipped."
|
| 601 |
+
|
| 602 |
+
result = validate_classification(parsed)
|
| 603 |
+
lines = [f"**Validation:** {'Passed' if result.valid else 'Issues found'}"]
|
| 604 |
+
if result.errors:
|
| 605 |
+
lines.append("**Errors**")
|
| 606 |
+
lines.extend(f"- {err}" for err in result.errors[:8])
|
| 607 |
+
if len(result.errors) > 8:
|
| 608 |
+
lines.append(f"- ... {len(result.errors) - 8} more")
|
| 609 |
+
if result.warnings:
|
| 610 |
+
lines.append("**Warnings**")
|
| 611 |
+
lines.extend(f"- {warn}" for warn in result.warnings[:8])
|
| 612 |
+
if len(result.warnings) > 8:
|
| 613 |
+
lines.append(f"- ... {len(result.warnings) - 8} more")
|
| 614 |
+
return "\n".join(lines)
|
| 615 |
+
|
| 616 |
+
|
| 617 |
+
def _filter_classification_rows(
|
| 618 |
+
rows: list[list[str]],
|
| 619 |
+
dimension_filter: str,
|
| 620 |
+
errors_only: bool,
|
| 621 |
+
) -> list[list[str]]:
|
| 622 |
+
"""Filter table rows by dimension and optionally error-only rows."""
|
| 623 |
+
filtered: list[list[str]] = []
|
| 624 |
+
for row in rows:
|
| 625 |
+
if len(row) != 3:
|
| 626 |
+
continue
|
| 627 |
+
dimension, field, value = row
|
| 628 |
+
|
| 629 |
+
if dimension_filter != "All" and dimension != dimension_filter:
|
| 630 |
+
continue
|
| 631 |
+
|
| 632 |
+
if errors_only:
|
| 633 |
+
blob = f"{dimension} {field} {value}".lower()
|
| 634 |
+
if "error" not in blob:
|
| 635 |
+
continue
|
| 636 |
+
|
| 637 |
+
filtered.append(row)
|
| 638 |
+
return filtered
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def _render_classification_output(
|
| 642 |
+
raw_json: str,
|
| 643 |
+
output_format: str,
|
| 644 |
+
all_rows: list[list[str]],
|
| 645 |
+
dimension_filter: str,
|
| 646 |
+
errors_only: bool,
|
| 647 |
+
):
|
| 648 |
+
"""Render classification as JSON code or filtered table."""
|
| 649 |
+
filtered_rows = _filter_classification_rows(all_rows, dimension_filter, errors_only)
|
| 650 |
+
show_table = output_format == "Table"
|
| 651 |
+
|
| 652 |
+
if show_table:
|
| 653 |
return (
|
| 654 |
gr.update(value=raw_json, visible=False),
|
| 655 |
+
gr.update(value=filtered_rows, visible=True),
|
| 656 |
+
gr.update(visible=True),
|
| 657 |
+
gr.update(visible=True, interactive=bool(filtered_rows)),
|
| 658 |
)
|
| 659 |
+
|
| 660 |
return (
|
| 661 |
gr.update(value=raw_json, visible=True),
|
| 662 |
gr.update(value=[], visible=False),
|
| 663 |
+
gr.update(visible=False),
|
| 664 |
+
gr.update(visible=False, interactive=False),
|
| 665 |
)
|
| 666 |
|
| 667 |
|
| 668 |
+
def _apply_table_filters(
|
| 669 |
+
all_rows: list[list[str]],
|
| 670 |
+
dimension_filter: str,
|
| 671 |
+
errors_only: bool,
|
| 672 |
+
):
|
| 673 |
+
"""Apply table-only filters without re-running inference."""
|
| 674 |
+
filtered_rows = _filter_classification_rows(all_rows, dimension_filter, errors_only)
|
| 675 |
+
return (
|
| 676 |
+
gr.update(value=filtered_rows),
|
| 677 |
+
gr.update(interactive=bool(filtered_rows)),
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
def _build_filtered_csv(
|
| 682 |
+
all_rows: list[list[str]],
|
| 683 |
+
dimension_filter: str,
|
| 684 |
+
errors_only: bool,
|
| 685 |
+
):
|
| 686 |
+
"""Create downloadable CSV file for filtered rows."""
|
| 687 |
+
filtered_rows = _filter_classification_rows(all_rows, dimension_filter, errors_only)
|
| 688 |
+
if not filtered_rows:
|
| 689 |
+
return gr.update(value=None, visible=False)
|
| 690 |
+
|
| 691 |
+
with tempfile.NamedTemporaryFile(
|
| 692 |
+
mode="w",
|
| 693 |
+
suffix=".csv",
|
| 694 |
+
delete=False,
|
| 695 |
+
newline="",
|
| 696 |
+
encoding="utf-8",
|
| 697 |
+
) as tmp:
|
| 698 |
+
writer = csv.writer(tmp)
|
| 699 |
+
writer.writerow(["Dimension", "Field", "Value"])
|
| 700 |
+
writer.writerows(filtered_rows)
|
| 701 |
+
csv_path = tmp.name
|
| 702 |
+
|
| 703 |
+
return gr.update(value=csv_path, visible=True)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
def classify_and_render(
|
| 707 |
description: str,
|
| 708 |
api_key: str,
|
| 709 |
output_format: str,
|
| 710 |
+
dimension_filter: str,
|
| 711 |
+
errors_only: bool,
|
| 712 |
request: gr.Request | None = None,
|
| 713 |
profile: gr.OAuthProfile | None = None,
|
| 714 |
):
|
| 715 |
"""Run classification and return display-ready outputs."""
|
| 716 |
raw_json = classify(description, api_key, request=request, profile=profile)
|
| 717 |
+
all_rows = _classification_rows_from_json(raw_json)
|
| 718 |
+
validation_md = _validation_summary_markdown(raw_json)
|
| 719 |
+
code_update, table_update, controls_update, export_btn_update = _render_classification_output(
|
| 720 |
+
raw_json,
|
| 721 |
+
output_format,
|
| 722 |
+
all_rows,
|
| 723 |
+
dimension_filter,
|
| 724 |
+
errors_only,
|
| 725 |
+
)
|
| 726 |
+
return (
|
| 727 |
+
raw_json,
|
| 728 |
+
all_rows,
|
| 729 |
+
validation_md,
|
| 730 |
+
code_update,
|
| 731 |
+
table_update,
|
| 732 |
+
controls_update,
|
| 733 |
+
export_btn_update,
|
| 734 |
+
gr.update(value=None, visible=False),
|
| 735 |
+
)
|
| 736 |
|
| 737 |
|
| 738 |
# ---------------------------------------------------------------------------
|
|
|
|
| 815 |
theme=THEME,
|
| 816 |
css=CUSTOM_CSS,
|
| 817 |
) as app:
|
| 818 |
+
session_status = None
|
| 819 |
|
| 820 |
# --- Hero Header ---
|
| 821 |
gr.HTML("""
|
|
|
|
| 896 |
'border:1px solid #334155;color:#cbd5e1;text-decoration:none;font-weight:600;">'
|
| 897 |
"Direct sign-in (if button refreshes)</a>"
|
| 898 |
)
|
| 899 |
+
session_status = gr.Markdown(
|
| 900 |
+
value="**Session status:** Checking...",
|
| 901 |
+
elem_classes=["status-card"],
|
| 902 |
+
)
|
| 903 |
else:
|
| 904 |
gr.HTML("""
|
| 905 |
<div class="model-banner">
|
|
|
|
| 958 |
info="Switch between raw JSON and a flattened table view.",
|
| 959 |
)
|
| 960 |
last_classification_raw = gr.State("")
|
| 961 |
+
classification_rows = gr.State([])
|
| 962 |
+
validation_output = gr.Markdown(
|
| 963 |
+
label="Validation",
|
| 964 |
+
value="*Validation summary will appear after classification.*",
|
| 965 |
+
)
|
| 966 |
classification_output = gr.Code(
|
| 967 |
label="VERIS Classification (JSON)",
|
| 968 |
language="json",
|
| 969 |
lines=20,
|
| 970 |
elem_classes=["code-output"],
|
| 971 |
)
|
| 972 |
+
with gr.Row(visible=False, elem_id="table-controls") as table_controls:
|
| 973 |
+
dimension_filter = gr.Dropdown(
|
| 974 |
+
choices=["All", "Actor", "Action", "Asset", "Attribute", "Error", "General"],
|
| 975 |
+
value="All",
|
| 976 |
+
label="Filter Dimension",
|
| 977 |
+
)
|
| 978 |
+
errors_only = gr.Checkbox(
|
| 979 |
+
value=False,
|
| 980 |
+
label="Errors Only",
|
| 981 |
+
)
|
| 982 |
+
export_csv_btn = gr.Button(
|
| 983 |
+
"Generate CSV",
|
| 984 |
+
size="sm",
|
| 985 |
+
interactive=False,
|
| 986 |
+
visible=False,
|
| 987 |
+
)
|
| 988 |
+
csv_file = gr.File(
|
| 989 |
+
label="Download Filtered CSV",
|
| 990 |
+
visible=False,
|
| 991 |
+
interactive=False,
|
| 992 |
+
)
|
| 993 |
classification_table = gr.Dataframe(
|
| 994 |
headers=["Dimension", "Field", "Value"],
|
| 995 |
datatype=["str", "str", "str"],
|
|
|
|
| 998 |
visible=False,
|
| 999 |
interactive=False,
|
| 1000 |
wrap=True,
|
| 1001 |
+
max_height=500,
|
| 1002 |
label="VERIS Classification (Table)",
|
| 1003 |
)
|
| 1004 |
|
|
|
|
| 1012 |
|
| 1013 |
classify_btn.click(
|
| 1014 |
fn=classify_and_render,
|
| 1015 |
+
inputs=[incident_input, api_key, output_format, dimension_filter, errors_only],
|
| 1016 |
+
outputs=[
|
| 1017 |
+
last_classification_raw,
|
| 1018 |
+
classification_rows,
|
| 1019 |
+
validation_output,
|
| 1020 |
+
classification_output,
|
| 1021 |
+
classification_table,
|
| 1022 |
+
table_controls,
|
| 1023 |
+
export_csv_btn,
|
| 1024 |
+
csv_file,
|
| 1025 |
+
],
|
| 1026 |
)
|
| 1027 |
output_format.change(
|
| 1028 |
fn=_render_classification_output,
|
| 1029 |
+
inputs=[
|
| 1030 |
+
last_classification_raw,
|
| 1031 |
+
output_format,
|
| 1032 |
+
classification_rows,
|
| 1033 |
+
dimension_filter,
|
| 1034 |
+
errors_only,
|
| 1035 |
+
],
|
| 1036 |
+
outputs=[classification_output, classification_table, table_controls, export_csv_btn],
|
| 1037 |
+
)
|
| 1038 |
+
dimension_filter.change(
|
| 1039 |
+
fn=_apply_table_filters,
|
| 1040 |
+
inputs=[classification_rows, dimension_filter, errors_only],
|
| 1041 |
+
outputs=[classification_table, export_csv_btn],
|
| 1042 |
+
)
|
| 1043 |
+
errors_only.change(
|
| 1044 |
+
fn=_apply_table_filters,
|
| 1045 |
+
inputs=[classification_rows, dimension_filter, errors_only],
|
| 1046 |
+
outputs=[classification_table, export_csv_btn],
|
| 1047 |
+
)
|
| 1048 |
+
export_csv_btn.click(
|
| 1049 |
+
fn=_build_filtered_csv,
|
| 1050 |
+
inputs=[classification_rows, dimension_filter, errors_only],
|
| 1051 |
+
outputs=[csv_file],
|
| 1052 |
)
|
| 1053 |
|
| 1054 |
# ---- TAB 2: Q&A ----
|
|
|
|
| 1201 |
</div>
|
| 1202 |
""")
|
| 1203 |
|
| 1204 |
+
if IS_SPACES and session_status is not None:
|
| 1205 |
+
app.load(
|
| 1206 |
+
fn=_session_status_markdown,
|
| 1207 |
+
outputs=[session_status],
|
| 1208 |
+
queue=False,
|
| 1209 |
+
)
|
| 1210 |
+
|
| 1211 |
return app
|
| 1212 |
|
| 1213 |
|