Skip to content

Piano Implementazione: Client Self-Service Pentest Packages con BYOK (Bring Your Own Key)

Data: 2026-03-31 Destinatario: Codex GPT-5.4 (high reasoning) Progetto: RedPick — BeDefended Automated Penetration Testing Suite


Context

I clienti BeDefended oggi possono creare engagement tramite il client portal, ma il testing usa sempre le subscription AI di BeDefended. Si vuole un nuovo modello commerciale dove il cliente:

  1. Acquista N pentest su un target specifico (numero concordato, prezzo personalizzato)
  2. Usa la propria subscription Claude (API Key o Max session token) e/o Codex (OpenAI API key) — non quella BeDefended
  3. Riceve accesso all'estensione Burp Suite, ma scope-locked solo sul proprio target
  4. Il sistema deve funzionare con solo Claude, solo Codex, o entrambi

Modello commerciale: L'admin BeDefended crea il pacchetto dopo trattativa col cliente (B2B). Non c'e' catalogo self-service.


Architettura esistente (riferimento)

Backend (FastAPI)

  • /api/v1 per internal staff, /api/v2/client per client portal
  • Modelli: Company, Plan, ClientUser, ClientEngagement, Invoice, ProxySession, ProxyExtensionGrant
  • Auth duale: JWT "access" (internal) + JWT "client" (client portal)
  • Stripe integration in billing_service.py
  • Router auto-discovery: file che iniziano con client_ vanno su /api/v2/client
  • Fernet encryption gia' usata in bugbounty_service.py (linee 32-49): _encrypt_token() / _decrypt_token()

Runner

  • dashboard/runner/runner.py — engine di esecuzione pentest
  • claude -p con --dangerously-skip-permissions per L2 dispatch
  • ANTHROPIC_API_KEY="" prefisso per forzare Max subscription
  • Codex dispatch via dispatch_codex_safe()
  • Env vars settate in _run_session() (linea ~990)

Burp Extension (Java)

  • extensions/burp-proxy-bridge/src/main/java/com/bedefended/burp/BDProxyBridge.java
  • Production-locked a dashboard URL
  • Bootstrap token exchange -> bearer auth (15min access + 7d refresh)
  • Scope enforcement via regex (linea 534)

Client Portal (Flutter)

  • App separata con screens per billing, engagement, findings
  • Ruoli: client_admin, client_technical, client_viewer
  • API su /api/v2/client/

Note di validazione sul piano

Durante la verifica sul codice reale sono emersi 3 aggiustamenti architetturali necessari:

  1. Runner client package flow: il lancio dei run package-side non passa dal router admin /api/v1/runner, che oggi usa CurrentUser, ma da un router client dedicato che chiama runner_client e propaga le credenziali BYOK.
  2. Burp target-lock: il bootstrap/config dell’estensione non va implementato con un nuovo flusso custom, ma estendendo proxy_history_service.py, che gia' gestisce bootstrap token, bearer auth e refresh.
  3. Associazione package <-> proxy session: per far rispettare lo scope lato ingest e lato bootstrap serve collegare ProxySession al package (proxy_sessions.package_id), non solo passare metadata transient.

Step 1: Database — Nuovi modelli + migrazione

File: dashboard/backend/app/models.py

1a. Modello PentestPackage

class PentestPackage(Base):
    __tablename__ = "pentest_packages"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    company_id: Mapped[int] = mapped_column(Integer, ForeignKey("companies.id", ondelete="CASCADE"), nullable=False)
    target_url: Mapped[str] = mapped_column(String(500), nullable=False)           # es. https://app.example.com
    target_pattern: Mapped[str] = mapped_column(String(500), nullable=False)        # es. *.example.com — per scope enforcement
    total_runs: Mapped[int] = mapped_column(Integer, nullable=False)                # numero pentest acquistati
    used_runs: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    price_cents: Mapped[int] = mapped_column(Integer, nullable=False)               # prezzo in centesimi EUR
    status: Mapped[str] = mapped_column(String(50), default="pending_payment", nullable=False)
        # pending_payment | active | exhausted | expired | suspended
    scope_json: Mapped[str | None] = mapped_column(Text, nullable=True)             # JSON scope completo
    flags_json: Mapped[str | None] = mapped_column(Text, nullable=True)             # flag pentest default es. ["--fast","--hwg"]
    stripe_checkout_session_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
    stripe_payment_intent_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
    notes: Mapped[str | None] = mapped_column(Text, nullable=True)                  # note admin
    expires_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    updated_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)

    company: Mapped["Company"] = relationship(back_populates="packages")
    ai_credentials: Mapped[list["ClientAICredential"]] = relationship(back_populates="package", cascade="all, delete-orphan")
    runs: Mapped[list["PackagePentestRun"]] = relationship(back_populates="package", cascade="all, delete-orphan")

1b. Modello ClientAICredential

class ClientAICredential(Base):
    __tablename__ = "client_ai_credentials"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    package_id: Mapped[int] = mapped_column(Integer, ForeignKey("pentest_packages.id", ondelete="CASCADE"), nullable=False)
    engine: Mapped[str] = mapped_column(String(20), nullable=False)      # "claude" | "codex"
    credential_type: Mapped[str] = mapped_column(String(50), nullable=False)  # "api_key" | "max_session"
    encrypted_value: Mapped[str] = mapped_column(Text, nullable=False)   # Fernet-encrypted
    is_valid: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
    last_validated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    validation_error: Mapped[str | None] = mapped_column(String(500), nullable=True)
    created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    updated_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)

    package: Mapped["PentestPackage"] = relationship(back_populates="ai_credentials")

credential_type values: - "api_key" — ANTHROPIC_API_KEY per Claude API, o OPENAI_API_KEY per Codex - "max_session" — Session token Claude Max (scade periodicamente, richiede refresh manuale dal cliente)

1c. Modello PackagePentestRun

class PackagePentestRun(Base):
    __tablename__ = "package_pentest_runs"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    package_id: Mapped[int] = mapped_column(Integer, ForeignKey("pentest_packages.id", ondelete="CASCADE"), nullable=False)
    engagement_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
    runner_session_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
    status: Mapped[str] = mapped_column(String(50), default="queued", nullable=False)
        # queued | running | completed | failed | cancelled
    engine_used: Mapped[str] = mapped_column(String(50), nullable=False)  # claude_only | codex_only | claude+codex
    started_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    finished_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    finding_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
    error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
    triggered_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("client_users.id", ondelete="SET NULL"), nullable=True)
    created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)

    package: Mapped["PentestPackage"] = relationship(back_populates="runs")

1d. Aggiungere relationship a Company

# In class Company, aggiungere:
packages: Mapped[list["PentestPackage"]] = relationship(back_populates="company", cascade="all, delete-orphan")

1e. Migrazione Alembic

File: dashboard/backend/alembic/versions/016_pentest_packages.py

Creare le 3 tabelle con indici su company_id, package_id, status.


Step 2: Service Layer — package_service.py

File: dashboard/backend/app/services/package_service.py (nuovo)

Riutilizzare il pattern Fernet da bugbounty_service.py (linee 32-49):

from app.services.bugbounty_service import _encrypt_token, _decrypt_token

Funzioni principali:

Funzione Descrizione
create_package(db, company_id, target_url, target_pattern, total_runs, price_cents, ...) Admin crea il pacchetto dopo trattativa
store_ai_credential(db, package_id, engine, credential_type, raw_value) Cripta e salva la chiave AI del cliente
validate_ai_credential(db, credential_id) -> (bool, error) Testa validita' chiave
get_decrypted_credentials(db, package_id) -> dict Ritorna {"ANTHROPIC_API_KEY": "...", "OPENAI_API_KEY": "..."} o {"CLAUDE_SESSION_KEY": "..."}
resolve_engine_mode(db, package_id) -> str Ritorna "claude+codex" / "claude_only" / "codex_only" in base a credenziali valide
start_run(db, package_id, client_user_id) -> PackagePentestRun Verifica quota, incrementa used_runs, crea record run
complete_run(db, run_id, status, finding_count, error) Finalizza run
refund_run(db, run_id) Rimborsa run fallito per errore infrastruttura (decrementa used_runs)
get_target_scope_regex(package) -> str Converte target_pattern in regex per Burp

Logica engine fallback:

  • claude+codex: comportamento standard (L2 usa claude -p, Codex per P9-P15)
  • claude_only: CODEX_AVAILABLE=false nel contesto, tutti i role contract Codex (P9-P15) eseguiti da Claude
  • codex_only: L2 usa codex exec al posto di claude -p, CLAUDE_AVAILABLE=false, L1 invariato (Docker tools sono engine-agnostic)

Validazione credenziali:

  • Claude API Key (credential_type = "api_key"):

    curl -s https://api.anthropic.com/v1/messages \
      -H "x-api-key: $KEY" \
      -H "anthropic-version: 2023-06-01" \
      -d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"test"}]}'
    
    Se 200 OK, valida. Usata per chi ha abbonamento API Anthropic.

  • Claude Max (credential_type = "max_session"): Il cliente fornisce il proprio session token Claude Max. Il runner usa claude -p con l'env var CLAUDE_SESSION_KEY settata al session token del cliente. Validazione: claude -p "echo ok" --max-turns 1 con il token settato. Nota: i session token Max scadono — il sistema deve: (a) validare prima di ogni run (b) notificare il cliente se scaduto con istruzioni per rigenerarlo (login su claude.ai -> developer settings -> session token) Per MVP: solo session token manuale. In futuro: login OAuth via claude login.

  • Codex/OpenAI key (credential_type = "api_key"): openai.models.list() con la key — se non 401, valida

Flusso credenziali Claude Max (dettaglio UI):

  1. Cliente accede a pagina pacchetto -> sezione "Credenziali AI"
  2. Tab "Claude" con sotto-opzione: "API Key" o "Max Session Token"
  3. Se API Key: campo testo, salva, valida con API call
  4. Se Max Session: campo testo, salva, valida con claude -p test. Mostrare warning: "Il session token Max scade periodicamente. Sara' necessario aggiornarlo prima di ogni sessione di test"
  5. Il backend salva entrambi i tipi nello stesso modello ClientAICredential con credential_type diverso
  6. Al momento del run, il runner controlla credential_type:
  7. api_key -> setta ANTHROPIC_API_KEY=<decrypted_value> nell'env
  8. max_session -> setta CLAUDE_SESSION_KEY=<decrypted_value> nell'env (il CLI claude la usa per autenticarsi)

Step 3: API Endpoints

3a. Admin Router — dashboard/backend/app/routers/packages.py (nuovo)

Metodo Path Descrizione Ruolo
POST /api/v1/packages/ Crea pacchetto per un'azienda admin
GET /api/v1/packages/ Lista tutti i pacchetti admin, pentester
GET /api/v1/packages/{id} Dettaglio pacchetto admin, pentester
PATCH /api/v1/packages/{id} Aggiorna (stato, aggiungi run, scope) admin
DELETE /api/v1/packages/{id} Soft-delete (status=suspended) admin
POST /api/v1/packages/{id}/validate-credentials Forza ri-validazione chiavi AI admin

3b. Client Router — dashboard/backend/app/routers/client_packages.py (nuovo)

Metodo Path Descrizione Ruolo
GET /api/v2/client/packages/ Lista pacchetti della propria company client_admin, client_technical
GET /api/v2/client/packages/{id} Dettaglio (run rimanenti, stato) client_admin, client_technical, client_viewer
POST /api/v2/client/packages/{id}/credentials Carica chiave AI (criptata) client_admin
PUT /api/v2/client/packages/{id}/credentials/{engine} Aggiorna chiave AI client_admin
DELETE /api/v2/client/packages/{id}/credentials/{engine} Rimuovi chiave client_admin
POST /api/v2/client/packages/{id}/credentials/{engine}/validate Testa validita' chiave client_admin
POST /api/v2/client/packages/{id}/runs Lancia un nuovo pentest client_admin, client_technical
GET /api/v2/client/packages/{id}/runs Lista run del pacchetto tutti i client
GET /api/v2/client/packages/{id}/runs/{run_id} Stato/output run tutti i client
POST /api/v2/client/packages/{id}/runs/{run_id}/stop Ferma pentest in corso client_admin, client_technical
GET /api/v2/client/packages/{id}/burp-config Ottieni config Burp (bootstrap token target-locked) client_admin, client_technical

Step 4: Billing — Stripe Checkout per Packages

File: dashboard/backend/app/services/billing_service.py

Aggiungere create_package_checkout_session():

def create_package_checkout_session(db, company, package: PentestPackage, success_url=None, cancel_url=None):
    stripe = get_stripe()
    settings = get_settings()
    base_url = settings.CLIENT_PORTAL_URL

    if not stripe or not settings.STRIPE_SECRET_KEY:
        return {
            "checkout_url": None,
            "detail": "Stripe non configurato. Contattare BeDefended per il pagamento.",
            "package_id": package.id,
            "price_eur": package.price_cents,
        }

    # Ensure Stripe customer exists (same pattern as existing create_checkout_session)
    if not company.stripe_customer_id:
        customer = stripe.Customer.create(
            email=company.billing_email or company.contact_email,
            name=company.name,
            metadata={"company_id": str(company.id)},
        )
        company.stripe_customer_id = customer.id
        db.commit()

    session = stripe.checkout.Session.create(
        customer=company.stripe_customer_id,
        line_items=[{
            "price_data": {
                "currency": "eur",
                "unit_amount": package.price_cents,
                "product_data": {
                    "name": f"Pentest Package: {package.total_runs} runs — {package.target_url}",
                    "description": f"Pacchetto {package.total_runs} pentest automatici su {package.target_url}",
                },
            },
            "quantity": 1,
        }],
        mode="payment",
        metadata={"type": "pentest_package", "package_id": str(package.id), "company_id": str(company.id)},
        success_url=success_url or f"{base_url}/packages/{package.id}?checkout=success",
        cancel_url=cancel_url or f"{base_url}/packages/{package.id}?checkout=cancelled",
    )
    package.stripe_checkout_session_id = session.id
    db.commit()
    return {"checkout_url": session.url}

Estendere handle_checkout_completed():

# Nel webhook handler esistente, aggiungere branch:
if metadata.get("type") == "pentest_package":
    package = db.query(PentestPackage).get(int(metadata["package_id"]))
    if package:
        package.status = "active"
        package.stripe_payment_intent_id = event_data.get("payment_intent")
        # Creare Invoice record
        invoice = Invoice(
            company_id=package.company_id,
            stripe_invoice_id=event_data.get("invoice"),
            amount_cents=package.price_cents,
            currency="eur",
            status="paid",
            description=f"Pentest Package #{package.id}: {package.total_runs} runs on {package.target_url}",
        )
        db.add(invoice)
        db.commit()

File: dashboard/backend/app/routers/client_billing.py — aggiungere:

@router.post("/package-checkout")
def package_checkout(body: PackageCheckoutRequest, current_user=Depends(get_current_client_user), db=Depends(get_db)):
    package = db.query(PentestPackage).filter_by(id=body.package_id, company_id=current_user.company_id).first()
    if not package:
        raise HTTPException(404, "Pacchetto non trovato")
    if package.status != "pending_payment":
        raise HTTPException(400, "Pacchetto gia' pagato o non disponibile")
    company = db.query(Company).get(current_user.company_id)
    return create_package_checkout_session(db, company, package)

Step 5: Runner — Supporto credenziali AI del cliente

5a. dashboard/backend/app/routers/runner.py — Gate in create_session()

Quando un client lancia un pentest da un pacchetto:

  1. Validare che package_id appartenga alla company del client
  2. Verificare package.status == "active" e package.used_runs < package.total_runs
  3. Verificare che body.target matchi package.target_pattern (scope enforcement server-side)
  4. Validare credenziali AI (re-validate se last_validated_at > 1 ora fa)
  5. Decriptare credenziali con get_decrypted_credentials()
  6. Risolvere engine mode con resolve_engine_mode()
  7. Creare PackagePentestRun record e incrementare used_runs
  8. Passare credenziali decriptate al runner nel payload

5b. dashboard/runner/runner.py — Env vars per sessione client

Nella funzione _run_session() (linea ~990):

env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"

# Client BYOK credentials
if body.get("client_anthropic_api_key"):
    env["ANTHROPIC_API_KEY"] = body["client_anthropic_api_key"]
elif body.get("client_claude_session_key"):
    env["CLAUDE_SESSION_KEY"] = body["client_claude_session_key"]
else:
    env["ANTHROPIC_API_KEY"] = ""  # Default: usa Max di BeDefended

if body.get("client_openai_api_key"):
    env["OPENAI_API_KEY"] = body["client_openai_api_key"]

# Engine mode flag
if body.get("client_engine_mode"):
    env["CLIENT_ENGINE_MODE"] = body["client_engine_mode"]  # claude_only | codex_only | claude+codex

5c. Dispatch modifiche

Il dispatch L2 (claude -p) deve controllare CLIENT_ENGINE_MODE: - Se codex_only: usare codex exec -a never -s workspace-write al posto di claude -p - Se claude_only: saltare tutte le chiamate dispatch_codex_safe(), eseguire con Claude - Se claude+codex: comportamento standard

Implementare in agent-dispatch.md e nei relativi helper shell.

5d. Error handling mid-run

Nella _read_stream(), detectare: - "credit balance too low" o "rate limit" -> callback al backend per segnalare errore - "authentication_error" -> marcare credential come is_valid=False - Su errore fatale: salvare stato per /resume, notificare cliente via WebSocket/notifiche


Step 6: Burp Extension — Target-Lock Mode

File: extensions/burp-proxy-bridge/src/main/java/com/bedefended/burp/BDProxyBridge.java

6a. Nuovo campo nel bootstrap config response

Aggiungere nel JSON restituito da /extension-config:

{
  "access_token": "...",
  "refresh_token": "...",
  "target_lock": "https://app.example.com",
  "target_lock_regex": "^https?://(.*\\.)?example\\.com(/|$)",
  "client_package_id": 42
}

6b. Logica target-lock nell'estensione

Quando target_lock presente nel config: 1. Sovrascrivere scopeRegex con target_lock_regex — renderlo immutabile 2. Disabilitare il campo scope nella UI (grigiato) 3. Mostrare label: "Scope bloccato su: app.example.com (Pacchetto #42)" 4. Nel check scope (linea 534), usare target_lock_regex SEMPRE, ignorando Burp Target Scope

6c. Backend — scope enforcement server-side

In proxy_history.py, endpoint batch ingest:

Se la sessione proxy e' associata a un pacchetto, validare che TUTTI gli URL nel batch matchino package.target_pattern. Rigettare batch con URL fuori scope con HTTP 403.

6d. Client bootstrap endpoint

File: dashboard/backend/app/routers/client_packages.py

@router.get("/{package_id}/burp-config")
def get_burp_config(package_id: int, current_user=Depends(get_current_client_user), db=Depends(get_db)):
    package = db.query(PentestPackage).filter_by(id=package_id, company_id=current_user.company_id).first()
    if not package:
        raise HTTPException(404, "Pacchetto non trovato")
    if package.status != "active":
        raise HTTPException(400, "Pacchetto non attivo")

    # Crea o riusa ProxySession per questo pacchetto
    session = get_or_create_proxy_session(db, package)

    # Genera bootstrap token con target_lock
    bootstrap = create_bootstrap_token(db, session, target_lock={
        "target_lock": package.target_url,
        "target_lock_regex": get_target_scope_regex(package),
        "client_package_id": package.id,
    })

    return {
        "dashboard_url": settings.DASHBOARD_URL,
        "bootstrap_token": bootstrap.token,
        "bootstrap_url": f"{settings.DASHBOARD_URL}/api/v1/proxy-history/sessions/{session.id}/extension-config?token={bootstrap.token}",
        "target_lock": package.target_url,
        "instructions": "1. Installare BD Bridge in Burp Suite\n2. Nella tab BD Bridge, cliccare 'Fetch Config'\n3. Incollare il bootstrap_url\n4. Lo scope sara' automaticamente bloccato sul target del pacchetto",
    }

Step 7: Frontend — Nuove pagine e componenti

7a. React Dashboard (admin side)

File: dashboard/frontend/src/pages/PackagesAdminPage.tsx (nuovo)

  • Lista tutti i pacchetti di tutte le company con filtri (stato, company)
  • Form creazione pacchetto: company (select), target URL, target pattern, num_runs, prezzo EUR, scadenza, scope JSON, flag pentest, note
  • Azioni per pacchetto: attiva/sospendi, aggiungi run extra, modifica scope, valida credenziali

File: dashboard/frontend/src/api/packages.ts (nuovo)

  • usePackages() — lista con filtri
  • usePackageDetail(id) — dettaglio singolo
  • useCreatePackage() — mutation creazione
  • useUpdatePackage(id) — mutation aggiornamento
  • useValidatePackageCredentials(id) — mutation validazione

7b. Client Portal (Flutter)

File: client_portal/lib/screens/packages/packages_screen.dart (nuovo)

  • Lista pacchetti del cliente con: target, run usati/totali, stato, scadenza
  • Dettaglio pacchetto con:
  • Sezione "Credenziali AI" — upload/validate Claude key e Codex key (separati), stato validazione
  • Sezione "Run" — tabella con storico run, pulsante "Lancia Pentest"
  • Sezione "Burp Extension" — pulsante "Scarica Config", istruzioni setup
  • Badge stato: attivo/esaurito/scaduto

File: client_portal/lib/api/packages_api.dart (nuovo)

  • Tutti gli endpoint /api/v2/client/packages/

File: client_portal/lib/screens/packages/ai_credentials_dialog.dart (nuovo)

  • Dialog con due tab: Claude / Codex
  • Per Claude: sotto-toggle "API Key" vs "Max Session Token"
  • Campo password per la chiave (mascherato)
  • Pulsante "Valida" con spinner e risultato (valido/invalido/errore)
  • Warning per Max session: "Il session token scade periodicamente"
  • Info: "La chiave viene criptata e mai conservata in chiaro"

File: client_portal/lib/screens/packages/launch_pentest_dialog.dart (nuovo)

  • Conferma lancio con: target, run rimanenti, engine disponibile, flag opzionali
  • Warning se solo un engine disponibile
  • Blocco se nessun engine configurato
  • Blocco se pacchetto esaurito/scaduto

Step 8: Sicurezza

8a. Crittografia chiavi AI

  • Fernet (AES-128-CBC + HMAC-SHA256) derivata da SECRET_KEY — pattern esistente in bugbounty_service.py
  • Chiavi decriptate SOLO in memoria, passate al runner via localhost
  • Mai loggate — aggiungere pattern in output_sanitizer.py per redact ANTHROPIC_API_KEY, OPENAI_API_KEY, CLAUDE_SESSION_KEY

8b. Scope enforcement (defense-in-depth, 5 livelli)

  1. DB: PentestPackage.target_pattern stored
  2. API: runner.py valida body.target vs package.target_pattern prima di creare sessione
  3. Extension: Burp riceve target_lock_regex immutabile nel bootstrap
  4. Ingest: Batch requests rifiutati se contengono URL fuori scope
  5. Runner: Scope constraint nel prompt inviato a claude -p / codex exec

8c. Quota enforcement

  • Incremento atomico used_runs con check DB-level (UPDATE ... WHERE used_runs < total_runs)
  • Doppio check: prima di passare al runner, ri-verificare
  • Rimborso solo per errori infrastruttura (non per chiave scaduta o target down)

8d. Audit trail

  • Tutte le operazioni su credenziali -> AuditLog (action: package_credential_create, package_credential_delete, etc.)
  • Tutti i run start/stop -> AuditLog (action: package_run_start, package_run_complete, package_run_fail)
  • Tutte le violazioni scope -> AuditLog (action: package_scope_violation)

Step 9: Ordine di implementazione

  1. Modelli DB + migrazionemodels.py + 016_pentest_packages.py
  2. Servicepackage_service.py (encrypt/decrypt, validazione, quota)
  3. Pydantic schemasschemas.py (request/response per packages)
  4. Admin APIpackages.py router
  5. Client APIclient_packages.py router
  6. Billingbilling_service.py + client_billing.py (checkout pacchetto)
  7. Runner backendrouters/runner.py (gate + passthrough credenziali)
  8. Runner enginerunner/runner.py (env vars, engine mode)
  9. Burp extensionBDProxyBridge.java (target-lock mode)
  10. Frontend adminPackagesAdminPage.tsx + packages.ts API
  11. Client portal — Flutter screens + API per pacchetti
  12. Security hardening — output sanitizer, audit logging

Step 10: Verifica end-to-end

  1. Admin crea pacchetto per company X, target https://test.example.com, 5 run
  2. Client X paga via Stripe -> pacchetto diventa active
  3. Client X carica API key Claude -> validazione OK
  4. Client X carica API key Codex -> validazione OK
  5. Client X lancia pentest -> used_runs incrementa a 1, engine mode = claude+codex
  6. Verificare che il runner usa la chiave AI del cliente (non quella BeDefended)
  7. Client X scarica config Burp -> scope bloccato su test.example.com
  8. Verificare che traffico Burp fuori scope viene rifiutato (sia client-side che server-side)
  9. Client X lancia 4 altri pentest -> alla quinta, used_runs == total_runs, pacchetto -> exhausted
  10. Tentativo di lancio #6 -> errore "pacchetto esaurito"
  11. Test con solo Claude (rimuovere Codex key) -> engine mode = claude_only, codex dispatch skipped
  12. Test con solo Codex -> engine mode = codex_only, codex exec usato al posto di claude -p
  13. Test con Claude Max session token -> verificare che CLAUDE_SESSION_KEY viene settata nell'env
  14. Test con session token scaduto -> errore chiaro, notifica al cliente

File critici da modificare

File Azione
dashboard/backend/app/models.py Aggiungere 3 modelli + relationship Company
dashboard/backend/alembic/versions/016_pentest_packages.py Nuova migrazione
dashboard/backend/app/schemas.py Pydantic schemas per packages
dashboard/backend/app/services/package_service.py NUOVO — logica core
dashboard/backend/app/services/billing_service.py Aggiungere create_package_checkout_session() + webhook handler
dashboard/backend/app/routers/packages.py NUOVO — admin API
dashboard/backend/app/routers/client_packages.py NUOVO — client API
dashboard/backend/app/routers/client_billing.py Aggiungere endpoint package-checkout
dashboard/backend/app/routers/runner.py Gate client + passthrough credenziali
dashboard/runner/runner.py Env vars sessione, engine mode
extensions/burp-proxy-bridge/.../BDProxyBridge.java Target-lock mode
dashboard/frontend/src/pages/PackagesAdminPage.tsx NUOVO — pagina admin
dashboard/frontend/src/api/packages.ts NUOVO — API hooks
client_portal/lib/screens/packages/ NUOVI — Flutter screens
client_portal/lib/api/packages_api.dart NUOVO — API client
docs/operations/roles-and-licensing.md Aggiornare documentazione tiers

Pattern e utility esistenti da riutilizzare

Pattern File sorgente Linee
Fernet encryption app/services/bugbounty_service.py 32-49
Router auto-discovery app/routers/__init__.py (automatico per client_*)
Role checking (client) app/dependencies.py require_client_role()
Company access isolation app/dependencies.py require_company_access()
Stripe checkout app/services/billing_service.py create_checkout_session()
Bootstrap token app/routers/proxy_history.py create_bootstrap()
Audit logging app/models.py AuditLog model
Output sanitization app/services/output_sanitizer.py pattern redaction

Error handling — Tabella decisioni

Errore Rilevamento Azione
Claude API key scaduta Auth error in output stream is_valid=False, notifica client, pausa run
Claude Max session scaduto claude -p auth failure is_valid=False, notifica "Rigenerare session token da claude.ai"
Claude rate limit "rate limit" in stderr Pausa, notifica, fallback a Codex se disponibile
Codex API key scaduta OpenAI 401 is_valid=False, notifica client
Codex rate limit OpenAI 429 Pausa, fallback a Claude se disponibile
Entrambi engine falliscono Entrambi is_valid=False Stop run, status=failed, rimborso quota
Pacchetto scaduto expires_at < now Blocca nuovi run, risultati consultabili
Target down Nessuna risposta HTTP Run failed, NO rimborso (non e' errore infra)