mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
Every stage config field is a decision; rate, concurrency, timeout and retries come from a per-tool intensity profile the stage reads as its transport. 173 fields become 83: 41 transport values, 40 constants and the dead ones leave the models, and the builder groups by capability with support stages as checkboxes and advanced fields behind one disclosure. Aggressive now changes what a tool is handed. Every project carries a built-in Default engine, seeded on project creation and on first list, undeletable: stage defaults, subfinder as the only passive source, vulnerability scan on with nuclei. From the tracing review: tool args can no longer restate a flag the stage set, the session check obeys the probe scheme, excluded scope reaches every stage that reads addresses, hosts or paths, the sitemap and source-map providers throttle at the transport rate, the blind tier obeys its switch, the fuzzing lanes split the budget, and the removed private-address and evidence switches are back as advanced fields. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import CurrentSuperuser, CurrentUser
|
|
from app.core.database import get_session
|
|
from app.services.scan_engine import ScanEngineService
|
|
from shared.models.organization import Organization
|
|
from shared.models.project import Project, ProjectCreate, ProjectRead, ProjectSummary
|
|
from shared.models.tag import Tag
|
|
from shared.models.target import Target
|
|
from shared.utils.slug import add_with_unique_slug
|
|
|
|
router = APIRouter(
|
|
prefix="/projects",
|
|
tags=["projects"],
|
|
)
|
|
|
|
|
|
@router.get("", response_model=list[ProjectRead])
|
|
async def list_projects(
|
|
_current_user: CurrentUser,
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
include_inactive: bool = Query(False, description="Include soft-deleted projects"),
|
|
):
|
|
query = select(Project)
|
|
|
|
if not include_inactive:
|
|
query = query.where(Project.is_active)
|
|
|
|
result = await session.execute(query)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/{slug}/summary", response_model=ProjectSummary)
|
|
async def get_project_summary(
|
|
slug: str,
|
|
_current_user: CurrentUser,
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
project_result = await session.execute(
|
|
select(Project).where(Project.slug == slug, Project.is_active)
|
|
)
|
|
project = project_result.scalar_one_or_none()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
target_count_result = await session.execute(
|
|
select(func.count(Target.id)).where(Target.project_id == project.id)
|
|
)
|
|
org_count_result = await session.execute(
|
|
select(func.count(Organization.id)).where(Organization.project_id == project.id)
|
|
)
|
|
tag_count_result = await session.execute(
|
|
select(func.count(Tag.id)).where(Tag.project_id == project.id)
|
|
)
|
|
return ProjectSummary(
|
|
project=project,
|
|
stats={
|
|
"targets": target_count_result.scalar_one(),
|
|
"organizations": org_count_result.scalar_one(),
|
|
"tags": tag_count_result.scalar_one(),
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("", response_model=ProjectRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_project(
|
|
project_in: ProjectCreate,
|
|
current_user: CurrentSuperuser,
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
project = Project(
|
|
name=project_in.name,
|
|
description=project_in.description,
|
|
label=project_in.label,
|
|
created_by=current_user.id,
|
|
)
|
|
await add_with_unique_slug(session, project, project_in.name)
|
|
await ScanEngineService(session).ensure_builtin(project.id, current_user.id)
|
|
await session.commit()
|
|
await session.refresh(project)
|
|
return project
|
|
|
|
|
|
@router.get("/{slug}", response_model=ProjectRead)
|
|
async def get_project(
|
|
slug: str,
|
|
_current_user: CurrentUser,
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
result = await session.execute(
|
|
select(Project).where(Project.slug == slug, Project.is_active)
|
|
)
|
|
project = result.scalar_one_or_none()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
|
)
|
|
|
|
return project
|
|
|
|
|
|
@router.delete("/{slug}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_project(
|
|
slug: str,
|
|
_current_user: CurrentSuperuser,
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
result = await session.execute(select(Project).where(Project.slug == slug))
|
|
project = result.scalar_one_or_none()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
|
)
|
|
|
|
project.is_active = False
|
|
await session.commit()
|