Licensing¶
RedPick includes a server-side licensing system designed to distribute the platform to client companies that need to manage penetration testing on their own applications.
Core principle
The licensing system is server-dependent: the desktop client is a UI shell that relies on the backend for every critical operation. Without the server, the client is unusable — there is nothing to "crack".
Overview¶
The licensing system handles:
- Hardware-bound activation — each installation is tied to the physical device
- Periodic heartbeat — the client must confirm license validity every N minutes
- Server-side feature gating — features are unlocked based on the license tier
- Usage tracking — monthly counters for engagements, reports, and API calls
- Anomaly detection — detects suspicious usage patterns (license sharing, multiple IPs)
- Immutable audit trail — every license operation is tracked
Roles¶
The platform uses a unified role system. All users (internal staff, licensed clients, bug hunters) log into the same application with different access levels.
| Role | Who | Key capabilities |
|---|---|---|
admin |
BeDefended staff | Everything: user mgmt, settings, webhooks, licenses, tickets |
pentester |
Internal security consultants | All testing + analysis, no admin settings |
client |
Licensed company (self-service PT) | Launch pentests, view findings, generate reports, upload template |
client_viewer |
Client receiving results (read-only) | View findings, reports, compliance, executive brief |
bughunter |
Bug bounty researcher | BB mode pentests, platform integration (H1/Bugcrowd/Intigriti/YWH), submission tracking |
For the full role comparison matrix see Roles & Licensing.
Tiers and Feature Gating¶
Licenses are organized into tiers per audience. Feature gating happens exclusively server-side: the client receives the list of enabled features from the server on every heartbeat.
Client Tiers¶
| Essentials | Professional | Enterprise | |
|---|---|---|---|
| Target | SMB, 1-2 apps | Mid-size, app portfolio | Large organization |
| Users | 3 | 10 | Unlimited |
| Engagements/month | 5 | 20 | Unlimited |
| Devices | 6 | 20 | Unlimited |
| Concurrent sessions | 3 | 10 | 50 |
| Automated pentest | Yes | Yes | Yes |
| Custom report template | Yes | Yes | Yes |
| Encrypted report delivery | Yes | Yes | Yes |
| Compliance | - | PCI-DSS, SOC2 | All (GDPR, NIS2, DORA, ISO 27001, ASVS) |
| Burp extension + AI analyzer | - | proxy_manual |
Yes |
| Executive Brief | - | Yes | Yes |
| Webhooks & SIEM | - | Yes | Yes |
| API access | - | Read-only | Full |
| CI/CD Integration | - | - | Yes |
| Surface Drift | - | Yes | Yes |
| Learning Loop | - | Yes | Yes |
| Continuous Monitoring | - | - | Yes |
| Team Assignments | - | - | Yes |
| Cost & ROI | - | - | Yes |
| Scan Intelligence | - | - | Yes |
| Threat Model | - | - | Yes |
| On-premise | - | - | Optional |
Bughunter Tiers¶
| Solo | Team | Pro | |
|---|---|---|---|
| Users | 1 | 5 | 15 |
| Platform connections | 2 | 5 | Unlimited |
| Pentests/month | 10 | 50 | Unlimited |
| Bug Bounty mode | Yes | Yes | Yes |
| Direct platform push | Yes | Yes | Yes |
| Submission tracking | Yes | Yes | Yes |
| Burp extension + AI analyzer | Yes | Yes | Yes |
| Payout dashboard | - | Yes | Yes |
| Scan Intel | - | Yes | Yes |
| Collaboration | - | Yes | Yes |
Essentials features¶
["engagements", "findings", "report_basic", "report_custom_template",
"timeline", "retest", "encrypted_delivery", "ticketing"]
Professional features¶
["engagements", "findings", "report_basic", "report_custom_template",
"timeline", "retest", "encrypted_delivery", "ticketing",
"compliance_pci", "compliance_soc2", "executive_brief",
"proxy_manual", "ai_analysis", "webhooks", "api_readonly",
"surface_drift", "learning"]
Enterprise features¶
["engagements", "findings", "report_basic", "report_custom_template",
"report_whitelabel", "timeline", "retest", "encrypted_delivery",
"ticketing", "compliance_all", "executive_brief", "proxy_manual",
"ai_analysis", "webhooks", "api_full", "ci_cd", "monitor",
"assignments", "costs", "scan_intelligence", "remediation",
"threat_model", "surface_drift", "learning", "on_premise"]
Security Architecture¶
The system implements 7 layers of protection. Layer 1 (server dependency) is what makes it uncrackable.
graph TB
subgraph "Layer 1 — Server Dependency (UNCRACKABLE)"
S[Report generation<br>AI analysis<br>Compliance mapping<br>Finding storage]
end
subgraph "Layer 2 — License Token"
L[Server-signed JWT<br>Expires every 5 min<br>Features + limits embedded]
end
subgraph "Layer 3 — Hardware Binding"
H[Device fingerprint<br>SHA-256 of hostname + OS + executable<br>Tied to N specific devices]
end
subgraph "Layer 4 — Heartbeat"
HB[Ping every 2-5 min<br>Missed heartbeat = dead session<br>Instant remote revocation]
end
subgraph "Layer 5 — Certificate Pinning + mTLS"
C[SHA-256 certificate pin<br>Per-company mTLS<br>Prevents MITM]
end
subgraph "Layer 6 — Binary Integrity"
B[Flutter native compile<br>Code obfuscation<br>Runtime integrity check]
end
subgraph "Layer 7 — Anomaly Detection"
A[Anomalous usage patterns<br>Multiple IPs = alert<br>Suspicious device IDs = block]
end
S --> L --> H --> HB --> C --> B --> A
Why it cannot be cracked¶
| Attack scenario | Result |
|---|---|
| Decompile the Flutter binary | Only sees API calls, zero business logic |
| Patch certificate pinning | Cannot forge server responses (JWT signature) |
| Copy binary to another machine | device_fingerprint does not match, rejected |
| Build a fake server | Missing report engine, AI, compliance, templates |
| Share credentials | Anomaly detection flags multiple IPs, revokes |
| Use offline | Heartbeat fails, session dies within 5 minutes |
Data Model¶
The system uses 4 database tables:
licenses¶
The main table representing a license assigned to a client company.
| Field | Type | Description |
|---|---|---|
id |
Integer | Primary key |
license_key |
String(64) | Unique key (48 hex chars, crypto-random) |
company_name |
String(255) | Client company name |
contact_email |
String(255) | Contact email |
tier |
String(20) | essentials / professional / enterprise |
features_json |
Text | JSON array of enabled features |
max_users |
Integer | User limit |
max_engagements_per_month |
Integer | Monthly engagement limit |
max_devices |
Integer | Activated device limit |
valid_from |
DateTime | License start date |
valid_until |
DateTime | License expiry |
is_active |
Boolean | Whether the license is active |
is_suspended |
Boolean | Whether the license is suspended |
suspension_reason |
Text | Reason for suspension |
heartbeat_interval_seconds |
Integer | Heartbeat interval (default 300s) |
last_heartbeat_at |
DateTime | Last heartbeat received |
last_heartbeat_ip |
String(45) | IP of last heartbeat |
missed_heartbeats |
Integer | Consecutive missed heartbeats |
max_concurrent_sessions |
Integer | Maximum concurrent sessions |
anomaly_flags_json |
Text | Detected anomaly log (JSON) |
license_usages¶
Monthly counters for limit enforcement.
| Field | Type | Description |
|---|---|---|
license_id |
FK → licenses | License reference |
period |
String(7) | Period YYYY-MM |
engagements_used |
Integer | Engagements used this month |
reports_generated |
Integer | Reports generated this month |
api_calls |
Integer | API calls this month |
license_activations¶
Activated devices per license — hardware binding.
| Field | Type | Description |
|---|---|---|
license_id |
FK → licenses | License reference |
device_fingerprint |
String(64) | SHA-256 of device |
device_name |
String(255) | Human-readable name (hostname + OS) |
device_os |
String(100) | Operating system |
is_active |
Boolean | Whether the device is active |
last_seen_at |
DateTime | Last heartbeat from this device |
last_ip |
String(45) | Last IP of the device |
license_audit_logs¶
Immutable trail of all license operations.
| Field | Type | Description |
|---|---|---|
license_id |
FK → licenses | License reference |
action |
String(50) | created / activated / heartbeat / suspended / revoked / anomaly / limit_hit |
details |
Text | Action details |
ip_address |
String(45) | IP of the actor |
device_fingerprint |
String(64) | Device fingerprint |
API Endpoints¶
Public (authenticated via license key)¶
These endpoints do not require a JWT — the license key is the authentication.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/licensing/validate |
Validate a license key (no side-effects) |
POST |
/api/v1/licensing/activate |
Activate a device for a license |
POST |
/api/v1/licensing/heartbeat |
Periodic heartbeat — returns current license state |
POST /licensing/validate¶
Verifies a license key is valid without side-effects. Used during initial setup.
curl -X POST https://api.bedefended.com/api/v1/licensing/validate \
-H "Content-Type: application/json" \
-d '{"license_key": "abc123...", "device_fingerprint": "sha256..."}'
Response (200):
{
"valid": true,
"license_id": 1,
"tier": "professional",
"company_name": "Acme Corp",
"features": ["engagements", "findings", "report_basic", "..."],
"limits": {
"max_users": 10,
"max_engagements_per_month": 20,
"max_devices": 20
},
"valid_until": "2027-03-17T00:00:00+00:00",
"heartbeat_interval_seconds": 300
}
POST /licensing/activate¶
Registers a new device. Checks the license device limit.
curl -X POST https://api.bedefended.com/api/v1/licensing/activate \
-H "Content-Type: application/json" \
-d '{
"license_key": "abc123...",
"device_fingerprint": "sha256...",
"device_name": "WORKSTATION-01 — Windows 11",
"device_os": "Windows 11 Pro 10.0.26200"
}'
Response (200):
{
"valid": true,
"license_id": 1,
"tier": "professional",
"features": ["..."],
"activation": "new"
}
Error — device limit reached (403):
POST /licensing/heartbeat¶
Periodic heartbeat. Updates last_heartbeat_at, verifies the device, checks for anomalies, and returns the updated license state with feature flags and current usage.
curl -X POST https://api.bedefended.com/api/v1/licensing/heartbeat \
-H "Content-Type: application/json" \
-d '{"license_key": "abc123...", "device_fingerprint": "sha256..."}'
Response (200):
{
"valid": true,
"license_id": 1,
"tier": "professional",
"company_name": "Acme Corp",
"features": ["engagements", "findings", "..."],
"limits": {
"max_users": 10,
"max_engagements_per_month": 20,
"max_devices": 20
},
"usage": {
"period": "2026-03",
"engagements_used": 7,
"reports_generated": 3,
"api_calls": 1250
},
"valid_until": "2027-03-17T00:00:00+00:00",
"heartbeat_interval_seconds": 300,
"server_time": "2026-03-17T14:30:00+00:00"
}
Admin (require admin JWT)¶
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/admin/licenses |
List all licenses |
POST |
/api/v1/admin/licenses |
Create a new license |
GET |
/api/v1/admin/licenses/{id} |
License detail + activations + audit log |
PATCH |
/api/v1/admin/licenses/{id} |
Update license (tier, limits, expiry) |
POST |
/api/v1/admin/licenses/{id}/suspend |
Suspend license (with reason) |
POST |
/api/v1/admin/licenses/{id}/revoke |
Permanently revoke |
POST |
/api/v1/admin/licenses/{id}/deactivate-device |
Remotely deactivate a device |
GET |
/api/v1/admin/licenses/{id}/audit |
License audit trail |
Create a license¶
curl -X POST https://api.bedefended.com/api/v1/admin/licenses \
-H "Authorization: Bearer <admin_jwt>" \
-H "Content-Type: application/json" \
-d '{
"company_name": "Acme Corp",
"contact_email": "security@acme.com",
"tier": "professional",
"valid_days": 365,
"notes": "Annual contract signed 2026-03-17"
}'
Suspend a license¶
curl -X POST https://api.bedefended.com/api/v1/admin/licenses/1/suspend \
-H "Authorization: Bearer <admin_jwt>" \
-H "Content-Type: application/json" \
-d '{"reason": "Payment overdue"}'
Suspension is immediate: on the next heartbeat the client receives a 403 and is disconnected.
Configuration¶
LICENSING_MODE¶
The LICENSING_MODE environment variable controls system behavior:
| Value | Behavior |
|---|---|
disabled |
No license checks (internal use / self-hosted). Default. |
audit |
Logs violations but allows all requests. Useful for migration. |
enforce |
Blocks requests without a valid license. Production mode. |
# .env
LICENSING_MODE=disabled # For internal BeDefended use
LICENSING_MODE=audit # For testing before enabling enforcement
LICENSING_MODE=enforce # For production with external clients
Gradual rollout
Recommended migration path: disabled → audit (review logs) → enforce (enable blocking). Do not jump directly to enforce in production.
Endpoint enforcement¶
Two dependency injections are available to protect endpoints:
from app.dependencies import require_license_feature, require_license_engagement_quota
# Gate an endpoint by feature
@router.get("/compliance", dependencies=[Depends(require_license_feature("compliance_all"))])
def get_compliance():
...
# Gate engagement creation with monthly quota
@router.post("/engagements", dependencies=[Depends(require_license_engagement_quota())])
def create_engagement():
...
The client sends the license key in the X-License-Key header on each request. The middleware verifies validity and the required feature.
Desktop Client Flow¶
First launch (activation)¶
sequenceDiagram
participant U as User
participant D as Desktop App
participant S as BeDefended Server
U->>D: Enters license key
D->>S: POST /licensing/validate
S-->>D: OK + tier + features
D->>D: Compute device_fingerprint (SHA-256)
D->>S: POST /licensing/activate
S->>S: Check device limit
S->>S: Create LicenseActivation
S->>S: Audit log: "activated"
S-->>D: OK + activation: "new"
D->>D: Save license key to Secure Storage
D->>D: Start heartbeat timer
D-->>U: App ready, features unlocked
Runtime (heartbeat)¶
sequenceDiagram
participant D as Desktop App
participant S as BeDefended Server
loop Every 5 minutes
D->>S: POST /licensing/heartbeat
S->>S: Verify license valid
S->>S: Verify device active
S->>S: Check anomalies (multiple IPs)
S->>S: Update last_heartbeat_at
S-->>D: OK + features + usage + limits
D->>D: Update UI feature flags
end
Note over D,S: If heartbeat returns 403:
D->>D: Stop heartbeat timer
D->>D: Show license error
D->>D: Block all operations
Remote revocation¶
sequenceDiagram
participant A as BeDefended Admin
participant S as Server
participant D as Desktop Client
A->>S: POST /admin/licenses/1/suspend
S->>S: is_suspended = true
S->>S: Audit log: "suspended"
Note over D,S: On next heartbeat:
D->>S: POST /licensing/heartbeat
S-->>D: 403 "License suspended"
D->>D: Stop timer
D->>D: Show error
D->>D: Block all operations
Admin Operations¶
Create a license for a new client¶
- Log in to the admin panel with admin credentials
- Go to Licenses → New License
- Fill in: company name, contact email, tier, duration
- The system generates a 48-character hex license key
- Send the license key to the client via a secure channel
Monitor active licenses¶
The GET /api/v1/admin/licenses endpoint returns all licenses with:
- Status (active / suspended / revoked / expired)
- Last heartbeat (to verify the client is online)
- Activated devices
- Current month usage vs limits
Revoke access immediately¶
# Suspension (reversible)
POST /api/v1/admin/licenses/{id}/suspend
{"reason": "Payment overdue for 30 days"}
# Permanent revocation
POST /api/v1/admin/licenses/{id}/revoke
# Deactivate a single device
POST /api/v1/admin/licenses/{id}/deactivate-device
{"device_fingerprint": "sha256..."}
File Structure¶
dashboard/backend/app/
├── models.py # License, LicenseUsage, LicenseActivation, LicenseAuditLog
├── config.py # LICENSING_MODE setting
├── dependencies.py # require_license_feature(), require_license_engagement_quota()
├── services/
│ └── licensing_service.py # Core business logic (CRUD, validation, heartbeat, anomaly)
└── routers/
└── licensing.py # public_router + admin_router
desktop/lib/
├── models/
│ └── license.dart # LicenseInfo, LicenseLimits, LicenseUsage data classes
├── api/
│ └── licensing_api.dart # validate(), activate(), heartbeat() API calls
├── services/
│ └── license_service.dart # Key storage, heartbeat timer, feature checking
├── providers/
│ └── license_provider.dart # LicenseState, LicenseNotifier (Riverpod)
└── config/
└── app_constants.dart # storageLicenseKey constant