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:
- Acquista N pentest su un target specifico (numero concordato, prezzo personalizzato)
- Usa la propria subscription Claude (API Key o Max session token) e/o Codex (OpenAI API key) — non quella BeDefended
- Riceve accesso all'estensione Burp Suite, ma scope-locked solo sul proprio target
- 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/v1per internal staff,/api/v2/clientper 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 pentestclaude -pcon--dangerously-skip-permissionsper L2 dispatchANTHROPIC_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:
- Runner client package flow: il lancio dei run package-side non passa dal router admin
/api/v1/runner, che oggi usaCurrentUser, ma da un router client dedicato che chiamarunner_cliente propaga le credenziali BYOK. - 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. - Associazione package <-> proxy session: per far rispettare lo scope lato ingest e lato bootstrap serve collegare
ProxySessional 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):
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 usaclaude -p, Codex per P9-P15)claude_only:CODEX_AVAILABLE=falsenel contesto, tutti i role contract Codex (P9-P15) eseguiti da Claudecodex_only: L2 usacodex execal posto diclaude -p,CLAUDE_AVAILABLE=false, L1 invariato (Docker tools sono engine-agnostic)
Validazione credenziali:¶
-
Claude API Key (
Se 200 OK, valida. Usata per chi ha abbonamento API Anthropic.credential_type = "api_key"): -
Claude Max (
credential_type = "max_session"): Il cliente fornisce il proprio session token Claude Max. Il runner usaclaude -pcon l'env varCLAUDE_SESSION_KEYsettata al session token del cliente. Validazione:claude -p "echo ok" --max-turns 1con 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 viaclaude login. -
Codex/OpenAI key (
credential_type = "api_key"):openai.models.list()con la key — se non 401, valida
Flusso credenziali Claude Max (dettaglio UI):¶
- Cliente accede a pagina pacchetto -> sezione "Credenziali AI"
- Tab "Claude" con sotto-opzione: "API Key" o "Max Session Token"
- Se API Key: campo testo, salva, valida con API call
- Se Max Session: campo testo, salva, valida con
claude -ptest. Mostrare warning: "Il session token Max scade periodicamente. Sara' necessario aggiornarlo prima di ogni sessione di test" - Il backend salva entrambi i tipi nello stesso modello
ClientAICredentialconcredential_typediverso - Al momento del run, il runner controlla
credential_type: api_key-> settaANTHROPIC_API_KEY=<decrypted_value>nell'envmax_session-> settaCLAUDE_SESSION_KEY=<decrypted_value>nell'env (il CLIclaudela 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:
- Validare che
package_idappartenga alla company del client - Verificare
package.status == "active"epackage.used_runs < package.total_runs - Verificare che
body.targetmatchipackage.target_pattern(scope enforcement server-side) - Validare credenziali AI (re-validate se
last_validated_at> 1 ora fa) - Decriptare credenziali con
get_decrypted_credentials() - Risolvere engine mode con
resolve_engine_mode() - Creare
PackagePentestRunrecord e incrementareused_runs - 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 filtriusePackageDetail(id)— dettaglio singolouseCreatePackage()— mutation creazioneuseUpdatePackage(id)— mutation aggiornamentouseValidatePackageCredentials(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 inbugbounty_service.py - Chiavi decriptate SOLO in memoria, passate al runner via localhost
- Mai loggate — aggiungere pattern in
output_sanitizer.pyper redactANTHROPIC_API_KEY,OPENAI_API_KEY,CLAUDE_SESSION_KEY
8b. Scope enforcement (defense-in-depth, 5 livelli)¶
- DB:
PentestPackage.target_patternstored - API:
runner.pyvalidabody.targetvspackage.target_patternprima di creare sessione - Extension: Burp riceve
target_lock_regeximmutabile nel bootstrap - Ingest: Batch requests rifiutati se contengono URL fuori scope
- Runner: Scope constraint nel prompt inviato a
claude -p/codex exec
8c. Quota enforcement¶
- Incremento atomico
used_runscon 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¶
- Modelli DB + migrazione —
models.py+016_pentest_packages.py - Service —
package_service.py(encrypt/decrypt, validazione, quota) - Pydantic schemas —
schemas.py(request/response per packages) - Admin API —
packages.pyrouter - Client API —
client_packages.pyrouter - Billing —
billing_service.py+client_billing.py(checkout pacchetto) - Runner backend —
routers/runner.py(gate + passthrough credenziali) - Runner engine —
runner/runner.py(env vars, engine mode) - Burp extension —
BDProxyBridge.java(target-lock mode) - Frontend admin —
PackagesAdminPage.tsx+packages.tsAPI - Client portal — Flutter screens + API per pacchetti
- Security hardening — output sanitizer, audit logging
Step 10: Verifica end-to-end¶
- Admin crea pacchetto per company X, target
https://test.example.com, 5 run - Client X paga via Stripe -> pacchetto diventa
active - Client X carica API key Claude -> validazione OK
- Client X carica API key Codex -> validazione OK
- Client X lancia pentest ->
used_runsincrementa a 1, engine mode =claude+codex - Verificare che il runner usa la chiave AI del cliente (non quella BeDefended)
- Client X scarica config Burp -> scope bloccato su
test.example.com - Verificare che traffico Burp fuori scope viene rifiutato (sia client-side che server-side)
- Client X lancia 4 altri pentest -> alla quinta,
used_runs == total_runs, pacchetto ->exhausted - Tentativo di lancio #6 -> errore "pacchetto esaurito"
- Test con solo Claude (rimuovere Codex key) -> engine mode =
claude_only, codex dispatch skipped - Test con solo Codex -> engine mode =
codex_only,codex execusato al posto diclaude -p - Test con Claude Max session token -> verificare che
CLAUDE_SESSION_KEYviene settata nell'env - 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) |