feat(templates): add pagination with total counts to catalog and campaign picker
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Test Catalog's pagination showed only 'Page N' with no way to know how many pages existed, and the campaign 'Add Test' modal's template picker had no pagination at all — with 2800+ templates in the catalog, its flat 100-row fetch made most templates unreachable unless a search happened to match. Adds GET /test-templates/count (open to any authenticated user, mirrors the list endpoint's filters) and wires real 'Page N of M' + total-available counts into both.
This commit is contained in:
@@ -23,6 +23,42 @@ from app.models.test import Test
|
||||
from app.utils import escape_like
|
||||
|
||||
|
||||
def _build_template_query(
|
||||
db: Session,
|
||||
*,
|
||||
source: str | None = None,
|
||||
platform: str | None = None,
|
||||
severity: str | None = None,
|
||||
mitre_technique_id: str | None = None,
|
||||
search: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
):
|
||||
"""Build the filtered TestTemplate query shared by list_templates() and
|
||||
count_templates() — a single source of truth so a paginated page of
|
||||
results and the total count it's paginated against can never drift
|
||||
apart (same pattern used for tests in test_crud_service.py)."""
|
||||
query = db.query(TestTemplate)
|
||||
if is_active is not None:
|
||||
query = query.filter(TestTemplate.is_active == is_active)
|
||||
if source:
|
||||
query = query.filter(TestTemplate.source == source)
|
||||
if platform:
|
||||
query = query.filter(TestTemplate.platform.ilike(f"%{escape_like(platform)}%"))
|
||||
if severity:
|
||||
query = query.filter(TestTemplate.severity == severity)
|
||||
if mitre_technique_id:
|
||||
query = query.filter(TestTemplate.mitre_technique_id == mitre_technique_id)
|
||||
if search:
|
||||
pattern = f"%{escape_like(search)}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
TestTemplate.name.ilike(pattern),
|
||||
TestTemplate.description.ilike(pattern),
|
||||
)
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
# Define function list_templates
|
||||
def list_templates(
|
||||
# Entry: db
|
||||
@@ -46,51 +82,16 @@ def list_templates(
|
||||
limit: int = 50,
|
||||
) -> list:
|
||||
"""Return paginated, filterable list of test templates."""
|
||||
# Assign query = db.query(TestTemplate)
|
||||
query = db.query(TestTemplate)
|
||||
# Check: is_active is not None
|
||||
if is_active is not None:
|
||||
# Assign query = query.filter(TestTemplate.is_active == is_active)
|
||||
query = query.filter(TestTemplate.is_active == is_active)
|
||||
query = _build_template_query(
|
||||
db, source=source, platform=platform, severity=severity,
|
||||
mitre_technique_id=mitre_technique_id, search=search, is_active=is_active,
|
||||
)
|
||||
|
||||
# Check: source
|
||||
if source:
|
||||
# Assign query = query.filter(TestTemplate.source == source)
|
||||
query = query.filter(TestTemplate.source == source)
|
||||
# Check: platform
|
||||
if platform:
|
||||
# Assign query = query.filter(TestTemplate.platform.ilike(f"%{escape_like(platform)}...
|
||||
query = query.filter(TestTemplate.platform.ilike(f"%{escape_like(platform)}%"))
|
||||
# Check: severity
|
||||
if severity:
|
||||
# Assign query = query.filter(TestTemplate.severity == severity)
|
||||
query = query.filter(TestTemplate.severity == severity)
|
||||
# Check: mitre_technique_id
|
||||
if mitre_technique_id:
|
||||
# Assign query = query.filter(TestTemplate.mitre_technique_id == mitre_technique_id)
|
||||
query = query.filter(TestTemplate.mitre_technique_id == mitre_technique_id)
|
||||
# Check: search
|
||||
if search:
|
||||
# Assign pattern = f"%{escape_like(search)}%"
|
||||
pattern = f"%{escape_like(search)}%"
|
||||
# Assign query = query.filter(
|
||||
query = query.filter(
|
||||
or_(
|
||||
TestTemplate.name.ilike(pattern),
|
||||
TestTemplate.description.ilike(pattern),
|
||||
)
|
||||
)
|
||||
|
||||
# Assign templates = (
|
||||
templates = (
|
||||
query
|
||||
# Chain .order_by() call
|
||||
.order_by(TestTemplate.mitre_technique_id, TestTemplate.name)
|
||||
# Chain .offset() call
|
||||
.offset(offset)
|
||||
# Chain .limit() call
|
||||
.limit(limit)
|
||||
# Chain .all() call
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -108,10 +109,29 @@ def list_templates(
|
||||
for t in templates:
|
||||
t.existing_test_count = counts.get(t.mitre_technique_id, 0)
|
||||
|
||||
# Return templates
|
||||
return templates
|
||||
|
||||
|
||||
def count_templates(
|
||||
db: Session,
|
||||
*,
|
||||
source: str | None = None,
|
||||
platform: str | None = None,
|
||||
severity: str | None = None,
|
||||
mitre_technique_id: str | None = None,
|
||||
search: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
) -> int:
|
||||
"""Return the total count of templates matching the same filters as
|
||||
list_templates() — lets the catalog UI show a true page count even
|
||||
though the list endpoint caps how many rows it returns per request."""
|
||||
query = _build_template_query(
|
||||
db, source=source, platform=platform, severity=severity,
|
||||
mitre_technique_id=mitre_technique_id, search=search, is_active=is_active,
|
||||
)
|
||||
return query.count()
|
||||
|
||||
|
||||
# Define function get_template_stats
|
||||
def get_template_stats(db: Session) -> dict:
|
||||
"""Return catalog statistics: totals by source, platform, active/inactive."""
|
||||
|
||||
Reference in New Issue
Block a user