diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index b2cac38..89c5f7a 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -17,7 +17,7 @@ from pydantic import BaseModel from sqlalchemy.orm import Session from app.database import SessionLocal, get_db -from app.dependencies.auth import require_role +from app.dependencies.auth import require_role, get_current_user from app.models.user import User from app.services.mitre_sync_service import sync_mitre from app.services.intel_service import scan_intel @@ -285,11 +285,13 @@ _JIRA_KEYS = { @router.get("/jira-config", response_model=JiraConfigOut) def get_jira_config( db: Session = Depends(get_db), - current_user: User = Depends(require_role("admin")), + current_user: User = Depends(get_current_user), ): """Return current Jira configuration (merged DB + env). - **Requires** the ``admin`` role. Credential values are never returned. + Open to any authenticated user — the frontend needs ``url`` to build + correct ``/browse/{key}`` links for non-admin users too. Credential + values (tokens, admin email) are never returned, only booleans. """ from app.services.jira_service import ( get_jira_url, get_jira_project_key, is_jira_enabled, diff --git a/backend/app/services/jira_service.py b/backend/app/services/jira_service.py index 43b8ca2..2f5d371 100644 --- a/backend/app/services/jira_service.py +++ b/backend/app/services/jira_service.py @@ -700,7 +700,7 @@ def auto_create_test_issue( poc = test.procedure_text or "N/A" severity = _technique_severity(technique).capitalize() - labels = ["aegis", "security-test", mitre_id.replace(".", "-")] + labels = ["aegis", mitre_id.replace(".", "-")] if test.platform: # Jira labels can't contain whitespace — normalize to kebab-case. labels.append(re.sub(r"[^a-z0-9_-]+", "-", test.platform.lower()).strip("-")) diff --git a/backend/tests/test_jira_config_endpoint.py b/backend/tests/test_jira_config_endpoint.py new file mode 100644 index 0000000..dc6781c --- /dev/null +++ b/backend/tests/test_jira_config_endpoint.py @@ -0,0 +1,21 @@ +"""GET /system/jira-config must be readable by any authenticated user. + +Regression: it used to be admin-only, so non-admin users (e.g. red_lead) +got a 403 and the frontend silently fell back to a hardcoded +"https://jira.atlassian.com" base URL when building ticket links. +""" + + +def test_non_admin_can_read_jira_config(client, red_lead_headers): + resp = client.get("/api/v1/system/jira-config", headers=red_lead_headers) + assert resp.status_code == 200, resp.text + assert "url" in resp.json() + + +def test_non_admin_cannot_update_jira_config(client, red_lead_headers): + resp = client.patch( + "/api/v1/system/jira-config", + json={"url": "https://evil.example.com"}, + headers=red_lead_headers, + ) + assert resp.status_code == 403 diff --git a/backend/tests/test_jira_service.py b/backend/tests/test_jira_service.py index fa6cbd2..2865c1d 100644 --- a/backend/tests/test_jira_service.py +++ b/backend/tests/test_jira_service.py @@ -414,11 +414,11 @@ def test_auto_create_test_issue_adds_platform_label( jira_service.auto_create_test_issue(db, test, actor, technique=technique) labels = mock_jira.issue_create.call_args.kwargs["fields"]["labels"] - assert ["aegis", "security-test", "T1003-006"] == labels[:3] + assert ["aegis", "T1003-006"] == labels[:2] if expected_label: assert expected_label in labels else: - assert len(labels) == 3 + assert len(labels) == 2 def _make_user(**overrides): diff --git a/frontend/src/pages/TestCatalogPage.tsx b/frontend/src/pages/TestCatalogPage.tsx index 11e48de..9230664 100644 --- a/frontend/src/pages/TestCatalogPage.tsx +++ b/frontend/src/pages/TestCatalogPage.tsx @@ -19,7 +19,8 @@ import { useAuth } from "../context/AuthContext"; // ── Constants ────────────────────────────────────────────────────── -const PAGE_SIZE = 12; +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; +const DEFAULT_PAGE_SIZE = 25; const SOURCE_OPTIONS = [ { value: "", label: "All Sources" }, @@ -89,7 +90,9 @@ export default function TestCatalogPage() { const [source, setSource] = useState(searchParams.get("source") || ""); const [platform, setPlatform] = useState(searchParams.get("platform") || ""); const [severity, setSeverity] = useState(searchParams.get("severity") || ""); + const [hideCovered, setHideCovered] = useState(searchParams.get("hide_covered") === "1"); const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); // Build filters const filters = { @@ -97,15 +100,21 @@ export default function TestCatalogPage() { source: source || undefined, platform: platform || undefined, severity: severity || undefined, - offset: page * PAGE_SIZE, - limit: PAGE_SIZE, + offset: page * pageSize, + limit: pageSize, }; - const { data: templates = [], isLoading } = useQuery({ + const { data: allTemplates = [], isLoading } = useQuery({ queryKey: ["test-templates", filters], queryFn: () => getTemplates(filters), }); + // "No tests yet" is a client-side filter — existing_test_count is already + // returned per-template, no need for a separate backend round-trip. + const templates = hideCovered + ? allTemplates.filter((t) => t.existing_test_count === 0) + : allTemplates; + // ── Filter handlers ────────────────────────────────────────────── const applyFilters = () => { @@ -115,6 +124,7 @@ export default function TestCatalogPage() { if (source) params.set("source", source); if (platform) params.set("platform", platform); if (severity) params.set("severity", severity); + if (hideCovered) params.set("hide_covered", "1"); setSearchParams(params); }; @@ -123,11 +133,12 @@ export default function TestCatalogPage() { setSource(""); setPlatform(""); setSeverity(""); + setHideCovered(false); setPage(0); setSearchParams({}); }; - const hasActiveFilters = search || source || platform || severity; + const hasActiveFilters = search || source || platform || severity || hideCovered; // ── Render ─────────────────────────────────────────────────────── @@ -220,6 +231,17 @@ export default function TestCatalogPage() { )} + + {/* No-coverage-yet toggle */} + {/* Results */} @@ -253,11 +275,26 @@ export default function TestCatalogPage() { {/* Pagination */}
-

- Showing {page * PAGE_SIZE + 1} - {" - "} - {page * PAGE_SIZE + templates.length} -

+
+

+ Showing {page * pageSize + 1} + {" - "} + {page * pageSize + allTemplates.length} + {hideCovered && templates.length !== allTemplates.length && ` (${templates.length} without existing tests)`} +

+ +