Major product upgrade: landing page, scanning UI, admin panel, working reports
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / docker-build (push) Has been cancelled
Test / backend-test (push) Has been cancelled
Test / frontend-test (push) Has been cancelled
Test / security-scan (push) Has been cancelled

Frontend:
- New public marketing landing page at / (hero, features, stats, how-it-works, CTA)
- New /scans page surfacing the scanning engine: asset health grid, one-click
  full scan, live scanner findings feed, 24h stats
- New /admin panel (was a 404 from the sidebar): audit report generation and
  instant PDF snapshot download
- Reports page: now visible to all roles, working PDF downloads, posture
  snapshot export for IT/admin
- Findings page: full-text search, sorting (severity/newest/title), severity
  count chips, CSV export
- Dashboard: scan activity strip, Run Scan + Export PDF quick actions
- Footprint page: summary stat cards
- api.ts: scanning, reports, and PDF download endpoints + types;
  fixed missing resolution_note on Finding type
- Removed unsupported eslint key from next.config.ts

Backend:
- Audit reports: list/get/PDF now open to executives and IT admins with strict
  tenant isolation (was trustos_admin-only, leaving tenants unable to see
  their own reports); PDF snapshot open to IT admins; generation stays
  admin-only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-07-07 14:42:24 +00:00
parent d92a7c057d
commit 2bdc085bc3
12 changed files with 1024 additions and 77 deletions

View File

@@ -9,7 +9,12 @@ import json
from app.db.session import get_db
from app.models.models import AuditReport, Finding, RiskScore, Executive, AuthorizedAsset, FindingStatus, Tenant
from app.schemas.schemas import AuditReportOut, AuditReportCreate
from app.core.security import require_admin
from app.core.security import require_admin, require_executive_or_above, require_it_or_above
def _check_tenant_access(payload: dict, tenant_id: str):
if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
router = APIRouter(prefix="/audit-reports", tags=["audit-reports"])
@@ -17,9 +22,10 @@ router = APIRouter(prefix="/audit-reports", tags=["audit-reports"])
@router.get("", response_model=List[AuditReportOut])
async def list_reports(
tenant_id: str = Query(...),
payload: dict = Depends(require_admin),
payload: dict = Depends(require_executive_or_above),
db: AsyncSession = Depends(get_db),
):
_check_tenant_access(payload, tenant_id)
result = await db.execute(
select(AuditReport)
.where(AuditReport.tenant_id == tenant_id)
@@ -89,26 +95,28 @@ async def generate_audit_report(
@router.get("/{report_id}", response_model=AuditReportOut)
async def get_report(
report_id: str,
payload: dict = Depends(require_admin),
payload: dict = Depends(require_executive_or_above),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(AuditReport).where(AuditReport.id == report_id))
report = result.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="Report not found")
_check_tenant_access(payload, report.tenant_id)
return report
@router.get("/{report_id}/pdf")
async def download_report_pdf(
report_id: str,
payload: dict = Depends(require_admin),
payload: dict = Depends(require_executive_or_above),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(AuditReport).where(AuditReport.id == report_id))
report = result.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="Report not found")
_check_tenant_access(payload, report.tenant_id)
tenant_result = await db.execute(select(Tenant).where(Tenant.id == report.tenant_id))
tenant = tenant_result.scalar_one_or_none()
@@ -142,10 +150,11 @@ async def download_report_pdf(
@router.post("/{tenant_id}/pdf-snapshot")
async def generate_pdf_snapshot(
tenant_id: str,
payload: dict = Depends(require_admin),
payload: dict = Depends(require_it_or_above),
db: AsyncSession = Depends(get_db),
):
"""Generate a one-off PDF report for a tenant (not stored as a record)."""
_check_tenant_access(payload, tenant_id)
tenant_result = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
tenant = tenant_result.scalar_one_or_none()
if not tenant: