Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions backend/secuscan/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
from fastapi import APIRouter

from typing import Any, Dict, Optional

_bearer_scheme = HTTPBearer(auto_error=False)
_api_key_header = APIKeyHeader(name="X-Api-Key", auto_error=False)

Expand Down Expand Up @@ -248,3 +250,17 @@ def resolve_owner_id(request: Request | None) -> str:
async def get_current_owner(request: Request) -> str:
"""FastAPI dependency yielding the owner identity for the request."""
return resolve_owner_id(request)

async def require_owned_task(db, task_id: str, owner: str, columns: str = "owner_id") -> Dict[str, Any]:
"""Fetch a task and enforce that it belongs to ``owner`` (issue #401).

Returns the selected row on success. Raises 404 when the task does not
exist and 403 when it is owned by a different user/workspace. ``columns``
must include ``owner_id`` so the ownership comparison can be made.
"""
row = await db.fetchone(f"SELECT {columns} FROM tasks WHERE id = ?", (task_id,))
if row is None:
raise HTTPException(status_code=404, detail="Task not found")
if row.get("owner_id") != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")
return row
135 changes: 76 additions & 59 deletions backend/secuscan/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _json_payload(value: Any, fallback: str) -> str:
from .reporting import reporting
from .vault import VaultCrypto
from .workflows import scheduler, _finalize_workflow_run
from .auth import require_api_key, get_current_owner
from .auth import require_api_key, get_current_owner, require_owned_task
from .execution_context import is_offensive_validation, normalize_execution_context
from .finding_intelligence import build_asset_summary, build_finding_groups
from .knowledgebase import KnowledgeBase
Expand Down Expand Up @@ -203,21 +203,6 @@ async def get_or_set_cached(key: str, builder):
return value


async def require_owned_task(db, task_id: str, owner: str, columns: str = "owner_id") -> Dict[str, Any]:
"""Fetch a task and enforce that it belongs to ``owner`` (issue #401).

Returns the selected row on success. Raises 404 when the task does not
exist and 403 when it is owned by a different user/workspace. ``columns``
must include ``owner_id`` so the ownership comparison can be made.
"""
row = await db.fetchone(f"SELECT {columns} FROM tasks WHERE id = ?", (task_id,))
if row is None:
raise HTTPException(status_code=404, detail="Task not found")
if row.get("owner_id") != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")
return row


def _report_generation_error_response(task_id: str, report_format: str) -> JSONResponse:
logger.exception("Report generation failed for task_id=%s format=%s", task_id, report_format)
return JSONResponse(
Expand Down Expand Up @@ -703,16 +688,25 @@ async def event_generator():
async def download_csv_report(task_id: str, owner: str = Depends(get_current_owner)):
"""Download task results as a CSV report."""
db = await get_db()
task_row = await db.fetchone(
"SELECT id, owner_id, plugin_id, tool_name, target, status, created_at, preset, inputs_json, command_used, structured_json FROM tasks WHERE id = ?",
(task_id,)
)

if not task_row:
raise HTTPException(status_code=404, detail="Task not found")

if task_row["owner_id"] != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")
task_row = await require_owned_task(
db,
task_id,
owner,
columns="""
id,
owner_id,
plugin_id,
tool_name,
target,
status,
created_at,
preset,
inputs_json,
command_used,
structured_json
""",
)

if task_row["status"] not in ["completed", "failed"]:
raise HTTPException(status_code=400, detail="Task is not finished yet")
Expand Down Expand Up @@ -741,16 +735,25 @@ async def download_csv_report(task_id: str, owner: str = Depends(get_current_own
async def download_html_report(task_id: str, owner: str = Depends(get_current_owner)):
"""Download task results as an HTML report."""
db = await get_db()
task_row = await db.fetchone(
"SELECT id, owner_id, plugin_id, tool_name, target, status, created_at, preset, inputs_json, command_used, structured_json FROM tasks WHERE id = ?",
(task_id,)
)

if not task_row:
raise HTTPException(status_code=404, detail="Task not found")

if task_row["owner_id"] != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")
task_row = await require_owned_task(
db,
task_id,
owner,
columns="""
id,
owner_id,
plugin_id,
tool_name,
target,
status,
created_at,
preset,
inputs_json,
command_used,
structured_json
""",
)

if task_row["status"] not in ["completed", "failed"]:
raise HTTPException(status_code=400, detail="Task is not finished yet")
Expand Down Expand Up @@ -779,17 +782,25 @@ async def download_html_report(task_id: str, owner: str = Depends(get_current_ow
async def download_pdf_report(task_id: str, owner: str = Depends(get_current_owner)):
"""Download task results as a PDF report."""
db = await get_db()
task_row = await db.fetchone(
"SELECT id, owner_id, plugin_id, tool_name, target, status, created_at, preset, inputs_json, command_used, structured_json FROM tasks WHERE id = ?",
(task_id,)
task_row = await require_owned_task(
db,
task_id,
owner,
columns="""
id,
owner_id,
plugin_id,
tool_name,
target,
status,
created_at,
preset,
inputs_json,
command_used,
structured_json
""",
)

if not task_row:
raise HTTPException(status_code=404, detail="Task not found")

if task_row["owner_id"] != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")

if task_row["status"] not in ["completed", "failed"]:
raise HTTPException(status_code=400, detail="Task is not finished yet")

Expand Down Expand Up @@ -818,17 +829,25 @@ async def download_pdf_report(task_id: str, owner: str = Depends(get_current_own
async def download_sarif_report(task_id: str, owner: str = Depends(get_current_owner)):
"""Download task results as a SARIF report."""
db = await get_db()
task_row = await db.fetchone(
"SELECT id, owner_id, plugin_id, tool_name, target, status, created_at, preset, inputs_json, command_used, structured_json FROM tasks WHERE id = ?",
(task_id,)
task_row = await require_owned_task(
db,
task_id,
owner,
columns="""
id,
owner_id,
plugin_id,
tool_name,
target,
status,
created_at,
preset,
inputs_json,
command_used,
structured_json
""",
)

if not task_row:
raise HTTPException(status_code=404, detail="Task not found")

if task_row["owner_id"] != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")

if task_row["status"] not in ["completed", "failed"]:
raise HTTPException(status_code=400, detail="Task is not finished yet")

Expand Down Expand Up @@ -1221,14 +1240,12 @@ async def build():
@router.get("/task/{task_id}/diff", dependencies=[Depends(read_heavy_limiter)])
async def get_task_diff(task_id: str, owner: str = Depends(get_current_owner)):
db = await get_db()
task_row = await db.fetchone(
"SELECT owner_id, structured_json FROM tasks WHERE id = ?",
(task_id,),
task_row = await require_owned_task(
db,
task_id,
owner,
columns="owner_id, structured_json",
)
if not task_row:
raise HTTPException(status_code=404, detail="Task not found")
if task_row["owner_id"] != owner:
raise HTTPException(status_code=403, detail="You do not have access to this task")

structured = {}
if task_row["structured_json"]:
Expand Down
Loading