Spaces:
Build error
Build error
Upload 16 files
Browse files- .gitattributes +35 -35
- .gitignore +0 -0
- Procfile +0 -0
- README.md +117 -12
- ai_models.py +838 -0
- anomaly_detector.py +345 -0
- app.json +25 -0
- app.py +787 -0
- identity_analyzer.py +216 -0
- optimax_reports.py +734 -0
- permission_analyzer.py +352 -0
- prompts.py +113 -0
- requirements.txt +50 -0
- runtime.txt +0 -0
- sharing_analyzer.py +129 -0
- vulenerabilities_db.py +272 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
Binary file (82 Bytes). View file
|
|
|
Procfile
ADDED
|
Binary file (42 Bytes). View file
|
|
|
README.md
CHANGED
|
@@ -1,12 +1,117 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Optimax-agent
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.3.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# 🛡️ Optimax Security Agent
|
| 14 |
+
|
| 15 |
+
AI-powered Salesforce security analysis API that receives metadata from Salesforce orgs and returns comprehensive security assessments.
|
| 16 |
+
|
| 17 |
+
## Architecture
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
Salesforce Org → Sends Metadata → Optimax Agent API → Returns Analysis
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
**Key Feature:** This API processes **ONLY METADATA** - no credentials are ever received or stored.
|
| 24 |
+
|
| 25 |
+
## Features
|
| 26 |
+
|
| 27 |
+
- 🤖 **AI-Powered Analysis** using Salesforce CodeGen 350M
|
| 28 |
+
- 🔍 **Permission Vulnerability Detection**
|
| 29 |
+
- 👤 **Identity & Access Management Analysis**
|
| 30 |
+
- 🔐 **Sharing Model Security Assessment**
|
| 31 |
+
- 📊 **Risk Scoring & Prioritization**
|
| 32 |
+
- 💡 **Actionable Recommendations**
|
| 33 |
+
|
| 34 |
+
## API Endpoints
|
| 35 |
+
|
| 36 |
+
### POST `/api/analyze`
|
| 37 |
+
|
| 38 |
+
Main analysis endpoint for Salesforce integration.
|
| 39 |
+
|
| 40 |
+
**Request:**
|
| 41 |
+
```json
|
| 42 |
+
{
|
| 43 |
+
"org_id": "00Dxx0000001234",
|
| 44 |
+
"org_name": "Your Organization",
|
| 45 |
+
"users": [...],
|
| 46 |
+
"profiles": [...],
|
| 47 |
+
"permission_sets": [...],
|
| 48 |
+
"login_history": [...],
|
| 49 |
+
"sharing_settings": {...}
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**Response:**
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"success": true,
|
| 57 |
+
"overall_risk_score": 45,
|
| 58 |
+
"risk_level": "MEDIUM",
|
| 59 |
+
"critical_findings": [...],
|
| 60 |
+
"high_findings": [...],
|
| 61 |
+
"ai_executive_summary": "...",
|
| 62 |
+
"ai_recommendations": [...]
|
| 63 |
+
}
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Usage
|
| 67 |
+
|
| 68 |
+
### From Salesforce Apex:
|
| 69 |
+
|
| 70 |
+
```apex
|
| 71 |
+
// 1. Collect metadata
|
| 72 |
+
Map<String, Object> metadata = MetadataExtractor.collectOrgMetadata();
|
| 73 |
+
|
| 74 |
+
// 2. Call Optimax Agent
|
| 75 |
+
HttpRequest req = new HttpRequest();
|
| 76 |
+
req.setEndpoint('callout:Optimax_Agent/api/analyze');
|
| 77 |
+
req.setMethod('POST');
|
| 78 |
+
req.setHeader('Content-Type', 'application/json');
|
| 79 |
+
req.setBody(JSON.serialize(metadata));
|
| 80 |
+
|
| 81 |
+
Http http = new Http();
|
| 82 |
+
HttpResponse res = http.send(req);
|
| 83 |
+
|
| 84 |
+
// 3. Parse response
|
| 85 |
+
Map<String, Object> analysis = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Using cURL:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
curl -X POST https://m8077anya-vishwakarma-optimax-agent.hf.space/api/analyze \
|
| 92 |
+
-H "Content-Type: application/json" \
|
| 93 |
+
-d @metadata.json
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## Testing
|
| 97 |
+
|
| 98 |
+
Visit the app in your browser to use the interactive testing interface.
|
| 99 |
+
|
| 100 |
+
## Security
|
| 101 |
+
|
| 102 |
+
- ✅ No credential processing
|
| 103 |
+
- ✅ Metadata-only analysis
|
| 104 |
+
- ✅ Stateless operation
|
| 105 |
+
- ✅ Private space (access controlled)
|
| 106 |
+
|
| 107 |
+
## Technical Details
|
| 108 |
+
|
| 109 |
+
- **Framework:** Gradio + FastAPI
|
| 110 |
+
- **AI Model:** Salesforce CodeGen 350M (350 million parameters)
|
| 111 |
+
- **Analysis:** Hybrid (AI + rule-based)
|
| 112 |
+
- **Deployment:** Hugging Face Spaces
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
**Version:** 2.0.0
|
| 117 |
+
**Last Updated:** January 2026
|
ai_models.py
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ai_models.py - Multi-Model AI Security Analyzer for Salesforce
|
| 3 |
+
|
| 4 |
+
Uses FOUR models for comprehensive analysis:
|
| 5 |
+
1. CodeBERT (125M) - For Apex code analysis
|
| 6 |
+
2. RoBERTa (125M) - For permission classification
|
| 7 |
+
3. SecBERT (110M) - For security policy analysis
|
| 8 |
+
4. Isolation Forest - For anomaly detection (NEW)
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
from ai_models import MultiModelAnalyzer
|
| 12 |
+
|
| 13 |
+
analyzer = MultiModelAnalyzer()
|
| 14 |
+
results = analyzer.analyze_org(metadata)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import logging
|
| 19 |
+
from transformers import (
|
| 20 |
+
RobertaTokenizer,
|
| 21 |
+
RobertaModel,
|
| 22 |
+
AutoTokenizer,
|
| 23 |
+
AutoModel
|
| 24 |
+
)
|
| 25 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 26 |
+
from sklearn.ensemble import IsolationForest # NEW: Anomaly detection
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# Import our anomaly detector
|
| 32 |
+
try:
|
| 33 |
+
from anomaly_detector import AnomalyDetector
|
| 34 |
+
ANOMALY_AVAILABLE = True
|
| 35 |
+
except ImportError:
|
| 36 |
+
logger.warning("⚠️ anomaly_detector.py not found - anomaly detection disabled")
|
| 37 |
+
ANOMALY_AVAILABLE = False
|
| 38 |
+
|
| 39 |
+
# ============================================================================
|
| 40 |
+
# MULTI-MODEL ANALYZER
|
| 41 |
+
# ============================================================================
|
| 42 |
+
|
| 43 |
+
class MultiModelAnalyzer:
|
| 44 |
+
"""
|
| 45 |
+
Multi-model AI analyzer using CodeBERT, RoBERTa, SecBERT, and Isolation Forest
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
"""Initialize all four models"""
|
| 50 |
+
self.available = False
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
logger.info("🤖 Loading AI Models...")
|
| 54 |
+
logger.info(" 1/4 Loading CodeBERT for code analysis...")
|
| 55 |
+
self.codebert = CodeBERTAnalyzer()
|
| 56 |
+
|
| 57 |
+
logger.info(" 2/4 Loading RoBERTa for permission classification...")
|
| 58 |
+
self.roberta = RoBERTaAnalyzer()
|
| 59 |
+
|
| 60 |
+
logger.info(" 3/4 Loading SecBERT for security analysis...")
|
| 61 |
+
self.secbert = SecBERTAnalyzer()
|
| 62 |
+
|
| 63 |
+
logger.info(" 4/4 Loading Anomaly Detector...")
|
| 64 |
+
if ANOMALY_AVAILABLE:
|
| 65 |
+
self.anomaly = AnomalyDetector()
|
| 66 |
+
logger.info(" ✅ Anomaly detector loaded!")
|
| 67 |
+
else:
|
| 68 |
+
self.anomaly = None
|
| 69 |
+
logger.warning(" ⚠️ Anomaly detector not available")
|
| 70 |
+
|
| 71 |
+
self.available = True
|
| 72 |
+
logger.info("✅ All AI models loaded successfully!")
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.error(f"❌ Failed to load AI models: {e}")
|
| 76 |
+
self.available = False
|
| 77 |
+
|
| 78 |
+
def analyze_org(self, metadata):
|
| 79 |
+
"""
|
| 80 |
+
Analyze entire Salesforce org with all models
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
metadata: Dict containing org data (users, profiles, permission_sets, etc.)
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
Dict with AI-powered insights from all models
|
| 87 |
+
"""
|
| 88 |
+
if not self.available:
|
| 89 |
+
return {
|
| 90 |
+
'available': False,
|
| 91 |
+
'message': 'AI models not available'
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
results = {
|
| 95 |
+
'available': True,
|
| 96 |
+
'code_analysis': [],
|
| 97 |
+
'permission_analysis': [],
|
| 98 |
+
'security_analysis': [],
|
| 99 |
+
'anomaly_analysis': [], # NEW
|
| 100 |
+
'overall_risk_score': 0,
|
| 101 |
+
'ai_recommendations': []
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
# 1. Analyze Apex code (if available)
|
| 106 |
+
if 'apex_classes' in metadata and metadata['apex_classes']:
|
| 107 |
+
logger.info("🔍 Analyzing Apex code with CodeBERT...")
|
| 108 |
+
results['code_analysis'] = self.codebert.analyze_apex_code(
|
| 109 |
+
metadata['apex_classes']
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# 2. Analyze permissions with RoBERTa
|
| 113 |
+
if 'permission_sets' in metadata or 'profiles' in metadata:
|
| 114 |
+
logger.info("🔍 Analyzing permissions with RoBERTa...")
|
| 115 |
+
results['permission_analysis'] = self.roberta.analyze_permissions(
|
| 116 |
+
permission_sets=metadata.get('permission_sets', []),
|
| 117 |
+
profiles=metadata.get('profiles', [])
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# 3. Analyze security policies with SecBERT
|
| 121 |
+
logger.info("🔍 Analyzing security policies with SecBERT...")
|
| 122 |
+
results['security_analysis'] = self.secbert.analyze_security(
|
| 123 |
+
metadata=metadata
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# 4. Detect anomalies with Isolation Forest (NEW)
|
| 127 |
+
if self.anomaly:
|
| 128 |
+
logger.info("🔍 Detecting behavioral anomalies...")
|
| 129 |
+
|
| 130 |
+
# Login anomalies
|
| 131 |
+
login_anomalies = self.anomaly.detect_login_anomalies(
|
| 132 |
+
metadata.get('login_history', [])
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Permission anomalies
|
| 136 |
+
perm_anomalies = self.anomaly.detect_permission_anomalies(
|
| 137 |
+
users=metadata.get('users', []),
|
| 138 |
+
permission_sets=metadata.get('permission_sets', [])
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Dormant accounts
|
| 142 |
+
dormant_anomalies = self.anomaly.detect_dormant_account_risks(
|
| 143 |
+
metadata.get('users', [])
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
results['anomaly_analysis'] = login_anomalies + perm_anomalies + dormant_anomalies
|
| 147 |
+
logger.info(f" Found {len(results['anomaly_analysis'])} anomalies")
|
| 148 |
+
|
| 149 |
+
# 5. Calculate overall AI risk score
|
| 150 |
+
results['overall_risk_score'] = self._calculate_ai_risk_score(results)
|
| 151 |
+
|
| 152 |
+
# 6. Generate AI recommendations
|
| 153 |
+
results['ai_recommendations'] = self._generate_recommendations(results)
|
| 154 |
+
|
| 155 |
+
logger.info(f"✅ AI Analysis complete - Risk Score: {results['overall_risk_score']}/100")
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
logger.error(f"❌ AI analysis error: {e}")
|
| 159 |
+
results['error'] = str(e)
|
| 160 |
+
|
| 161 |
+
return results
|
| 162 |
+
|
| 163 |
+
def _calculate_ai_risk_score(self, results):
|
| 164 |
+
"""Calculate overall risk score from all model outputs"""
|
| 165 |
+
score = 0
|
| 166 |
+
|
| 167 |
+
# Code vulnerabilities
|
| 168 |
+
code_issues = len(results.get('code_analysis', []))
|
| 169 |
+
score += min(25, code_issues * 10)
|
| 170 |
+
|
| 171 |
+
# Permission risks
|
| 172 |
+
high_risk_perms = len([p for p in results.get('permission_analysis', [])
|
| 173 |
+
if p.get('risk_level') in ['CRITICAL', 'HIGH']])
|
| 174 |
+
score += min(30, high_risk_perms * 8)
|
| 175 |
+
|
| 176 |
+
# Security policy issues
|
| 177 |
+
security_issues = len(results.get('security_analysis', []))
|
| 178 |
+
score += min(25, security_issues * 5)
|
| 179 |
+
|
| 180 |
+
# Anomalies (NEW)
|
| 181 |
+
anomaly_issues = len(results.get('anomaly_analysis', []))
|
| 182 |
+
high_anomalies = len([a for a in results.get('anomaly_analysis', [])
|
| 183 |
+
if a.get('severity') == 'High'])
|
| 184 |
+
score += min(20, high_anomalies * 10 + (anomaly_issues - high_anomalies) * 3)
|
| 185 |
+
|
| 186 |
+
return min(100, score)
|
| 187 |
+
|
| 188 |
+
def _generate_recommendations(self, results):
|
| 189 |
+
"""Generate AI-powered recommendations"""
|
| 190 |
+
recommendations = []
|
| 191 |
+
|
| 192 |
+
if results.get('code_analysis'):
|
| 193 |
+
recommendations.append(
|
| 194 |
+
"🔍 Code vulnerabilities detected - Review Apex classes for security issues"
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
if results.get('permission_analysis'):
|
| 198 |
+
high_risk = [p for p in results['permission_analysis']
|
| 199 |
+
if p.get('risk_level') == 'CRITICAL']
|
| 200 |
+
if high_risk:
|
| 201 |
+
recommendations.append(
|
| 202 |
+
f"⚠️ {len(high_risk)} critical permission risks - Implement least privilege"
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
if results.get('security_analysis'):
|
| 206 |
+
recommendations.append(
|
| 207 |
+
"🛡️ Security policy gaps identified - Strengthen access controls"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
# NEW: Anomaly recommendations
|
| 211 |
+
if results.get('anomaly_analysis'):
|
| 212 |
+
high_anomalies = [a for a in results['anomaly_analysis']
|
| 213 |
+
if a.get('severity') == 'High']
|
| 214 |
+
if high_anomalies:
|
| 215 |
+
recommendations.append(
|
| 216 |
+
f"🚨 {len(high_anomalies)} high-risk behavioral anomalies - Investigate immediately"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
login_anomalies = [a for a in results['anomaly_analysis']
|
| 220 |
+
if 'Login' in a.get('type', '')]
|
| 221 |
+
if login_anomalies:
|
| 222 |
+
recommendations.append(
|
| 223 |
+
f"🔐 {len(login_anomalies)} unusual login patterns - Enable MFA and IP restrictions"
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
if not recommendations:
|
| 227 |
+
recommendations.append("✅ No critical AI-detected issues found")
|
| 228 |
+
|
| 229 |
+
return recommendations
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# ============================================================================
|
| 233 |
+
# CODEBERT ANALYZER (For Apex Code)
|
| 234 |
+
# ============================================================================
|
| 235 |
+
|
| 236 |
+
class CodeBERTAnalyzer:
|
| 237 |
+
"""
|
| 238 |
+
CodeBERT model for analyzing Apex code
|
| 239 |
+
Zero-shot vulnerability detection using embeddings
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
def __init__(self):
|
| 243 |
+
self.model_name = "microsoft/codebert-base"
|
| 244 |
+
self.tokenizer = RobertaTokenizer.from_pretrained(self.model_name)
|
| 245 |
+
self.model = RobertaModel.from_pretrained(self.model_name)
|
| 246 |
+
self.model.eval()
|
| 247 |
+
|
| 248 |
+
# Vulnerability patterns (embeddings will be compared against these)
|
| 249 |
+
self.vulnerability_patterns = {
|
| 250 |
+
'SOQL_INJECTION': [
|
| 251 |
+
'dynamic SOQL query with string concatenation',
|
| 252 |
+
'database query with user input without binding',
|
| 253 |
+
'SQL injection vulnerability in Salesforce'
|
| 254 |
+
],
|
| 255 |
+
'MISSING_SHARING': [
|
| 256 |
+
'class without sharing keywords',
|
| 257 |
+
'missing with sharing declaration',
|
| 258 |
+
'no sharing enforcement in Apex'
|
| 259 |
+
],
|
| 260 |
+
'HARDCODED_CREDENTIALS': [
|
| 261 |
+
'hardcoded API key in code',
|
| 262 |
+
'exposed credentials in source',
|
| 263 |
+
'plaintext password in class'
|
| 264 |
+
],
|
| 265 |
+
'UNSAFE_DML': [
|
| 266 |
+
'DML operation without try catch',
|
| 267 |
+
'unhandled database operation',
|
| 268 |
+
'unsafe insert update delete'
|
| 269 |
+
]
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
# Pre-compute pattern embeddings
|
| 273 |
+
self.pattern_embeddings = self._compute_pattern_embeddings()
|
| 274 |
+
|
| 275 |
+
def _compute_pattern_embeddings(self):
|
| 276 |
+
"""Pre-compute embeddings for vulnerability patterns"""
|
| 277 |
+
embeddings = {}
|
| 278 |
+
|
| 279 |
+
for vuln_type, patterns in self.vulnerability_patterns.items():
|
| 280 |
+
pattern_embs = []
|
| 281 |
+
for pattern in patterns:
|
| 282 |
+
emb = self._get_embedding(pattern)
|
| 283 |
+
pattern_embs.append(emb)
|
| 284 |
+
embeddings[vuln_type] = np.mean(pattern_embs, axis=0)
|
| 285 |
+
|
| 286 |
+
return embeddings
|
| 287 |
+
|
| 288 |
+
def _get_embedding(self, text):
|
| 289 |
+
"""Get embedding for text using CodeBERT"""
|
| 290 |
+
inputs = self.tokenizer(
|
| 291 |
+
text,
|
| 292 |
+
return_tensors='pt',
|
| 293 |
+
max_length=512,
|
| 294 |
+
truncation=True,
|
| 295 |
+
padding=True
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
with torch.no_grad():
|
| 299 |
+
outputs = self.model(**inputs)
|
| 300 |
+
# Use [CLS] token embedding
|
| 301 |
+
embedding = outputs.last_hidden_state[:, 0, :].numpy()
|
| 302 |
+
|
| 303 |
+
return embedding[0]
|
| 304 |
+
|
| 305 |
+
def analyze_code_snippet(self, code):
|
| 306 |
+
"""Analyze a single code snippet"""
|
| 307 |
+
# Get code embedding
|
| 308 |
+
code_embedding = self._get_embedding(code)
|
| 309 |
+
|
| 310 |
+
# Compare with vulnerability patterns
|
| 311 |
+
vulnerabilities = []
|
| 312 |
+
|
| 313 |
+
for vuln_type, pattern_emb in self.pattern_embeddings.items():
|
| 314 |
+
similarity = cosine_similarity(
|
| 315 |
+
code_embedding.reshape(1, -1),
|
| 316 |
+
pattern_emb.reshape(1, -1)
|
| 317 |
+
)[0][0]
|
| 318 |
+
|
| 319 |
+
# Threshold for detection (tune this based on testing)
|
| 320 |
+
if similarity > 0.7:
|
| 321 |
+
vulnerabilities.append({
|
| 322 |
+
'type': vuln_type,
|
| 323 |
+
'confidence': float(similarity),
|
| 324 |
+
'severity': 'HIGH' if similarity > 0.85 else 'MEDIUM'
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
+
# Also use pattern matching as backup
|
| 328 |
+
pattern_matches = self._pattern_match(code)
|
| 329 |
+
vulnerabilities.extend(pattern_matches)
|
| 330 |
+
|
| 331 |
+
return vulnerabilities
|
| 332 |
+
|
| 333 |
+
def _pattern_match(self, code):
|
| 334 |
+
"""Simple pattern matching for common issues"""
|
| 335 |
+
issues = []
|
| 336 |
+
|
| 337 |
+
if 'Database.query(' in code and '+' in code:
|
| 338 |
+
issues.append({
|
| 339 |
+
'type': 'POTENTIAL_SOQL_INJECTION',
|
| 340 |
+
'confidence': 0.9,
|
| 341 |
+
'severity': 'CRITICAL',
|
| 342 |
+
'description': 'Dynamic SOQL with string concatenation detected'
|
| 343 |
+
})
|
| 344 |
+
|
| 345 |
+
if ('public class' in code and
|
| 346 |
+
'with sharing' not in code and
|
| 347 |
+
'without sharing' not in code):
|
| 348 |
+
issues.append({
|
| 349 |
+
'type': 'MISSING_SHARING',
|
| 350 |
+
'confidence': 0.95,
|
| 351 |
+
'severity': 'HIGH',
|
| 352 |
+
'description': 'Class missing sharing declaration'
|
| 353 |
+
})
|
| 354 |
+
|
| 355 |
+
# Check for potential hardcoded credentials
|
| 356 |
+
credential_keywords = ['password', 'apikey', 'api_key', 'secret', 'token']
|
| 357 |
+
for keyword in credential_keywords:
|
| 358 |
+
if f'{keyword} =' in code.lower() or f'{keyword}=' in code.lower():
|
| 359 |
+
issues.append({
|
| 360 |
+
'type': 'POTENTIAL_HARDCODED_CREDENTIALS',
|
| 361 |
+
'confidence': 0.8,
|
| 362 |
+
'severity': 'CRITICAL',
|
| 363 |
+
'description': f'Possible hardcoded {keyword} found'
|
| 364 |
+
})
|
| 365 |
+
break
|
| 366 |
+
|
| 367 |
+
return issues
|
| 368 |
+
|
| 369 |
+
def analyze_apex_code(self, apex_classes):
|
| 370 |
+
"""Analyze multiple Apex classes"""
|
| 371 |
+
results = []
|
| 372 |
+
|
| 373 |
+
for apex_class in apex_classes[:10]: # Limit to avoid timeout
|
| 374 |
+
class_name = apex_class.get('Name', 'Unknown')
|
| 375 |
+
code = apex_class.get('Body', '')
|
| 376 |
+
|
| 377 |
+
if not code:
|
| 378 |
+
continue
|
| 379 |
+
|
| 380 |
+
vulnerabilities = self.analyze_code_snippet(code)
|
| 381 |
+
|
| 382 |
+
if vulnerabilities:
|
| 383 |
+
results.append({
|
| 384 |
+
'class_name': class_name,
|
| 385 |
+
'vulnerabilities': vulnerabilities,
|
| 386 |
+
'total_issues': len(vulnerabilities)
|
| 387 |
+
})
|
| 388 |
+
|
| 389 |
+
return results
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# ============================================================================
|
| 393 |
+
# ROBERTA ANALYZER (For Permission Classification)
|
| 394 |
+
# ============================================================================
|
| 395 |
+
|
| 396 |
+
class RoBERTaAnalyzer:
|
| 397 |
+
"""
|
| 398 |
+
RoBERTa model for permission risk classification
|
| 399 |
+
Zero-shot classification using semantic similarity
|
| 400 |
+
"""
|
| 401 |
+
|
| 402 |
+
def __init__(self):
|
| 403 |
+
self.model_name = "roberta-base"
|
| 404 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 405 |
+
self.model = AutoModel.from_pretrained(self.model_name)
|
| 406 |
+
self.model.eval()
|
| 407 |
+
|
| 408 |
+
# Risk level definitions
|
| 409 |
+
self.risk_definitions = {
|
| 410 |
+
'CRITICAL': [
|
| 411 |
+
'full system access with modify all data',
|
| 412 |
+
'administrative privileges with user management',
|
| 413 |
+
'unrestricted access to all records',
|
| 414 |
+
'complete control over organization data'
|
| 415 |
+
],
|
| 416 |
+
'HIGH': [
|
| 417 |
+
'elevated permissions with data modification',
|
| 418 |
+
'access to sensitive user information',
|
| 419 |
+
'ability to change security settings',
|
| 420 |
+
'export capabilities for all data'
|
| 421 |
+
],
|
| 422 |
+
'MEDIUM': [
|
| 423 |
+
'limited administrative functions',
|
| 424 |
+
'read access to multiple objects',
|
| 425 |
+
'standard user permissions with extras',
|
| 426 |
+
'moderate data access rights'
|
| 427 |
+
],
|
| 428 |
+
'LOW': [
|
| 429 |
+
'basic user permissions',
|
| 430 |
+
'read-only access to owned records',
|
| 431 |
+
'minimal system privileges',
|
| 432 |
+
'restricted data access'
|
| 433 |
+
]
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
# Pre-compute risk embeddings
|
| 437 |
+
self.risk_embeddings = self._compute_risk_embeddings()
|
| 438 |
+
|
| 439 |
+
def _compute_risk_embeddings(self):
|
| 440 |
+
"""Pre-compute embeddings for risk levels"""
|
| 441 |
+
embeddings = {}
|
| 442 |
+
|
| 443 |
+
for risk_level, descriptions in self.risk_definitions.items():
|
| 444 |
+
risk_embs = []
|
| 445 |
+
for desc in descriptions:
|
| 446 |
+
emb = self._get_embedding(desc)
|
| 447 |
+
risk_embs.append(emb)
|
| 448 |
+
embeddings[risk_level] = np.mean(risk_embs, axis=0)
|
| 449 |
+
|
| 450 |
+
return embeddings
|
| 451 |
+
|
| 452 |
+
def _get_embedding(self, text):
|
| 453 |
+
"""Get embedding using RoBERTa"""
|
| 454 |
+
inputs = self.tokenizer(
|
| 455 |
+
text,
|
| 456 |
+
return_tensors='pt',
|
| 457 |
+
max_length=512,
|
| 458 |
+
truncation=True,
|
| 459 |
+
padding=True
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
with torch.no_grad():
|
| 463 |
+
outputs = self.model(**inputs)
|
| 464 |
+
embedding = outputs.last_hidden_state[:, 0, :].numpy()
|
| 465 |
+
|
| 466 |
+
return embedding[0]
|
| 467 |
+
|
| 468 |
+
def classify_permission_set(self, permission_set):
|
| 469 |
+
"""Classify a single permission set's risk level"""
|
| 470 |
+
|
| 471 |
+
# Build description from permissions
|
| 472 |
+
dangerous_perms = []
|
| 473 |
+
|
| 474 |
+
if permission_set.get('PermissionsModifyAllData'):
|
| 475 |
+
dangerous_perms.append('modify all data')
|
| 476 |
+
if permission_set.get('PermissionsViewAllData'):
|
| 477 |
+
dangerous_perms.append('view all data')
|
| 478 |
+
if permission_set.get('PermissionsManageUsers'):
|
| 479 |
+
dangerous_perms.append('manage users')
|
| 480 |
+
if permission_set.get('PermissionsAuthorApex'):
|
| 481 |
+
dangerous_perms.append('author apex code')
|
| 482 |
+
|
| 483 |
+
if not dangerous_perms:
|
| 484 |
+
return {
|
| 485 |
+
'name': permission_set.get('Name', 'Unknown'),
|
| 486 |
+
'risk_level': 'LOW',
|
| 487 |
+
'confidence': 0.9,
|
| 488 |
+
'dangerous_permissions': []
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
# Create description
|
| 492 |
+
description = f"permission set with {', '.join(dangerous_perms)}"
|
| 493 |
+
|
| 494 |
+
# Get embedding
|
| 495 |
+
perm_embedding = self._get_embedding(description)
|
| 496 |
+
|
| 497 |
+
# Compare with risk levels
|
| 498 |
+
similarities = {}
|
| 499 |
+
for risk_level, risk_emb in self.risk_embeddings.items():
|
| 500 |
+
similarity = cosine_similarity(
|
| 501 |
+
perm_embedding.reshape(1, -1),
|
| 502 |
+
risk_emb.reshape(1, -1)
|
| 503 |
+
)[0][0]
|
| 504 |
+
similarities[risk_level] = similarity
|
| 505 |
+
|
| 506 |
+
# Get highest similarity
|
| 507 |
+
predicted_risk = max(similarities, key=similarities.get)
|
| 508 |
+
confidence = similarities[predicted_risk]
|
| 509 |
+
|
| 510 |
+
return {
|
| 511 |
+
'name': permission_set.get('Name', 'Unknown'),
|
| 512 |
+
'risk_level': predicted_risk,
|
| 513 |
+
'confidence': float(confidence),
|
| 514 |
+
'dangerous_permissions': dangerous_perms
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
def analyze_permissions(self, permission_sets, profiles):
|
| 518 |
+
"""Analyze all permission sets and profiles"""
|
| 519 |
+
results = []
|
| 520 |
+
|
| 521 |
+
# Analyze permission sets
|
| 522 |
+
for perm_set in permission_sets[:20]: # Limit for performance
|
| 523 |
+
result = self.classify_permission_set(perm_set)
|
| 524 |
+
result['type'] = 'Permission Set'
|
| 525 |
+
results.append(result)
|
| 526 |
+
|
| 527 |
+
# Analyze profiles
|
| 528 |
+
for profile in profiles[:10]:
|
| 529 |
+
result = self.classify_permission_set(profile)
|
| 530 |
+
result['type'] = 'Profile'
|
| 531 |
+
results.append(result)
|
| 532 |
+
|
| 533 |
+
return results
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
# ============================================================================
|
| 537 |
+
# SECBERT ANALYZER (For Security Policies)
|
| 538 |
+
# ============================================================================
|
| 539 |
+
|
| 540 |
+
class SecBERTAnalyzer:
|
| 541 |
+
"""
|
| 542 |
+
SecBERT model for security policy analysis
|
| 543 |
+
Detects security misconfigurations and policy violations
|
| 544 |
+
"""
|
| 545 |
+
|
| 546 |
+
def __init__(self):
|
| 547 |
+
# SecBERT might not be available, fallback to RoBERTa
|
| 548 |
+
try:
|
| 549 |
+
self.model_name = "jackaduma/SecBERT"
|
| 550 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 551 |
+
self.model = AutoModel.from_pretrained(self.model_name)
|
| 552 |
+
except:
|
| 553 |
+
logger.warning("⚠️ SecBERT not available, using RoBERTa as fallback")
|
| 554 |
+
self.model_name = "roberta-base"
|
| 555 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 556 |
+
self.model = AutoModel.from_pretrained(self.model_name)
|
| 557 |
+
|
| 558 |
+
self.model.eval()
|
| 559 |
+
|
| 560 |
+
# Security violation patterns
|
| 561 |
+
self.security_patterns = {
|
| 562 |
+
'MFA_NOT_ENFORCED': [
|
| 563 |
+
'multi-factor authentication not enabled',
|
| 564 |
+
'no MFA requirement for users',
|
| 565 |
+
'missing two-factor authentication'
|
| 566 |
+
],
|
| 567 |
+
'EXCESSIVE_ADMINS': [
|
| 568 |
+
'too many administrative accounts',
|
| 569 |
+
'excessive system administrator privileges',
|
| 570 |
+
'multiple users with full access'
|
| 571 |
+
],
|
| 572 |
+
'DORMANT_ACCOUNTS': [
|
| 573 |
+
'inactive user accounts with permissions',
|
| 574 |
+
'dormant administrative accounts',
|
| 575 |
+
'unused accounts with access rights'
|
| 576 |
+
],
|
| 577 |
+
'WEAK_PASSWORD_POLICY': [
|
| 578 |
+
'insufficient password requirements',
|
| 579 |
+
'weak password complexity rules',
|
| 580 |
+
'no password expiration policy'
|
| 581 |
+
]
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
self.pattern_embeddings = self._compute_pattern_embeddings()
|
| 585 |
+
|
| 586 |
+
def _compute_pattern_embeddings(self):
|
| 587 |
+
"""Pre-compute embeddings for security patterns"""
|
| 588 |
+
embeddings = {}
|
| 589 |
+
|
| 590 |
+
for pattern_type, descriptions in self.security_patterns.items():
|
| 591 |
+
pattern_embs = []
|
| 592 |
+
for desc in descriptions:
|
| 593 |
+
emb = self._get_embedding(desc)
|
| 594 |
+
pattern_embs.append(emb)
|
| 595 |
+
embeddings[pattern_type] = np.mean(pattern_embs, axis=0)
|
| 596 |
+
|
| 597 |
+
return embeddings
|
| 598 |
+
|
| 599 |
+
def _get_embedding(self, text):
|
| 600 |
+
"""Get embedding using SecBERT/RoBERTa"""
|
| 601 |
+
inputs = self.tokenizer(
|
| 602 |
+
text,
|
| 603 |
+
return_tensors='pt',
|
| 604 |
+
max_length=512,
|
| 605 |
+
truncation=True,
|
| 606 |
+
padding=True
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
with torch.no_grad():
|
| 610 |
+
outputs = self.model(**inputs)
|
| 611 |
+
embedding = outputs.last_hidden_state[:, 0, :].numpy()
|
| 612 |
+
|
| 613 |
+
return embedding[0]
|
| 614 |
+
|
| 615 |
+
def analyze_security(self, metadata):
|
| 616 |
+
"""Analyze security policies and configurations"""
|
| 617 |
+
findings = []
|
| 618 |
+
|
| 619 |
+
users = metadata.get('users', [])
|
| 620 |
+
profiles = metadata.get('profiles', [])
|
| 621 |
+
|
| 622 |
+
# Check MFA compliance
|
| 623 |
+
mfa_result = self._check_mfa_compliance(users)
|
| 624 |
+
if mfa_result:
|
| 625 |
+
findings.append(mfa_result)
|
| 626 |
+
|
| 627 |
+
# Check admin count
|
| 628 |
+
admin_result = self._check_admin_count(users, profiles)
|
| 629 |
+
if admin_result:
|
| 630 |
+
findings.append(admin_result)
|
| 631 |
+
|
| 632 |
+
# Check dormant accounts
|
| 633 |
+
dormant_result = self._check_dormant_accounts(users)
|
| 634 |
+
if dormant_result:
|
| 635 |
+
findings.append(dormant_result)
|
| 636 |
+
|
| 637 |
+
return findings
|
| 638 |
+
|
| 639 |
+
def _check_mfa_compliance(self, users):
|
| 640 |
+
"""Check MFA compliance using AI"""
|
| 641 |
+
# Simple heuristic for demo
|
| 642 |
+
total_users = len(users)
|
| 643 |
+
if total_users == 0:
|
| 644 |
+
return None
|
| 645 |
+
|
| 646 |
+
# In real scenario, check MfaEnabled field
|
| 647 |
+
# For demo, create finding
|
| 648 |
+
description = f"multi-factor authentication compliance check for {total_users} users"
|
| 649 |
+
emb = self._get_embedding(description)
|
| 650 |
+
|
| 651 |
+
mfa_pattern = self.pattern_embeddings['MFA_NOT_ENFORCED']
|
| 652 |
+
similarity = cosine_similarity(
|
| 653 |
+
emb.reshape(1, -1),
|
| 654 |
+
mfa_pattern.reshape(1, -1)
|
| 655 |
+
)[0][0]
|
| 656 |
+
|
| 657 |
+
if similarity > 0.6:
|
| 658 |
+
return {
|
| 659 |
+
'type': 'MFA_NOT_ENFORCED',
|
| 660 |
+
'severity': 'HIGH',
|
| 661 |
+
'confidence': float(similarity),
|
| 662 |
+
'description': 'MFA may not be enforced for all users',
|
| 663 |
+
'recommendation': 'Enable MFA for all users, especially admins'
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
return None
|
| 667 |
+
|
| 668 |
+
def _check_admin_count(self, users, profiles):
|
| 669 |
+
"""Check for excessive administrators"""
|
| 670 |
+
admin_count = len([u for u in users if 'Admin' in str(u.get('Profile', {}))])
|
| 671 |
+
|
| 672 |
+
if admin_count > 5:
|
| 673 |
+
description = f"{admin_count} administrative users with full system access"
|
| 674 |
+
emb = self._get_embedding(description)
|
| 675 |
+
|
| 676 |
+
pattern = self.pattern_embeddings['EXCESSIVE_ADMINS']
|
| 677 |
+
similarity = cosine_similarity(
|
| 678 |
+
emb.reshape(1, -1),
|
| 679 |
+
pattern.reshape(1, -1)
|
| 680 |
+
)[0][0]
|
| 681 |
+
|
| 682 |
+
return {
|
| 683 |
+
'type': 'EXCESSIVE_ADMINS',
|
| 684 |
+
'severity': 'MEDIUM',
|
| 685 |
+
'confidence': float(similarity),
|
| 686 |
+
'admin_count': admin_count,
|
| 687 |
+
'description': f'{admin_count} users have System Administrator profile',
|
| 688 |
+
'recommendation': 'Reduce admin count, use Permission Sets instead'
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
return None
|
| 692 |
+
|
| 693 |
+
def _check_dormant_accounts(self, users):
|
| 694 |
+
"""Check for dormant user accounts"""
|
| 695 |
+
# Simple check - in production, check LastLoginDate
|
| 696 |
+
inactive_count = len([u for u in users if not u.get('IsActive', True)])
|
| 697 |
+
|
| 698 |
+
if inactive_count > 0:
|
| 699 |
+
description = f"{inactive_count} inactive user accounts still present"
|
| 700 |
+
emb = self._get_embedding(description)
|
| 701 |
+
|
| 702 |
+
pattern = self.pattern_embeddings['DORMANT_ACCOUNTS']
|
| 703 |
+
similarity = cosine_similarity(
|
| 704 |
+
emb.reshape(1, -1),
|
| 705 |
+
pattern.reshape(1, -1)
|
| 706 |
+
)[0][0]
|
| 707 |
+
|
| 708 |
+
return {
|
| 709 |
+
'type': 'DORMANT_ACCOUNTS',
|
| 710 |
+
'severity': 'LOW',
|
| 711 |
+
'confidence': float(similarity),
|
| 712 |
+
'inactive_count': inactive_count,
|
| 713 |
+
'description': f'{inactive_count} inactive accounts found',
|
| 714 |
+
'recommendation': 'Review and remove dormant accounts'
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
return None
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
# ============================================================================
|
| 721 |
+
# HELPER FUNCTIONS FOR INTEGRATION
|
| 722 |
+
# ============================================================================
|
| 723 |
+
|
| 724 |
+
def get_ai_summary(analysis_results):
|
| 725 |
+
"""Generate human-readable summary from AI analysis"""
|
| 726 |
+
|
| 727 |
+
if not analysis_results.get('available'):
|
| 728 |
+
return "AI analysis not available"
|
| 729 |
+
|
| 730 |
+
summary_parts = []
|
| 731 |
+
|
| 732 |
+
# Code analysis summary
|
| 733 |
+
if analysis_results.get('code_analysis'):
|
| 734 |
+
code_issues = len(analysis_results['code_analysis'])
|
| 735 |
+
summary_parts.append(f"Detected {code_issues} code vulnerabilities")
|
| 736 |
+
|
| 737 |
+
# Permission analysis summary
|
| 738 |
+
if analysis_results.get('permission_analysis'):
|
| 739 |
+
critical = len([p for p in analysis_results['permission_analysis']
|
| 740 |
+
if p.get('risk_level') == 'CRITICAL'])
|
| 741 |
+
if critical > 0:
|
| 742 |
+
summary_parts.append(f"Found {critical} critical permission risks")
|
| 743 |
+
|
| 744 |
+
# Security analysis summary
|
| 745 |
+
if analysis_results.get('security_analysis'):
|
| 746 |
+
security_issues = len(analysis_results['security_analysis'])
|
| 747 |
+
summary_parts.append(f"Identified {security_issues} security policy gaps")
|
| 748 |
+
|
| 749 |
+
# NEW: Anomaly analysis summary
|
| 750 |
+
if analysis_results.get('anomaly_analysis'):
|
| 751 |
+
anomaly_count = len(analysis_results['anomaly_analysis'])
|
| 752 |
+
high_anomalies = len([a for a in analysis_results['anomaly_analysis']
|
| 753 |
+
if a.get('severity') == 'High'])
|
| 754 |
+
if high_anomalies > 0:
|
| 755 |
+
summary_parts.append(f"Detected {high_anomalies} high-risk behavioral anomalies")
|
| 756 |
+
elif anomaly_count > 0:
|
| 757 |
+
summary_parts.append(f"Detected {anomaly_count} behavioral anomalies")
|
| 758 |
+
|
| 759 |
+
if not summary_parts:
|
| 760 |
+
return "AI analysis completed - no critical issues detected"
|
| 761 |
+
|
| 762 |
+
return ". ".join(summary_parts) + "."
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
# ============================================================================
|
| 766 |
+
# EXAMPLE USAGE
|
| 767 |
+
# ============================================================================
|
| 768 |
+
|
| 769 |
+
if __name__ == "__main__":
|
| 770 |
+
|
| 771 |
+
print("=" * 80)
|
| 772 |
+
print("MULTI-MODEL AI ANALYZER - TEST")
|
| 773 |
+
print("=" * 80)
|
| 774 |
+
|
| 775 |
+
# Initialize analyzer
|
| 776 |
+
analyzer = MultiModelAnalyzer()
|
| 777 |
+
|
| 778 |
+
if not analyzer.available:
|
| 779 |
+
print("❌ AI models failed to load")
|
| 780 |
+
exit(1)
|
| 781 |
+
|
| 782 |
+
# Test data
|
| 783 |
+
test_metadata = {
|
| 784 |
+
'apex_classes': [
|
| 785 |
+
{
|
| 786 |
+
'Name': 'UnsafeController',
|
| 787 |
+
'Body': '''
|
| 788 |
+
public class UnsafeController {
|
| 789 |
+
public void queryData(String input) {
|
| 790 |
+
String query = 'SELECT Id FROM User WHERE Name = \\'' + input + '\\'';
|
| 791 |
+
Database.query(query);
|
| 792 |
+
}
|
| 793 |
+
}
|
| 794 |
+
'''
|
| 795 |
+
}
|
| 796 |
+
],
|
| 797 |
+
'permission_sets': [
|
| 798 |
+
{
|
| 799 |
+
'Name': 'AdminPermSet',
|
| 800 |
+
'PermissionsModifyAllData': True,
|
| 801 |
+
'PermissionsViewAllData': True,
|
| 802 |
+
'PermissionsManageUsers': True
|
| 803 |
+
}
|
| 804 |
+
],
|
| 805 |
+
'profiles': [
|
| 806 |
+
{
|
| 807 |
+
'Name': 'System Administrator',
|
| 808 |
+
'PermissionsModifyAllData': True,
|
| 809 |
+
'PermissionsViewAllData': True
|
| 810 |
+
}
|
| 811 |
+
],
|
| 812 |
+
'users': [
|
| 813 |
+
{'Id': '1', 'IsActive': True},
|
| 814 |
+
{'Id': '2', 'IsActive': True},
|
| 815 |
+
{'Id': '3', 'IsActive': False}
|
| 816 |
+
],
|
| 817 |
+
'login_history': []
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
# Run analysis
|
| 821 |
+
print("\n🔍 Running AI analysis...")
|
| 822 |
+
results = analyzer.analyze_org(test_metadata)
|
| 823 |
+
|
| 824 |
+
# Display results
|
| 825 |
+
print("\n📊 RESULTS:")
|
| 826 |
+
print(f" Overall AI Risk Score: {results['overall_risk_score']}/100")
|
| 827 |
+
print(f" Code Issues: {len(results.get('code_analysis', []))}")
|
| 828 |
+
print(f" Permission Risks: {len(results.get('permission_analysis', []))}")
|
| 829 |
+
print(f" Security Findings: {len(results.get('security_analysis', []))}")
|
| 830 |
+
print(f" Anomalies Detected: {len(results.get('anomaly_analysis', []))}")
|
| 831 |
+
|
| 832 |
+
print("\n💡 AI Recommendations:")
|
| 833 |
+
for rec in results.get('ai_recommendations', []):
|
| 834 |
+
print(f" • {rec}")
|
| 835 |
+
|
| 836 |
+
print("\n" + "=" * 80)
|
| 837 |
+
print("✅ Multi-Model AI Analysis Complete!")
|
| 838 |
+
print("=" * 80)
|
anomaly_detector.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
anomaly_detector.py - Behavioral Anomaly Detection
|
| 3 |
+
Add this file to detect unusual login patterns and permission usage
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
from anomaly_detector import AnomalyDetector
|
| 7 |
+
|
| 8 |
+
detector = AnomalyDetector()
|
| 9 |
+
anomalies = detector.detect_login_anomalies(login_history)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from sklearn.ensemble import IsolationForest
|
| 14 |
+
from datetime import datetime, timedelta
|
| 15 |
+
import logging
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
class AnomalyDetector:
|
| 20 |
+
"""
|
| 21 |
+
Detects anomalous behavior in Salesforce org using Isolation Forest
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self):
|
| 25 |
+
"""Initialize anomaly detector"""
|
| 26 |
+
self.login_detector = IsolationForest(
|
| 27 |
+
contamination=0.1, # Expect ~10% anomalies
|
| 28 |
+
random_state=42
|
| 29 |
+
)
|
| 30 |
+
self.available = True
|
| 31 |
+
logger.info("✅ Anomaly Detector initialized")
|
| 32 |
+
|
| 33 |
+
def detect_login_anomalies(self, login_history: list) -> list:
|
| 34 |
+
"""
|
| 35 |
+
Detect anomalous login patterns
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
login_history: List of login records
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
List of anomaly findings
|
| 42 |
+
"""
|
| 43 |
+
if not login_history or len(login_history) < 10:
|
| 44 |
+
return []
|
| 45 |
+
|
| 46 |
+
findings = []
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
# Extract features from login history
|
| 50 |
+
features = self._extract_login_features(login_history)
|
| 51 |
+
|
| 52 |
+
if len(features) < 10:
|
| 53 |
+
return []
|
| 54 |
+
|
| 55 |
+
# Fit and predict anomalies
|
| 56 |
+
predictions = self.login_detector.fit_predict(features)
|
| 57 |
+
scores = self.login_detector.score_samples(features)
|
| 58 |
+
|
| 59 |
+
# Find anomalies (prediction = -1)
|
| 60 |
+
for idx, (pred, score) in enumerate(zip(predictions, scores)):
|
| 61 |
+
if pred == -1: # Anomaly detected
|
| 62 |
+
login = login_history[idx]
|
| 63 |
+
|
| 64 |
+
# Determine anomaly type
|
| 65 |
+
anomaly_type = self._classify_login_anomaly(login, login_history)
|
| 66 |
+
|
| 67 |
+
findings.append({
|
| 68 |
+
'type': f'Anomalous Login - {anomaly_type}',
|
| 69 |
+
'severity': 'High' if score < -0.5 else 'Medium',
|
| 70 |
+
'user_id': login.get('UserId', 'Unknown'),
|
| 71 |
+
'login_time': login.get('LoginTime', 'Unknown'),
|
| 72 |
+
'source_ip': login.get('SourceIp', 'Unknown'),
|
| 73 |
+
'anomaly_score': float(score),
|
| 74 |
+
'description': f'Unusual {anomaly_type.lower()} detected',
|
| 75 |
+
'impact': 'Potential account compromise or unauthorized access',
|
| 76 |
+
'recommendation': 'Investigate login context, verify with user, consider MFA'
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
logger.info(f"🔍 Detected {len(findings)} login anomalies")
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"❌ Anomaly detection error: {e}")
|
| 83 |
+
|
| 84 |
+
return findings
|
| 85 |
+
|
| 86 |
+
def _extract_login_features(self, login_history: list) -> np.ndarray:
|
| 87 |
+
"""
|
| 88 |
+
Extract numerical features from login history
|
| 89 |
+
|
| 90 |
+
Features:
|
| 91 |
+
- Hour of day (0-23)
|
| 92 |
+
- Day of week (0-6)
|
| 93 |
+
- Login success (0/1)
|
| 94 |
+
- IP address hash (numeric representation)
|
| 95 |
+
"""
|
| 96 |
+
features = []
|
| 97 |
+
|
| 98 |
+
for login in login_history:
|
| 99 |
+
try:
|
| 100 |
+
login_time = login.get('LoginTime', '')
|
| 101 |
+
if not login_time:
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
# Parse timestamp
|
| 105 |
+
dt = datetime.fromisoformat(login_time.replace('Z', '+00:00'))
|
| 106 |
+
|
| 107 |
+
# Extract features
|
| 108 |
+
hour = dt.hour
|
| 109 |
+
day_of_week = dt.weekday()
|
| 110 |
+
is_success = 1 if login.get('Status') == 'Success' else 0
|
| 111 |
+
|
| 112 |
+
# Simple IP hash (just use last octet for demo)
|
| 113 |
+
ip = login.get('SourceIp', '0.0.0.0')
|
| 114 |
+
ip_hash = hash(ip) % 1000
|
| 115 |
+
|
| 116 |
+
features.append([hour, day_of_week, is_success, ip_hash])
|
| 117 |
+
|
| 118 |
+
except Exception as e:
|
| 119 |
+
continue
|
| 120 |
+
|
| 121 |
+
return np.array(features) if features else np.array([])
|
| 122 |
+
|
| 123 |
+
def _classify_login_anomaly(self, login: dict, all_logins: list) -> str:
|
| 124 |
+
"""
|
| 125 |
+
Classify the type of anomaly detected
|
| 126 |
+
"""
|
| 127 |
+
login_time = login.get('LoginTime', '')
|
| 128 |
+
source_ip = login.get('SourceIp', '')
|
| 129 |
+
status = login.get('Status', '')
|
| 130 |
+
|
| 131 |
+
if not login_time:
|
| 132 |
+
return 'Unknown'
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
dt = datetime.fromisoformat(login_time.replace('Z', '+00:00'))
|
| 136 |
+
hour = dt.hour
|
| 137 |
+
|
| 138 |
+
# Check time-based anomalies
|
| 139 |
+
if hour < 6 or hour > 22:
|
| 140 |
+
return 'Off-Hours Access'
|
| 141 |
+
|
| 142 |
+
# Check IP-based anomalies
|
| 143 |
+
common_ips = [l.get('SourceIp') for l in all_logins]
|
| 144 |
+
if source_ip not in common_ips[:10]: # Not in top 10 IPs
|
| 145 |
+
return 'Unusual Location'
|
| 146 |
+
|
| 147 |
+
# Check status-based anomalies
|
| 148 |
+
if status == 'Failed':
|
| 149 |
+
return 'Failed Login Attempt'
|
| 150 |
+
|
| 151 |
+
return 'Unusual Pattern'
|
| 152 |
+
|
| 153 |
+
except Exception:
|
| 154 |
+
return 'Unknown'
|
| 155 |
+
|
| 156 |
+
def detect_permission_anomalies(self, users: list, permission_sets: list) -> list:
|
| 157 |
+
"""
|
| 158 |
+
Detect users with anomalous permission combinations
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
users: List of user records
|
| 162 |
+
permission_sets: List of permission set assignments
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
List of permission anomaly findings
|
| 166 |
+
"""
|
| 167 |
+
findings = []
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
# Count permissions per user
|
| 171 |
+
user_perm_counts = {}
|
| 172 |
+
|
| 173 |
+
for ps in permission_sets:
|
| 174 |
+
user_id = ps.get('AssigneeId')
|
| 175 |
+
if user_id:
|
| 176 |
+
user_perm_counts[user_id] = user_perm_counts.get(user_id, 0) + 1
|
| 177 |
+
|
| 178 |
+
if not user_perm_counts:
|
| 179 |
+
return []
|
| 180 |
+
|
| 181 |
+
# Convert to array for anomaly detection
|
| 182 |
+
perm_counts = np.array(list(user_perm_counts.values())).reshape(-1, 1)
|
| 183 |
+
|
| 184 |
+
if len(perm_counts) < 5:
|
| 185 |
+
return []
|
| 186 |
+
|
| 187 |
+
# Detect anomalies
|
| 188 |
+
detector = IsolationForest(contamination=0.1, random_state=42)
|
| 189 |
+
predictions = detector.fit_predict(perm_counts)
|
| 190 |
+
|
| 191 |
+
# Find users with anomalous permission counts
|
| 192 |
+
for user_id, perm_count in user_perm_counts.items():
|
| 193 |
+
idx = list(user_perm_counts.keys()).index(user_id)
|
| 194 |
+
|
| 195 |
+
if predictions[idx] == -1: # Anomaly
|
| 196 |
+
# Find user details
|
| 197 |
+
user = next((u for u in users if u.get('Id') == user_id), {})
|
| 198 |
+
|
| 199 |
+
findings.append({
|
| 200 |
+
'type': 'Anomalous Permission Assignment',
|
| 201 |
+
'severity': 'Medium',
|
| 202 |
+
'user_id': user_id,
|
| 203 |
+
'username': user.get('Username', 'Unknown'),
|
| 204 |
+
'permission_count': perm_count,
|
| 205 |
+
'description': f'User has {perm_count} permission sets (unusual)',
|
| 206 |
+
'impact': 'Potential privilege escalation or excessive access',
|
| 207 |
+
'recommendation': 'Review permission assignments, ensure least privilege'
|
| 208 |
+
})
|
| 209 |
+
|
| 210 |
+
logger.info(f"🔍 Detected {len(findings)} permission anomalies")
|
| 211 |
+
|
| 212 |
+
except Exception as e:
|
| 213 |
+
logger.error(f"❌ Permission anomaly detection error: {e}")
|
| 214 |
+
|
| 215 |
+
return findings
|
| 216 |
+
|
| 217 |
+
def detect_dormant_account_risks(self, users: list) -> list:
|
| 218 |
+
"""
|
| 219 |
+
Detect dormant accounts with elevated privileges
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
users: List of user records
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
List of dormant account findings
|
| 226 |
+
"""
|
| 227 |
+
findings = []
|
| 228 |
+
threshold_days = 90
|
| 229 |
+
threshold_date = datetime.now() - timedelta(days=threshold_days)
|
| 230 |
+
|
| 231 |
+
for user in users:
|
| 232 |
+
last_login = user.get('LastLoginDate')
|
| 233 |
+
profile = user.get('Profile', {}).get('Name', '')
|
| 234 |
+
|
| 235 |
+
# Check for dormant admin accounts
|
| 236 |
+
if 'Admin' in profile or 'System Administrator' in profile:
|
| 237 |
+
|
| 238 |
+
if not last_login:
|
| 239 |
+
findings.append({
|
| 240 |
+
'type': 'Dormant Admin Account - Never Used',
|
| 241 |
+
'severity': 'High',
|
| 242 |
+
'user_id': user.get('Id'),
|
| 243 |
+
'username': user.get('Username'),
|
| 244 |
+
'profile': profile,
|
| 245 |
+
'description': 'Admin account never logged in',
|
| 246 |
+
'impact': 'Orphaned privileged account - prime target for attackers',
|
| 247 |
+
'recommendation': 'Deactivate immediately or remove admin privileges'
|
| 248 |
+
})
|
| 249 |
+
|
| 250 |
+
elif isinstance(last_login, str):
|
| 251 |
+
try:
|
| 252 |
+
last_login_date = datetime.fromisoformat(last_login.replace('Z', '+00:00'))
|
| 253 |
+
|
| 254 |
+
if last_login_date < threshold_date:
|
| 255 |
+
days_inactive = (datetime.now() - last_login_date).days
|
| 256 |
+
|
| 257 |
+
findings.append({
|
| 258 |
+
'type': 'Dormant Admin Account',
|
| 259 |
+
'severity': 'High',
|
| 260 |
+
'user_id': user.get('Id'),
|
| 261 |
+
'username': user.get('Username'),
|
| 262 |
+
'profile': profile,
|
| 263 |
+
'days_inactive': days_inactive,
|
| 264 |
+
'last_login': last_login,
|
| 265 |
+
'description': f'Admin account inactive for {days_inactive} days',
|
| 266 |
+
'impact': 'Orphaned privileged account - security risk',
|
| 267 |
+
'recommendation': 'Deactivate or remove admin privileges'
|
| 268 |
+
})
|
| 269 |
+
except Exception:
|
| 270 |
+
pass
|
| 271 |
+
|
| 272 |
+
logger.info(f"🔍 Detected {len(findings)} dormant account risks")
|
| 273 |
+
return findings
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# ============================================================================
|
| 277 |
+
# INTEGRATION HELPER
|
| 278 |
+
# ============================================================================
|
| 279 |
+
|
| 280 |
+
def analyze_with_anomaly_detection(metadata: dict) -> dict:
|
| 281 |
+
"""
|
| 282 |
+
Run anomaly detection on org metadata
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
metadata: Org metadata from Salesforce
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
Anomaly analysis results
|
| 289 |
+
"""
|
| 290 |
+
detector = AnomalyDetector()
|
| 291 |
+
|
| 292 |
+
findings = []
|
| 293 |
+
|
| 294 |
+
# Detect login anomalies
|
| 295 |
+
login_findings = detector.detect_login_anomalies(
|
| 296 |
+
metadata.get('login_history', [])
|
| 297 |
+
)
|
| 298 |
+
findings.extend(login_findings)
|
| 299 |
+
|
| 300 |
+
# Detect permission anomalies
|
| 301 |
+
perm_findings = detector.detect_permission_anomalies(
|
| 302 |
+
users=metadata.get('users', []),
|
| 303 |
+
permission_sets=metadata.get('permission_sets', [])
|
| 304 |
+
)
|
| 305 |
+
findings.extend(perm_findings)
|
| 306 |
+
|
| 307 |
+
# Detect dormant account risks
|
| 308 |
+
dormant_findings = detector.detect_dormant_account_risks(
|
| 309 |
+
metadata.get('users', [])
|
| 310 |
+
)
|
| 311 |
+
findings.extend(dormant_findings)
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
'findings': findings,
|
| 315 |
+
'total_anomalies': len(findings),
|
| 316 |
+
'login_anomalies': len(login_findings),
|
| 317 |
+
'permission_anomalies': len(perm_findings),
|
| 318 |
+
'dormant_accounts': len(dormant_findings)
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
if __name__ == '__main__':
|
| 323 |
+
# Test
|
| 324 |
+
print("=" * 80)
|
| 325 |
+
print("ANOMALY DETECTOR - TEST")
|
| 326 |
+
print("=" * 80)
|
| 327 |
+
|
| 328 |
+
detector = AnomalyDetector()
|
| 329 |
+
|
| 330 |
+
# Test login data
|
| 331 |
+
test_logins = [
|
| 332 |
+
{'UserId': '1', 'LoginTime': '2026-01-19T09:00:00Z', 'SourceIp': '192.168.1.1', 'Status': 'Success'},
|
| 333 |
+
{'UserId': '2', 'LoginTime': '2026-01-19T03:00:00Z', 'SourceIp': '10.0.0.1', 'Status': 'Success'}, # Anomaly: 3 AM
|
| 334 |
+
{'UserId': '3', 'LoginTime': '2026-01-19T10:00:00Z', 'SourceIp': '192.168.1.1', 'Status': 'Success'},
|
| 335 |
+
] * 5 # Repeat to have enough data
|
| 336 |
+
|
| 337 |
+
anomalies = detector.detect_login_anomalies(test_logins)
|
| 338 |
+
|
| 339 |
+
print(f"\n✅ Detected {len(anomalies)} anomalies")
|
| 340 |
+
for anomaly in anomalies:
|
| 341 |
+
print(f" • {anomaly['type']} - {anomaly['description']}")
|
| 342 |
+
|
| 343 |
+
print("\n" + "=" * 80)
|
| 344 |
+
print("✅ Anomaly Detection Test Complete!")
|
| 345 |
+
print("=" * 80)
|
app.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Optimax Security Agent",
|
| 3 |
+
"description": "AI-powered Salesforce security analysis",
|
| 4 |
+
"keywords": ["salesforce", "security", "ai"],
|
| 5 |
+
"formation": {
|
| 6 |
+
"web": {
|
| 7 |
+
"quantity": 1,
|
| 8 |
+
"size": "standard-2x"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"env": {
|
| 12 |
+
"SF_CLIENT_ID": {
|
| 13 |
+
"description": "Your Salesforce Client ID",
|
| 14 |
+
"required": true
|
| 15 |
+
},
|
| 16 |
+
"SF_CLIENT_SECRET": {
|
| 17 |
+
"description": "Your Salesforce Client Secret",
|
| 18 |
+
"required": true
|
| 19 |
+
},
|
| 20 |
+
"SF_REDIRECT_URI": {
|
| 21 |
+
"description": "OAuth Redirect URI",
|
| 22 |
+
"value": "https://login.salesforce.com/services/oauth2/success"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime, timezone
|
| 5 |
+
from simple_salesforce import Salesforce
|
| 6 |
+
import requests
|
| 7 |
+
from urllib.parse import urlencode, urlparse, parse_qs, unquote
|
| 8 |
+
import secrets
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
# Import AI models
|
| 12 |
+
from ai_models import MultiModelAnalyzer, get_ai_summary
|
| 13 |
+
|
| 14 |
+
# Import analyzers
|
| 15 |
+
from permission_analyzer import PermissionAnalyzer
|
| 16 |
+
from identity_analyzer import IdentityAnalyzer
|
| 17 |
+
from sharing_analyzer import SharingAnalyzer
|
| 18 |
+
|
| 19 |
+
# Import report generator
|
| 20 |
+
from optimax_reports import generate_report
|
| 21 |
+
|
| 22 |
+
# Setup logging
|
| 23 |
+
logging.basicConfig(level=logging.INFO)
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# ============================================================================
|
| 27 |
+
# OAUTH CONFIGURATION
|
| 28 |
+
# ============================================================================
|
| 29 |
+
|
| 30 |
+
SF_CLIENT_ID = "3MVG9VMBZCsTL9hmy3bhf4_UX7eXivaplob0liijsZucnNjPHD1yTL4J6mxXSN42BHh9wwOhQLbrA_vsl.oqc"
|
| 31 |
+
SF_CLIENT_SECRET = "4AC7C18215B1312D5148278BDC01D327DE8BB282DF9C5B697733DAF0E47A736E"
|
| 32 |
+
SF_REDIRECT_URI = "https://login.salesforce.com/services/oauth2/success"
|
| 33 |
+
|
| 34 |
+
# Session storage
|
| 35 |
+
OAUTH_SESSIONS = {}
|
| 36 |
+
|
| 37 |
+
# ============================================================================
|
| 38 |
+
# INITIALIZE COMPONENTS
|
| 39 |
+
# ============================================================================
|
| 40 |
+
|
| 41 |
+
logger.info("=" * 80)
|
| 42 |
+
logger.info("🚀 INITIALIZING OPTIMAX SECURITY AGENT")
|
| 43 |
+
logger.info("=" * 80)
|
| 44 |
+
|
| 45 |
+
# Initialize rule-based analyzers
|
| 46 |
+
logger.info("📋 Loading rule-based analyzers...")
|
| 47 |
+
perm_analyzer = PermissionAnalyzer()
|
| 48 |
+
identity_analyzer = IdentityAnalyzer()
|
| 49 |
+
sharing_analyzer = SharingAnalyzer()
|
| 50 |
+
logger.info("✅ Rule-based analyzers loaded")
|
| 51 |
+
|
| 52 |
+
# Initialize AI models
|
| 53 |
+
logger.info("")
|
| 54 |
+
logger.info("🤖 Loading Multi-Model AI Analyzer...")
|
| 55 |
+
logger.info(" This may take 30-60 seconds on first run...")
|
| 56 |
+
try:
|
| 57 |
+
ai_analyzer = MultiModelAnalyzer()
|
| 58 |
+
if ai_analyzer.available:
|
| 59 |
+
logger.info("✅ AI Models loaded successfully!")
|
| 60 |
+
logger.info(" • CodeBERT (125M) - Code vulnerability detection")
|
| 61 |
+
logger.info(" • RoBERTa (125M) - Permission risk classification")
|
| 62 |
+
logger.info(" • SecBERT (110M) - Security policy analysis")
|
| 63 |
+
logger.info(" • Isolation Forest - Behavioral anomaly detection")
|
| 64 |
+
else:
|
| 65 |
+
logger.warning("⚠️ AI models failed to load - using rule-based analysis only")
|
| 66 |
+
ai_analyzer = None
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error(f"⚠️ AI models not available: {e}")
|
| 69 |
+
logger.warning(" Continuing with rule-based analysis only")
|
| 70 |
+
ai_analyzer = None
|
| 71 |
+
|
| 72 |
+
logger.info("")
|
| 73 |
+
logger.info("=" * 80)
|
| 74 |
+
logger.info("✅ ALL COMPONENTS INITIALIZED")
|
| 75 |
+
logger.info("=" * 80)
|
| 76 |
+
|
| 77 |
+
# ============================================================================
|
| 78 |
+
# OAUTH FUNCTIONS WITH AUTO CODE EXTRACTION
|
| 79 |
+
# ============================================================================
|
| 80 |
+
|
| 81 |
+
def check_credentials_configured():
|
| 82 |
+
"""Check if OAuth credentials are properly configured"""
|
| 83 |
+
return "YOUR_" not in SF_CLIENT_ID and "YOUR_" not in SF_CLIENT_SECRET
|
| 84 |
+
|
| 85 |
+
def generate_oauth_url(org_type: str) -> tuple:
|
| 86 |
+
"""Generate OAuth URL for user authorization with JavaScript auto-extraction"""
|
| 87 |
+
|
| 88 |
+
if not check_credentials_configured():
|
| 89 |
+
return "", "", "⚠️ OAuth credentials not configured"
|
| 90 |
+
|
| 91 |
+
# Generate unique session ID
|
| 92 |
+
session_id = secrets.token_urlsafe(32)
|
| 93 |
+
OAUTH_SESSIONS[session_id] = {
|
| 94 |
+
"status": "pending",
|
| 95 |
+
"org_type": org_type,
|
| 96 |
+
"created_at": datetime.now(timezone.utc),
|
| 97 |
+
"code": None
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
# Determine authorization endpoint
|
| 101 |
+
if org_type == "sandbox":
|
| 102 |
+
auth_endpoint = "https://test.salesforce.com/services/oauth2/authorize"
|
| 103 |
+
redirect_uri = "https://test.salesforce.com/services/oauth2/success"
|
| 104 |
+
else:
|
| 105 |
+
auth_endpoint = "https://login.salesforce.com/services/oauth2/authorize"
|
| 106 |
+
redirect_uri = SF_REDIRECT_URI
|
| 107 |
+
|
| 108 |
+
# Build OAuth parameters
|
| 109 |
+
params = {
|
| 110 |
+
"response_type": "code",
|
| 111 |
+
"client_id": SF_CLIENT_ID,
|
| 112 |
+
"redirect_uri": redirect_uri,
|
| 113 |
+
"scope": "full refresh_token",
|
| 114 |
+
"state": session_id, # Pass session ID for tracking
|
| 115 |
+
"prompt": "login" # Force fresh login every time
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
oauth_url = f"{auth_endpoint}?{urlencode(params)}"
|
| 119 |
+
|
| 120 |
+
logger.info("=" * 80)
|
| 121 |
+
logger.info(f"🔗 Generated OAuth URL for {org_type.upper()}")
|
| 122 |
+
logger.info(f" Session ID: {session_id}")
|
| 123 |
+
logger.info(f" Redirect URI: {redirect_uri}")
|
| 124 |
+
logger.info("=" * 80)
|
| 125 |
+
|
| 126 |
+
# Create instructions with JavaScript auto-extraction
|
| 127 |
+
instructions = f"""
|
| 128 |
+
## ✅ Step 1: Click Authorization Link Below
|
| 129 |
+
|
| 130 |
+
Click the **blue button** to open Salesforce authorization.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## 🔐 Step 2: Log In & Allow Access
|
| 135 |
+
|
| 136 |
+
1. **Log into your {org_type.title()} org**
|
| 137 |
+
2. Click **"Allow"** to grant permissions
|
| 138 |
+
3. The authorization code will be **automatically detected!** ✨
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## ⏳ Step 3: Wait for Automatic Processing
|
| 143 |
+
|
| 144 |
+
Once you authorize:
|
| 145 |
+
- 🔍 The code will be **automatically extracted** from the URL
|
| 146 |
+
- 🚀 The security scan will **start automatically**
|
| 147 |
+
- 📊 Results will appear below
|
| 148 |
+
|
| 149 |
+
**No manual copying needed!** 🎉
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
**Session ID:** `{session_id}`
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
return session_id, oauth_url, instructions
|
| 157 |
+
|
| 158 |
+
def connect_production_org():
|
| 159 |
+
"""Connect to Production/Developer Org"""
|
| 160 |
+
return generate_oauth_url("production")
|
| 161 |
+
|
| 162 |
+
def connect_sandbox_org():
|
| 163 |
+
"""Connect to Sandbox Org"""
|
| 164 |
+
return generate_oauth_url("sandbox")
|
| 165 |
+
|
| 166 |
+
def exchange_code_for_token(code: str, org_type: str) -> dict:
|
| 167 |
+
"""Exchange authorization code for access token"""
|
| 168 |
+
|
| 169 |
+
# Determine token endpoint
|
| 170 |
+
if org_type == "sandbox":
|
| 171 |
+
token_endpoint = "https://test.salesforce.com/services/oauth2/token"
|
| 172 |
+
redirect_uri = "https://test.salesforce.com/services/oauth2/success"
|
| 173 |
+
else:
|
| 174 |
+
token_endpoint = "https://login.salesforce.com/services/oauth2/token"
|
| 175 |
+
redirect_uri = SF_REDIRECT_URI
|
| 176 |
+
|
| 177 |
+
# Clean and decode the authorization code
|
| 178 |
+
clean_code = unquote(code.strip())
|
| 179 |
+
|
| 180 |
+
logger.info(f"🔐 Exchanging code for {org_type} token...")
|
| 181 |
+
logger.info(f" Code length: {len(clean_code)} chars")
|
| 182 |
+
|
| 183 |
+
# Exchange code for token
|
| 184 |
+
response = requests.post(
|
| 185 |
+
token_endpoint,
|
| 186 |
+
data={
|
| 187 |
+
"grant_type": "authorization_code",
|
| 188 |
+
"client_id": SF_CLIENT_ID,
|
| 189 |
+
"client_secret": SF_CLIENT_SECRET,
|
| 190 |
+
"redirect_uri": redirect_uri,
|
| 191 |
+
"code": clean_code
|
| 192 |
+
},
|
| 193 |
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
if response.status_code != 200:
|
| 197 |
+
try:
|
| 198 |
+
error_data = response.json()
|
| 199 |
+
logger.error(f"❌ Token exchange failed: {error_data}")
|
| 200 |
+
return {
|
| 201 |
+
"error": error_data.get("error", "unknown_error"),
|
| 202 |
+
"error_description": error_data.get("error_description", "Token exchange failed")
|
| 203 |
+
}
|
| 204 |
+
except:
|
| 205 |
+
logger.error(f"❌ Token exchange failed with status {response.status_code}")
|
| 206 |
+
return {
|
| 207 |
+
"error": "token_exchange_failed",
|
| 208 |
+
"error_description": f"HTTP {response.status_code}"
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
token_data = response.json()
|
| 212 |
+
logger.info("✅ Token exchange successful!")
|
| 213 |
+
|
| 214 |
+
return token_data
|
| 215 |
+
|
| 216 |
+
# ============================================================================
|
| 217 |
+
# CORE ANALYSIS FUNCTIONS (same as original)
|
| 218 |
+
# ============================================================================
|
| 219 |
+
|
| 220 |
+
def extract_metadata(sf: Salesforce) -> dict:
|
| 221 |
+
"""Extract metadata from Salesforce org"""
|
| 222 |
+
|
| 223 |
+
logger.info("📡 Extracting org metadata...")
|
| 224 |
+
metadata = {}
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
# Get users
|
| 228 |
+
logger.info(" • Fetching users...")
|
| 229 |
+
users_query = """
|
| 230 |
+
SELECT Id, Username, Name, Email, IsActive, Profile.Name, LastLoginDate,
|
| 231 |
+
CreatedDate, UserRole.Name
|
| 232 |
+
FROM User
|
| 233 |
+
WHERE IsActive = true
|
| 234 |
+
LIMIT 1000
|
| 235 |
+
"""
|
| 236 |
+
users_result = sf.query(users_query)
|
| 237 |
+
metadata['users'] = users_result['records']
|
| 238 |
+
logger.info(f" ✅ Retrieved {len(metadata['users'])} users")
|
| 239 |
+
|
| 240 |
+
# Get permission sets
|
| 241 |
+
logger.info(" • Fetching permission sets...")
|
| 242 |
+
ps_query = """
|
| 243 |
+
SELECT Id, Name, Label, Description, IsOwnedByProfile,
|
| 244 |
+
PermissionsModifyAllData, PermissionsViewAllData, PermissionsManageUsers,
|
| 245 |
+
PermissionsCustomizeApplication, PermissionsAuthorApex,
|
| 246 |
+
(SELECT Id, AssigneeId, Assignee.Username, Assignee.Name, Assignee.Email
|
| 247 |
+
FROM Assignments)
|
| 248 |
+
FROM PermissionSet
|
| 249 |
+
WHERE IsOwnedByProfile = false
|
| 250 |
+
LIMIT 500
|
| 251 |
+
"""
|
| 252 |
+
ps_result = sf.query(ps_query)
|
| 253 |
+
metadata['permission_sets'] = ps_result['records']
|
| 254 |
+
logger.info(f" ✅ Retrieved {len(metadata['permission_sets'])} permission sets")
|
| 255 |
+
|
| 256 |
+
# Get profiles
|
| 257 |
+
logger.info(" • Fetching profiles...")
|
| 258 |
+
profiles_query = """
|
| 259 |
+
SELECT Id, Name, UserLicense.Name, IsCustom,
|
| 260 |
+
PermissionsModifyAllData, PermissionsViewAllData, PermissionsManageUsers,
|
| 261 |
+
PermissionsCustomizeApplication, PermissionsAuthorApex
|
| 262 |
+
FROM Profile
|
| 263 |
+
LIMIT 100
|
| 264 |
+
"""
|
| 265 |
+
profiles_result = sf.query(profiles_query)
|
| 266 |
+
metadata['profiles'] = profiles_result['records']
|
| 267 |
+
logger.info(f" ✅ Retrieved {len(metadata['profiles'])} profiles")
|
| 268 |
+
|
| 269 |
+
# Get org info
|
| 270 |
+
logger.info(" • Fetching org info...")
|
| 271 |
+
org_query = "SELECT Id, Name, OrganizationType, InstanceName FROM Organization LIMIT 1"
|
| 272 |
+
org_result = sf.query(org_query)
|
| 273 |
+
if org_result['records']:
|
| 274 |
+
metadata['organization'] = org_result['records'][0]
|
| 275 |
+
logger.info(f" ✅ Retrieved org info: {metadata['organization'].get('Name')}")
|
| 276 |
+
|
| 277 |
+
# Try to get login history
|
| 278 |
+
try:
|
| 279 |
+
logger.info(" • Fetching login history...")
|
| 280 |
+
login_query = """
|
| 281 |
+
SELECT Id, UserId, LoginTime, SourceIp, Status, LoginType
|
| 282 |
+
FROM LoginHistory
|
| 283 |
+
WHERE LoginTime = LAST_N_DAYS:30
|
| 284 |
+
LIMIT 1000
|
| 285 |
+
"""
|
| 286 |
+
login_result = sf.query(login_query)
|
| 287 |
+
metadata['login_history'] = login_result['records']
|
| 288 |
+
logger.info(f" ✅ Retrieved {len(metadata['login_history'])} login records")
|
| 289 |
+
except Exception as e:
|
| 290 |
+
logger.warning(f" ⚠️ Could not fetch login history: {e}")
|
| 291 |
+
metadata['login_history'] = []
|
| 292 |
+
|
| 293 |
+
logger.info("✅ Metadata extraction complete!")
|
| 294 |
+
return metadata
|
| 295 |
+
|
| 296 |
+
except Exception as e:
|
| 297 |
+
logger.error(f"❌ Error extracting metadata: {e}")
|
| 298 |
+
raise
|
| 299 |
+
|
| 300 |
+
def calculate_risk_score(findings: list) -> tuple:
|
| 301 |
+
"""Calculate overall risk score based on findings"""
|
| 302 |
+
|
| 303 |
+
score = 0
|
| 304 |
+
severity_weights = {
|
| 305 |
+
'Critical': 30,
|
| 306 |
+
'High': 15,
|
| 307 |
+
'Medium': 5,
|
| 308 |
+
'Low': 1
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
for finding in findings:
|
| 312 |
+
severity = finding.get('severity', 'Low')
|
| 313 |
+
score += severity_weights.get(severity, 0)
|
| 314 |
+
|
| 315 |
+
# Normalize to 0-100
|
| 316 |
+
score = min(100, score)
|
| 317 |
+
|
| 318 |
+
# Determine risk level
|
| 319 |
+
if score >= 70:
|
| 320 |
+
risk_level = "CRITICAL"
|
| 321 |
+
elif score >= 50:
|
| 322 |
+
risk_level = "HIGH"
|
| 323 |
+
elif score >= 30:
|
| 324 |
+
risk_level = "MEDIUM"
|
| 325 |
+
else:
|
| 326 |
+
risk_level = "LOW"
|
| 327 |
+
|
| 328 |
+
return score, risk_level
|
| 329 |
+
|
| 330 |
+
def analyze_org(metadata: dict) -> dict:
|
| 331 |
+
"""Perform comprehensive security analysis"""
|
| 332 |
+
|
| 333 |
+
logger.info("")
|
| 334 |
+
logger.info("=" * 80)
|
| 335 |
+
logger.info("🔍 STARTING SECURITY ANALYSIS")
|
| 336 |
+
logger.info("=" * 80)
|
| 337 |
+
|
| 338 |
+
all_findings = []
|
| 339 |
+
|
| 340 |
+
# Create user lookup for analyzers
|
| 341 |
+
user_lookup = {u.get('Id'): u for u in metadata.get('users', [])}
|
| 342 |
+
|
| 343 |
+
# 1. Permission Analysis
|
| 344 |
+
logger.info("1️⃣ Analyzing permissions...")
|
| 345 |
+
perm_results = perm_analyzer.analyze_all_permissions(
|
| 346 |
+
metadata.get('permission_sets', []),
|
| 347 |
+
metadata.get('profiles', []),
|
| 348 |
+
metadata.get('users', [])
|
| 349 |
+
)
|
| 350 |
+
all_findings.extend(perm_results['findings'])
|
| 351 |
+
logger.info(f" ✅ Found {len(perm_results['findings'])} permission issues")
|
| 352 |
+
|
| 353 |
+
# 2. Identity Analysis
|
| 354 |
+
logger.info("2️⃣ Analyzing identity & access...")
|
| 355 |
+
identity_results = identity_analyzer.analyze_users(
|
| 356 |
+
metadata.get('users', []),
|
| 357 |
+
metadata.get('login_history', [])
|
| 358 |
+
)
|
| 359 |
+
all_findings.extend(identity_results['findings'])
|
| 360 |
+
logger.info(f" ✅ Found {len(identity_results['findings'])} identity issues")
|
| 361 |
+
|
| 362 |
+
# 3. Sharing Analysis
|
| 363 |
+
logger.info("3️⃣ Analyzing sharing model...")
|
| 364 |
+
sharing_results = sharing_analyzer.analyze_sharing_settings({
|
| 365 |
+
'organization_wide_defaults': [],
|
| 366 |
+
'role_hierarchy': [],
|
| 367 |
+
'sharing_rules': []
|
| 368 |
+
})
|
| 369 |
+
all_findings.extend(sharing_results['findings'])
|
| 370 |
+
logger.info(f" ✅ Found {len(sharing_results['findings'])} sharing issues")
|
| 371 |
+
|
| 372 |
+
# 4. AI Analysis
|
| 373 |
+
ai_summary = ""
|
| 374 |
+
ai_recommendations = []
|
| 375 |
+
|
| 376 |
+
if ai_analyzer and ai_analyzer.available:
|
| 377 |
+
logger.info("4️⃣ Running AI analysis...")
|
| 378 |
+
try:
|
| 379 |
+
ai_analysis = ai_analyzer.analyze_security(all_findings, metadata)
|
| 380 |
+
ai_summary = ai_analysis.get('executive_summary', '')
|
| 381 |
+
ai_recommendations = ai_analysis.get('recommendations', [])
|
| 382 |
+
|
| 383 |
+
for finding in all_findings:
|
| 384 |
+
if finding.get('type') in ai_analysis.get('ai_insights', {}):
|
| 385 |
+
finding['ai_insight'] = ai_analysis['ai_insights'][finding['type']]
|
| 386 |
+
|
| 387 |
+
logger.info(f" ✅ AI analysis complete")
|
| 388 |
+
except Exception as e:
|
| 389 |
+
logger.warning(f" ⚠️ AI analysis failed: {e}")
|
| 390 |
+
else:
|
| 391 |
+
logger.info("4️⃣ Skipping AI analysis (not available)")
|
| 392 |
+
|
| 393 |
+
# Calculate risk score
|
| 394 |
+
risk_score, risk_level = calculate_risk_score(all_findings)
|
| 395 |
+
|
| 396 |
+
# Organize findings by severity
|
| 397 |
+
findings_by_severity = {
|
| 398 |
+
'Critical': [f for f in all_findings if f.get('severity') == 'Critical'],
|
| 399 |
+
'High': [f for f in all_findings if f.get('severity') == 'High'],
|
| 400 |
+
'Medium': [f for f in all_findings if f.get('severity') == 'Medium'],
|
| 401 |
+
'Low': [f for f in all_findings if f.get('severity') == 'Low']
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
logger.info("")
|
| 405 |
+
logger.info("=" * 80)
|
| 406 |
+
logger.info("✅ ANALYSIS COMPLETE")
|
| 407 |
+
logger.info(f" Risk Score: {risk_score}/100 ({risk_level})")
|
| 408 |
+
logger.info(f" Total Findings: {len(all_findings)}")
|
| 409 |
+
logger.info("=" * 80)
|
| 410 |
+
|
| 411 |
+
return {
|
| 412 |
+
'success': True,
|
| 413 |
+
'overall_risk_score': risk_score,
|
| 414 |
+
'risk_level': risk_level,
|
| 415 |
+
'total_findings': len(all_findings),
|
| 416 |
+
'findings_by_severity': findings_by_severity,
|
| 417 |
+
'all_findings': all_findings,
|
| 418 |
+
'ai_executive_summary': ai_summary,
|
| 419 |
+
'ai_recommendations': ai_recommendations,
|
| 420 |
+
'metadata_summary': {
|
| 421 |
+
'total_users': len(metadata.get('users', [])),
|
| 422 |
+
'active_users': len([u for u in metadata.get('users', []) if u.get('IsActive')]),
|
| 423 |
+
'permission_sets': len(metadata.get('permission_sets', [])),
|
| 424 |
+
'profiles': len(metadata.get('profiles', [])),
|
| 425 |
+
'org_name': metadata.get('organization', {}).get('Name', 'Unknown')
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
def scan_salesforce_org(session_id: str, auth_code_or_url: str):
|
| 430 |
+
"""
|
| 431 |
+
Scan Salesforce org using authorization code
|
| 432 |
+
Accepts either the full URL or just the code
|
| 433 |
+
"""
|
| 434 |
+
|
| 435 |
+
logger.info("=" * 80)
|
| 436 |
+
logger.info(f"🚀 SCAN STARTED - Session: {session_id}")
|
| 437 |
+
logger.info("=" * 80)
|
| 438 |
+
|
| 439 |
+
# Validate session
|
| 440 |
+
if session_id not in OAUTH_SESSIONS:
|
| 441 |
+
error_msg = "❌ Invalid session ID. Please click 'Connect' again."
|
| 442 |
+
logger.error(error_msg)
|
| 443 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps({"error": error_msg}, indent=2), None
|
| 444 |
+
|
| 445 |
+
session = OAUTH_SESSIONS[session_id]
|
| 446 |
+
org_type = session.get('org_type', 'production')
|
| 447 |
+
|
| 448 |
+
# Extract code from URL if full URL was provided
|
| 449 |
+
auth_code = auth_code_or_url.strip()
|
| 450 |
+
|
| 451 |
+
if 'login.salesforce.com' in auth_code or 'test.salesforce.com' in auth_code:
|
| 452 |
+
# Full URL provided - extract code
|
| 453 |
+
try:
|
| 454 |
+
parsed_url = urlparse(auth_code)
|
| 455 |
+
query_params = parse_qs(parsed_url.query)
|
| 456 |
+
|
| 457 |
+
if 'code' in query_params:
|
| 458 |
+
auth_code = query_params['code'][0]
|
| 459 |
+
logger.info("✅ Extracted code from URL")
|
| 460 |
+
else:
|
| 461 |
+
error_msg = "❌ No authorization code found in URL"
|
| 462 |
+
logger.error(error_msg)
|
| 463 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps({"error": error_msg}, indent=2), None
|
| 464 |
+
except Exception as e:
|
| 465 |
+
error_msg = f"❌ Failed to parse URL: {str(e)}"
|
| 466 |
+
logger.error(error_msg)
|
| 467 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps({"error": error_msg}, indent=2), None
|
| 468 |
+
|
| 469 |
+
if not auth_code:
|
| 470 |
+
error_msg = "❌ No authorization code provided"
|
| 471 |
+
logger.error(error_msg)
|
| 472 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps({"error": error_msg}, indent=2), None
|
| 473 |
+
|
| 474 |
+
try:
|
| 475 |
+
# Exchange code for token
|
| 476 |
+
token_data = exchange_code_for_token(auth_code, org_type)
|
| 477 |
+
|
| 478 |
+
if 'error' in token_data:
|
| 479 |
+
error_msg = f"❌ {token_data.get('error_description', 'Authentication failed')}"
|
| 480 |
+
logger.error(error_msg)
|
| 481 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps(token_data, indent=2), None
|
| 482 |
+
|
| 483 |
+
# Connect to Salesforce
|
| 484 |
+
sf = Salesforce(
|
| 485 |
+
instance_url=token_data['instance_url'],
|
| 486 |
+
session_id=token_data['access_token']
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Extract metadata
|
| 490 |
+
metadata = extract_metadata(sf)
|
| 491 |
+
|
| 492 |
+
# Run analysis
|
| 493 |
+
analysis_results = analyze_org(metadata)
|
| 494 |
+
|
| 495 |
+
# Generate HTML report
|
| 496 |
+
report_path = generate_report(analysis_results, metadata)
|
| 497 |
+
|
| 498 |
+
# Read the report
|
| 499 |
+
with open(report_path, 'r') as f:
|
| 500 |
+
report_html = f.read()
|
| 501 |
+
|
| 502 |
+
# Clean up session
|
| 503 |
+
if session_id in OAUTH_SESSIONS:
|
| 504 |
+
del OAUTH_SESSIONS[session_id]
|
| 505 |
+
|
| 506 |
+
logger.info("✅ Scan complete!")
|
| 507 |
+
|
| 508 |
+
return report_html, json.dumps(analysis_results, indent=2, default=str), report_path
|
| 509 |
+
|
| 510 |
+
except Exception as e:
|
| 511 |
+
error_msg = f"❌ Analysis failed: {str(e)}"
|
| 512 |
+
logger.error(error_msg)
|
| 513 |
+
logger.exception("Full error:")
|
| 514 |
+
return f"<div style='color: red; padding: 20px;'>{error_msg}</div>", json.dumps({"error": str(e)}, indent=2), None
|
| 515 |
+
|
| 516 |
+
# ============================================================================
|
| 517 |
+
# JAVASCRIPT AUTO CODE EXTRACTION
|
| 518 |
+
# ============================================================================
|
| 519 |
+
|
| 520 |
+
AUTO_EXTRACT_JS = """
|
| 521 |
+
<script>
|
| 522 |
+
// Auto-extract authorization code from URL
|
| 523 |
+
function autoExtractCode() {
|
| 524 |
+
// Check if we're on the success page
|
| 525 |
+
const currentUrl = window.location.href;
|
| 526 |
+
|
| 527 |
+
// Look for code in URL parameters
|
| 528 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 529 |
+
const code = urlParams.get('code');
|
| 530 |
+
const state = urlParams.get('state');
|
| 531 |
+
|
| 532 |
+
if (code && state) {
|
| 533 |
+
// Send code back to parent window
|
| 534 |
+
if (window.opener) {
|
| 535 |
+
window.opener.postMessage({
|
| 536 |
+
type: 'SALESFORCE_AUTH_CODE',
|
| 537 |
+
code: code,
|
| 538 |
+
state: state
|
| 539 |
+
}, '*');
|
| 540 |
+
|
| 541 |
+
// Close this window
|
| 542 |
+
setTimeout(() => {
|
| 543 |
+
window.close();
|
| 544 |
+
}, 1000);
|
| 545 |
+
}
|
| 546 |
+
}
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
// Listen for auth codes from popup windows
|
| 550 |
+
window.addEventListener('message', function(event) {
|
| 551 |
+
// Verify message is from expected source
|
| 552 |
+
if (event.data.type === 'SALESFORCE_AUTH_CODE') {
|
| 553 |
+
const code = event.data.code;
|
| 554 |
+
const state = event.data.state;
|
| 555 |
+
|
| 556 |
+
// Find the session ID input and auth code input
|
| 557 |
+
const sessionInput = document.querySelector('input[label="📋 Session ID (Auto-filled)"]');
|
| 558 |
+
const codeInput = document.querySelector('textarea[label="🔑 Authorization Code"]');
|
| 559 |
+
|
| 560 |
+
if (codeInput && code) {
|
| 561 |
+
// Auto-fill the code
|
| 562 |
+
codeInput.value = code;
|
| 563 |
+
|
| 564 |
+
// Trigger input event so Gradio detects the change
|
| 565 |
+
const inputEvent = new Event('input', { bubbles: true });
|
| 566 |
+
codeInput.dispatchEvent(inputEvent);
|
| 567 |
+
|
| 568 |
+
// Auto-click the scan button after a short delay
|
| 569 |
+
setTimeout(() => {
|
| 570 |
+
const scanButton = document.querySelector('button:contains("Start AI-Powered Security Scan")');
|
| 571 |
+
if (scanButton) {
|
| 572 |
+
scanButton.click();
|
| 573 |
+
}
|
| 574 |
+
}, 500);
|
| 575 |
+
|
| 576 |
+
console.log('✅ Authorization code auto-extracted and scan started!');
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
});
|
| 580 |
+
|
| 581 |
+
// Run on page load
|
| 582 |
+
autoExtractCode();
|
| 583 |
+
</script>
|
| 584 |
+
"""
|
| 585 |
+
|
| 586 |
+
# ============================================================================
|
| 587 |
+
# GRADIO INTERFACE WITH AUTO EXTRACTION
|
| 588 |
+
# ============================================================================
|
| 589 |
+
|
| 590 |
+
ai_status = "✅ Active" if (ai_analyzer and ai_analyzer.available) else "⚠️ Not Available"
|
| 591 |
+
|
| 592 |
+
with gr.Blocks(
|
| 593 |
+
title="Optimax Security Agent",
|
| 594 |
+
theme=gr.themes.Soft(primary_hue="purple"),
|
| 595 |
+
head=AUTO_EXTRACT_JS # Inject JavaScript for auto-extraction
|
| 596 |
+
) as demo:
|
| 597 |
+
|
| 598 |
+
gr.Markdown("""
|
| 599 |
+
# 🛡️ Optimax Security Agent
|
| 600 |
+
|
| 601 |
+
## Multi-Model AI-Powered Security Scanner
|
| 602 |
+
|
| 603 |
+
**Enhanced with Automatic Code Detection! 🎉**
|
| 604 |
+
""")
|
| 605 |
+
|
| 606 |
+
with gr.Tab("🔍 Scan Your Org"):
|
| 607 |
+
gr.Markdown("""
|
| 608 |
+
### Connect Your Salesforce Org
|
| 609 |
+
|
| 610 |
+
Advanced AI-powered security analysis using 4 specialized models.
|
| 611 |
+
|
| 612 |
+
🔒 **Your credentials stay in Salesforce · We only read metadata · AI + Rule-based analysis**
|
| 613 |
+
|
| 614 |
+
✨ **NEW: Authorization code is automatically detected from the URL!**
|
| 615 |
+
""")
|
| 616 |
+
|
| 617 |
+
with gr.Row():
|
| 618 |
+
prod_button = gr.Button(
|
| 619 |
+
"🏢 Production / Developer Org",
|
| 620 |
+
variant="primary",
|
| 621 |
+
size="lg",
|
| 622 |
+
scale=1
|
| 623 |
+
)
|
| 624 |
+
sandbox_button = gr.Button(
|
| 625 |
+
"🧪 Sandbox Org",
|
| 626 |
+
variant="secondary",
|
| 627 |
+
size="lg",
|
| 628 |
+
scale=1
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
instructions = gr.Markdown("")
|
| 632 |
+
oauth_url_hidden = gr.Textbox(visible=False)
|
| 633 |
+
|
| 634 |
+
# Authorization link - opens in popup for auto-extraction
|
| 635 |
+
auth_link = gr.HTML("")
|
| 636 |
+
|
| 637 |
+
gr.Markdown("---")
|
| 638 |
+
|
| 639 |
+
with gr.Row():
|
| 640 |
+
session_id = gr.Textbox(
|
| 641 |
+
label="📋 Session ID (Auto-filled)",
|
| 642 |
+
interactive=False,
|
| 643 |
+
scale=1
|
| 644 |
+
)
|
| 645 |
+
auth_code = gr.Textbox(
|
| 646 |
+
label="🔑 Authorization Code (Auto-detected)",
|
| 647 |
+
placeholder="Will be automatically filled from the authorization popup...",
|
| 648 |
+
lines=2,
|
| 649 |
+
scale=2
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
+
scan_button = gr.Button(
|
| 653 |
+
"🚀 Start AI-Powered Security Scan",
|
| 654 |
+
variant="primary",
|
| 655 |
+
size="lg"
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
gr.Markdown("""
|
| 659 |
+
💡 **How it works:**
|
| 660 |
+
1. Click "Connect" → Opens authorization in new window
|
| 661 |
+
2. Authorize in Salesforce
|
| 662 |
+
3. Code is **automatically extracted** and scan starts! ✨
|
| 663 |
+
|
| 664 |
+
If automatic detection fails, you can still paste the code manually.
|
| 665 |
+
""")
|
| 666 |
+
|
| 667 |
+
gr.Markdown("---\n### 📊 Security Analysis Results")
|
| 668 |
+
|
| 669 |
+
# Download Button
|
| 670 |
+
download_button = gr.File(
|
| 671 |
+
label="📥 Download Complete HTML Report",
|
| 672 |
+
visible=True,
|
| 673 |
+
interactive=False
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
# HTML Report Display
|
| 677 |
+
report_html = gr.HTML(
|
| 678 |
+
label="Security Report",
|
| 679 |
+
value="<div style='text-align: center; padding: 40px; color: #6b7280;'>Your AI-powered security analysis will appear here...</div>"
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
# Raw JSON
|
| 683 |
+
with gr.Accordion("🔍 View Raw JSON Data", open=False):
|
| 684 |
+
results = gr.Textbox(
|
| 685 |
+
label="Analysis Report (JSON)",
|
| 686 |
+
lines=15,
|
| 687 |
+
placeholder="Raw JSON data..."
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
# Event handlers
|
| 691 |
+
def show_oauth_link(sid, url, inst):
|
| 692 |
+
if url:
|
| 693 |
+
# Open in popup window for auto-extraction
|
| 694 |
+
link_html = f'''
|
| 695 |
+
<div style="text-align: center; margin: 30px 0;">
|
| 696 |
+
<a href="{url}" target="_blank" onclick="window.open(this.href, 'SalesforceAuth', 'width=600,height=700'); return false;" style="display: inline-block; padding: 15px 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 10px; font-size: 18px; font-weight: bold; box-shadow: 0 4px 6px rgba(0,0,0,0.2);">
|
| 697 |
+
🚀 Open Salesforce to Authorize
|
| 698 |
+
</a>
|
| 699 |
+
<p style="margin-top: 15px; color: #666; font-size: 14px;">
|
| 700 |
+
✨ Authorization code will be automatically detected!
|
| 701 |
+
</p>
|
| 702 |
+
</div>
|
| 703 |
+
'''
|
| 704 |
+
return link_html
|
| 705 |
+
return ""
|
| 706 |
+
|
| 707 |
+
prod_button.click(
|
| 708 |
+
fn=connect_production_org,
|
| 709 |
+
outputs=[session_id, oauth_url_hidden, instructions]
|
| 710 |
+
).then(
|
| 711 |
+
fn=show_oauth_link,
|
| 712 |
+
inputs=[session_id, oauth_url_hidden, instructions],
|
| 713 |
+
outputs=[auth_link]
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
sandbox_button.click(
|
| 717 |
+
fn=connect_sandbox_org,
|
| 718 |
+
outputs=[session_id, oauth_url_hidden, instructions]
|
| 719 |
+
).then(
|
| 720 |
+
fn=show_oauth_link,
|
| 721 |
+
inputs=[session_id, oauth_url_hidden, instructions],
|
| 722 |
+
outputs=[auth_link]
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
scan_button.click(
|
| 726 |
+
fn=scan_salesforce_org,
|
| 727 |
+
inputs=[session_id, auth_code],
|
| 728 |
+
outputs=[report_html, results, download_button]
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
with gr.Tab("ℹ️ About"):
|
| 732 |
+
ai_models = ["CodeBERT (125M)", "RoBERTa (125M)", "SecBERT (110M)", "Isolation Forest"] if (ai_analyzer and ai_analyzer.available) else []
|
| 733 |
+
|
| 734 |
+
gr.Markdown(f"""
|
| 735 |
+
## Optimax Security Agent
|
| 736 |
+
|
| 737 |
+
### 🤖 AI Models
|
| 738 |
+
**Status:** {ai_status}
|
| 739 |
+
|
| 740 |
+
{'**Active Models:**' if ai_models else '**AI Models:** Not loaded'}
|
| 741 |
+
{chr(10).join(['- ' + model for model in ai_models]) if ai_models else ''}
|
| 742 |
+
|
| 743 |
+
### 🎯 Features
|
| 744 |
+
- 🤖 Multi-Model AI Analysis (4 specialized models)
|
| 745 |
+
- ✨ **Automatic Code Detection** (JavaScript-based)
|
| 746 |
+
- 📋 Rule-Based Vulnerability Detection
|
| 747 |
+
- 🔍 Permission & Access Analysis
|
| 748 |
+
- 👤 Identity Management Review
|
| 749 |
+
- 🔐 Sharing Model Assessment
|
| 750 |
+
- 🚨 Behavioral Anomaly Detection
|
| 751 |
+
- 📊 Hybrid Risk Scoring (AI + Rules)
|
| 752 |
+
- 💡 Actionable Recommendations
|
| 753 |
+
|
| 754 |
+
### 🔒 Security & Privacy
|
| 755 |
+
- ✅ OAuth 2.0 authentication
|
| 756 |
+
- ✅ Automatic code extraction (browser-side)
|
| 757 |
+
- ✅ Metadata-only scanning
|
| 758 |
+
- ✅ Zero credential storage
|
| 759 |
+
- ✅ No data persistence
|
| 760 |
+
|
| 761 |
+
### 📖 How It Works
|
| 762 |
+
1. Click "Connect" → Opens popup
|
| 763 |
+
2. Authorize in Salesforce
|
| 764 |
+
3. JavaScript auto-detects code from URL
|
| 765 |
+
4. Code auto-fills and scan starts
|
| 766 |
+
5. View comprehensive results
|
| 767 |
+
|
| 768 |
+
**Version:** 3.3.0 (Auto Code Detection)
|
| 769 |
+
**Last Updated:** January 2026
|
| 770 |
+
""")
|
| 771 |
+
|
| 772 |
+
if __name__ == "__main__":
|
| 773 |
+
logger.info("")
|
| 774 |
+
logger.info("=" * 80)
|
| 775 |
+
logger.info("🌐 STARTING GRADIO INTERFACE")
|
| 776 |
+
logger.info("=" * 80)
|
| 777 |
+
logger.info(f" AI Models: {ai_status}")
|
| 778 |
+
logger.info(f" OAuth: ✅ Configured")
|
| 779 |
+
logger.info(f" Mode: Auto code detection (JavaScript)")
|
| 780 |
+
logger.info("=" * 80)
|
| 781 |
+
logger.info("")
|
| 782 |
+
|
| 783 |
+
demo.launch(
|
| 784 |
+
server_name="0.0.0.0",
|
| 785 |
+
server_port=7860,
|
| 786 |
+
show_error=True
|
| 787 |
+
)
|
identity_analyzer.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# analyzers/identity_analyzer.py
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from typing import Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
class IdentityAnalyzer:
|
| 6 |
+
"""
|
| 7 |
+
Analyzes identity and access management for security issues
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def analyze_users(self, users: List[Dict], login_history: List[Dict]) -> Dict:
|
| 11 |
+
"""
|
| 12 |
+
Analyze user accounts for security issues
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
users: List of user records
|
| 16 |
+
login_history: Login history records
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
Analysis results with findings
|
| 20 |
+
"""
|
| 21 |
+
findings = []
|
| 22 |
+
|
| 23 |
+
# Analyze dormant users
|
| 24 |
+
dormant_findings = self._find_dormant_users(users)
|
| 25 |
+
findings.extend(dormant_findings)
|
| 26 |
+
|
| 27 |
+
# Analyze admin users
|
| 28 |
+
admin_findings = self._analyze_admin_users(users)
|
| 29 |
+
findings.extend(admin_findings)
|
| 30 |
+
|
| 31 |
+
# Analyze MFA compliance
|
| 32 |
+
mfa_findings = self._analyze_mfa_compliance(users)
|
| 33 |
+
findings.extend(mfa_findings)
|
| 34 |
+
|
| 35 |
+
# Analyze login anomalies
|
| 36 |
+
login_findings = self._analyze_login_history(login_history)
|
| 37 |
+
findings.extend(login_findings)
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
'findings': findings,
|
| 41 |
+
'total_users': len(users),
|
| 42 |
+
'active_users': len([u for u in users if u.get('IsActive')]),
|
| 43 |
+
'admin_users': len([u for u in users if 'Admin' in u.get('Profile', {}).get('Name', '')])
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def _find_dormant_users(self, users: List[Dict]) -> List[Dict]:
|
| 47 |
+
"""
|
| 48 |
+
Find users who haven't logged in recently
|
| 49 |
+
"""
|
| 50 |
+
findings = []
|
| 51 |
+
threshold_days = 90
|
| 52 |
+
threshold_date = datetime.now() - timedelta(days=threshold_days)
|
| 53 |
+
|
| 54 |
+
for user in users:
|
| 55 |
+
last_login = user.get('LastLoginDate')
|
| 56 |
+
|
| 57 |
+
if not last_login:
|
| 58 |
+
findings.append({
|
| 59 |
+
'type': 'Dormant User - Never Logged In',
|
| 60 |
+
'severity': 'Medium',
|
| 61 |
+
'username': user.get('Username'),
|
| 62 |
+
'user_id': user.get('Id'),
|
| 63 |
+
'name': user.get('Name', 'Unknown'), # ADDED
|
| 64 |
+
'email': user.get('Email', 'N/A'), # ADDED
|
| 65 |
+
'status': 'Active' if user.get('IsActive') else 'Inactive', # ADDED
|
| 66 |
+
'profile': user.get('Profile', {}).get('Name', 'Unknown'),
|
| 67 |
+
'description': f"User {user.get('Username')} has never logged in",
|
| 68 |
+
'recommendation': 'Deactivate or review necessity of this account'
|
| 69 |
+
})
|
| 70 |
+
elif isinstance(last_login, str):
|
| 71 |
+
try:
|
| 72 |
+
last_login_date = datetime.fromisoformat(last_login.replace('Z', '+00:00'))
|
| 73 |
+
if last_login_date < threshold_date:
|
| 74 |
+
days_inactive = (datetime.now() - last_login_date).days
|
| 75 |
+
findings.append({
|
| 76 |
+
'type': 'Dormant User - Inactive',
|
| 77 |
+
'severity': 'Medium',
|
| 78 |
+
'username': user.get('Username'),
|
| 79 |
+
'user_id': user.get('Id'),
|
| 80 |
+
'name': user.get('Name', 'Unknown'), # ADDED
|
| 81 |
+
'email': user.get('Email', 'N/A'), # ADDED
|
| 82 |
+
'status': 'Active' if user.get('IsActive') else 'Inactive', # ADDED
|
| 83 |
+
'profile': user.get('Profile', {}).get('Name', 'Unknown'),
|
| 84 |
+
'days_inactive': days_inactive,
|
| 85 |
+
'last_login': last_login,
|
| 86 |
+
'description': f"User inactive for {days_inactive} days",
|
| 87 |
+
'recommendation': 'Consider deactivating inactive accounts'
|
| 88 |
+
})
|
| 89 |
+
except:
|
| 90 |
+
pass
|
| 91 |
+
|
| 92 |
+
return findings
|
| 93 |
+
|
| 94 |
+
def _analyze_admin_users(self, users: List[Dict]) -> List[Dict]:
|
| 95 |
+
"""
|
| 96 |
+
Analyze administrative users for security risks
|
| 97 |
+
"""
|
| 98 |
+
findings = []
|
| 99 |
+
|
| 100 |
+
admin_users = [
|
| 101 |
+
u for u in users
|
| 102 |
+
if 'Admin' in u.get('Profile', {}).get('Name', '') or
|
| 103 |
+
'System Administrator' in u.get('Profile', {}).get('Name', '')
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
if len(admin_users) > 5:
|
| 107 |
+
# Create detailed user list with name, email, status
|
| 108 |
+
admin_details = []
|
| 109 |
+
for u in admin_users:
|
| 110 |
+
admin_details.append({
|
| 111 |
+
'username': u.get('Username'),
|
| 112 |
+
'name': u.get('Name', 'Unknown'),
|
| 113 |
+
'email': u.get('Email', 'N/A'),
|
| 114 |
+
'status': 'Active' if u.get('IsActive') else 'Inactive'
|
| 115 |
+
})
|
| 116 |
+
|
| 117 |
+
findings.append({
|
| 118 |
+
'type': 'Excessive Admin Users',
|
| 119 |
+
'severity': 'High',
|
| 120 |
+
'admin_count': len(admin_users),
|
| 121 |
+
'description': f"{len(admin_users)} users have System Administrator profile",
|
| 122 |
+
'impact': 'Too many users with full org access increases security risk',
|
| 123 |
+
'recommendation': 'Reduce admin users, use Permission Sets for specific needs',
|
| 124 |
+
'admin_usernames': [u.get('Username') for u in admin_users],
|
| 125 |
+
'admin_details': admin_details # ADDED: Detailed user info
|
| 126 |
+
})
|
| 127 |
+
|
| 128 |
+
return findings
|
| 129 |
+
|
| 130 |
+
def _analyze_mfa_compliance(self, users: List[Dict]) -> List[Dict]:
|
| 131 |
+
"""
|
| 132 |
+
Analyze Multi-Factor Authentication compliance
|
| 133 |
+
"""
|
| 134 |
+
findings = []
|
| 135 |
+
|
| 136 |
+
# Check for users without MFA (if field exists)
|
| 137 |
+
non_mfa_users = []
|
| 138 |
+
non_mfa_details = [] # ADDED
|
| 139 |
+
|
| 140 |
+
for user in users:
|
| 141 |
+
# Note: MfaEnabled__c might be a custom field
|
| 142 |
+
if user.get('MfaEnabled__c') == False:
|
| 143 |
+
non_mfa_users.append(user.get('Username'))
|
| 144 |
+
# ADDED: Collect detailed user info
|
| 145 |
+
non_mfa_details.append({
|
| 146 |
+
'username': user.get('Username'),
|
| 147 |
+
'name': user.get('Name', 'Unknown'),
|
| 148 |
+
'email': user.get('Email', 'N/A'),
|
| 149 |
+
'status': 'Active' if user.get('IsActive') else 'Inactive',
|
| 150 |
+
'profile': user.get('Profile', {}).get('Name', 'Unknown')
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
if non_mfa_users:
|
| 154 |
+
findings.append({
|
| 155 |
+
'type': 'MFA Not Enabled',
|
| 156 |
+
'severity': 'High',
|
| 157 |
+
'users_without_mfa': len(non_mfa_users),
|
| 158 |
+
'description': f"{len(non_mfa_users)} users don't have MFA enabled",
|
| 159 |
+
'impact': 'Accounts vulnerable to credential compromise',
|
| 160 |
+
'recommendation': 'Enable MFA for all users, especially admins',
|
| 161 |
+
'affected_users': non_mfa_users[:10], # Show first 10 usernames
|
| 162 |
+
'user_details': non_mfa_details[:10] # ADDED: First 10 with full details
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
return findings
|
| 166 |
+
|
| 167 |
+
def _analyze_login_history(self, login_history: List[Dict]) -> List[Dict]:
|
| 168 |
+
"""
|
| 169 |
+
Analyze login history for suspicious patterns
|
| 170 |
+
"""
|
| 171 |
+
findings = []
|
| 172 |
+
|
| 173 |
+
# Count failed login attempts
|
| 174 |
+
failed_logins = [l for l in login_history if l.get('Status') == 'Failed']
|
| 175 |
+
|
| 176 |
+
if len(failed_logins) > 100:
|
| 177 |
+
findings.append({
|
| 178 |
+
'type': 'High Failed Login Attempts',
|
| 179 |
+
'severity': 'High',
|
| 180 |
+
'failed_count': len(failed_logins),
|
| 181 |
+
'description': f"{len(failed_logins)} failed login attempts detected",
|
| 182 |
+
'impact': 'Possible brute force attack or credential stuffing',
|
| 183 |
+
'recommendation': 'Review failed attempts, consider IP restrictions'
|
| 184 |
+
})
|
| 185 |
+
|
| 186 |
+
# Check for logins from unusual locations (simplified)
|
| 187 |
+
unique_ips = set(l.get('SourceIp', '') for l in login_history if l.get('SourceIp'))
|
| 188 |
+
|
| 189 |
+
if len(unique_ips) > 100:
|
| 190 |
+
findings.append({
|
| 191 |
+
'type': 'Many Unique Login IPs',
|
| 192 |
+
'severity': 'Medium',
|
| 193 |
+
'unique_ip_count': len(unique_ips),
|
| 194 |
+
'description': f"Logins from {len(unique_ips)} different IP addresses",
|
| 195 |
+
'recommendation': 'Review for unauthorized access, implement IP restrictions'
|
| 196 |
+
})
|
| 197 |
+
|
| 198 |
+
return findings
|
| 199 |
+
|
| 200 |
+
def check_mfa_compliance(self, users: List[Dict]) -> Dict:
|
| 201 |
+
"""
|
| 202 |
+
Check organization-wide MFA compliance
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
MFA compliance statistics
|
| 206 |
+
"""
|
| 207 |
+
total_users = len(users)
|
| 208 |
+
mfa_enabled = sum(1 for u in users if u.get('MfaEnabled__c') == True)
|
| 209 |
+
|
| 210 |
+
return {
|
| 211 |
+
'total_users': total_users,
|
| 212 |
+
'mfa_enabled': mfa_enabled,
|
| 213 |
+
'mfa_disabled': total_users - mfa_enabled,
|
| 214 |
+
'compliance_percentage': (mfa_enabled / total_users * 100) if total_users > 0 else 0,
|
| 215 |
+
'is_compliant': (mfa_enabled / total_users) >= 0.95 if total_users > 0 else False
|
| 216 |
+
}
|
optimax_reports.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Optimax Report Generator - Category-Based Security Analysis
|
| 3 |
+
Organizes findings into 6 security categories WITHOUT mentioning AI models:
|
| 4 |
+
|
| 5 |
+
1. Profiles & Permissions
|
| 6 |
+
2. Sharing Model
|
| 7 |
+
3. API & Integration
|
| 8 |
+
4. Guest/Public Exposure
|
| 9 |
+
5. Custom Code
|
| 10 |
+
6. Identity & Sessions
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
from optimax_reports import generate_report
|
| 14 |
+
html_report = generate_report(analysis_result)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def generate_report(analysis_result: dict) -> str:
|
| 18 |
+
"""Generate comprehensive HTML report organized by security categories"""
|
| 19 |
+
|
| 20 |
+
if not analysis_result.get('success', False):
|
| 21 |
+
return _generate_error_html(analysis_result.get('error', 'Unknown error'))
|
| 22 |
+
|
| 23 |
+
# Extract data
|
| 24 |
+
org_name = analysis_result.get('org_name', 'Unknown Organization')
|
| 25 |
+
org_id = analysis_result.get('org_id', 'N/A')
|
| 26 |
+
timestamp = analysis_result.get('timestamp', '')
|
| 27 |
+
risk_score = analysis_result.get('overall_risk_score', 0)
|
| 28 |
+
risk_level = analysis_result.get('risk_level', 'UNKNOWN')
|
| 29 |
+
|
| 30 |
+
# Get all findings
|
| 31 |
+
all_findings = (
|
| 32 |
+
analysis_result.get('critical_findings', []) +
|
| 33 |
+
analysis_result.get('high_findings', []) +
|
| 34 |
+
analysis_result.get('medium_findings', []) +
|
| 35 |
+
analysis_result.get('low_findings', [])
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
ai_summary = analysis_result.get('ai_executive_summary', '')
|
| 39 |
+
ai_recommendations = analysis_result.get('ai_recommendations', [])
|
| 40 |
+
ai_detailed = analysis_result.get('ai_detailed_results', {})
|
| 41 |
+
statistics = analysis_result.get('statistics', {})
|
| 42 |
+
|
| 43 |
+
# Categorize all findings
|
| 44 |
+
categorized_findings = _categorize_findings(all_findings, ai_detailed)
|
| 45 |
+
|
| 46 |
+
risk_colors = {
|
| 47 |
+
'CRITICAL': '#dc2626',
|
| 48 |
+
'HIGH': '#ea580c',
|
| 49 |
+
'MEDIUM': '#f59e0b',
|
| 50 |
+
'LOW': '#10b981'
|
| 51 |
+
}
|
| 52 |
+
risk_color = risk_colors.get(risk_level, '#6b7280')
|
| 53 |
+
|
| 54 |
+
# Build comprehensive HTML with FIXED download button
|
| 55 |
+
html = f"""
|
| 56 |
+
<style>
|
| 57 |
+
.optimax-report * {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
| 58 |
+
.optimax-report {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; line-height: 1.6; color: #000000 !important; background: #ffffff !important; border-radius: 12px; overflow: hidden; }}
|
| 59 |
+
.optimax-header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff !important; padding: 40px; text-align: center; }}
|
| 60 |
+
.optimax-header h1 {{ font-size: 2.5em; margin-bottom: 10px; font-weight: 700; color: #ffffff !important; }}
|
| 61 |
+
.optimax-header .subtitle {{ font-size: 1.2em; opacity: 1; color: #ffffff !important; margin-top: 10px; }}
|
| 62 |
+
.optimax-content {{ padding: 40px; background: #ffffff !important; }}
|
| 63 |
+
.optimax-section {{ margin-bottom: 40px; }}
|
| 64 |
+
.optimax-section-title {{ font-size: 1.8em; font-weight: 700; margin-bottom: 20px; color: #000000 !important; border-bottom: 3px solid #667eea; padding-bottom: 10px; }}
|
| 65 |
+
.optimax-category-title {{ font-size: 1.4em; font-weight: 700; margin: 30px 0 15px 0; color: #000000 !important; display: flex; align-items: center; gap: 10px; }}
|
| 66 |
+
.optimax-risk-score-box {{ text-align: center; padding: 30px; background: #f9fafb !important; border-radius: 10px; margin-bottom: 30px; border: 1px solid #e5e7eb; }}
|
| 67 |
+
.optimax-risk-score {{ font-size: 4em; font-weight: 700; color: {risk_color} !important; margin: 10px 0; }}
|
| 68 |
+
.optimax-risk-level {{ font-size: 1.3em; font-weight: 700; color: {risk_color} !important; text-transform: uppercase; letter-spacing: 0.1em; }}
|
| 69 |
+
.optimax-risk-cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }}
|
| 70 |
+
.optimax-risk-card {{ background: #ffffff !important; border-radius: 10px; padding: 25px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 5px solid; }}
|
| 71 |
+
.optimax-risk-card.critical {{ border-left-color: #dc2626; }}
|
| 72 |
+
.optimax-risk-card.high {{ border-left-color: #ea580c; }}
|
| 73 |
+
.optimax-risk-card.medium {{ border-left-color: #f59e0b; }}
|
| 74 |
+
.optimax-risk-card.low {{ border-left-color: #10b981; }}
|
| 75 |
+
.optimax-risk-card-title {{ font-size: 0.9em; text-transform: uppercase; color: #4b5563 !important; margin-bottom: 10px; font-weight: 700; }}
|
| 76 |
+
.optimax-risk-card-value {{ font-size: 2.5em; font-weight: 700; color: #000000 !important; text-shadow: 0 1px 2px rgba(0,0,0,0.1); }}
|
| 77 |
+
.optimax-stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 30px; }}
|
| 78 |
+
.optimax-stat-item {{ background: #f9fafb !important; padding: 20px; border-radius: 8px; border-left: 4px solid #667eea; }}
|
| 79 |
+
.optimax-stat-label {{ font-size: 0.85em; color: #4b5563 !important; text-transform: uppercase; font-weight: 700; margin-bottom: 5px; }}
|
| 80 |
+
.optimax-stat-value {{ font-size: 1.6em; font-weight: 700; color: #000000 !important; }}
|
| 81 |
+
.optimax-finding-card {{ background: #f8fafc !important; border: 2px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 15px 0; }}
|
| 82 |
+
.optimax-finding-header {{ display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }}
|
| 83 |
+
.optimax-finding-title {{ font-weight: 700; color: #000000 !important; font-size: 1.05em; flex: 1; min-width: 200px; }}
|
| 84 |
+
.optimax-finding-body {{ color: #000000 !important; margin: 10px 0; line-height: 1.7; }}
|
| 85 |
+
.optimax-finding-body strong {{ color: #000000 !important; font-weight: 700; }}
|
| 86 |
+
.optimax-severity-badge {{ display: inline-block !important; padding: 6px 14px !important; border-radius: 20px !important; font-size: 0.85em !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; white-space: nowrap !important; }}
|
| 87 |
+
.optimax-severity-critical {{ background: #fee2e2 !important; color: #991b1b !important; border: 2px solid #991b1b !important; }}
|
| 88 |
+
.optimax-severity-high {{ background: #ffedd5 !important; color: #9a3412 !important; border: 2px solid #9a3412 !important; }}
|
| 89 |
+
.optimax-severity-medium {{ background: #fef3c7 !important; color: #92400e !important; border: 2px solid #92400e !important; }}
|
| 90 |
+
.optimax-severity-low {{ background: #d1fae5 !important; color: #065f46 !important; border: 2px solid #065f46 !important; }}
|
| 91 |
+
.optimax-summary-box {{ background: #e0f2fe; padding: 25px; border-radius: 10px; margin-bottom: 30px; border-left: 5px solid #0ea5e9; }}
|
| 92 |
+
.optimax-summary-title {{ font-size: 1.3em; font-weight: 700; color: #0c4a6e; margin-bottom: 15px; }}
|
| 93 |
+
.optimax-summary-box p {{ color: #000000 !important; line-height: 1.7; }}
|
| 94 |
+
.optimax-rec-box {{ background: #f0fdf4; padding: 25px; border-radius: 10px; border-left: 5px solid #10b981; }}
|
| 95 |
+
.optimax-rec-title {{ font-size: 1.3em; font-weight: 700; color: #065f46; margin-bottom: 15px; }}
|
| 96 |
+
.optimax-rec-list {{ list-style: none; padding-left: 0; }}
|
| 97 |
+
.optimax-rec-list li {{ padding: 12px 0; padding-left: 30px; position: relative; border-bottom: 1px solid #d1fae5; color: #000000 !important; line-height: 1.6; }}
|
| 98 |
+
.optimax-rec-list li:last-child {{ border-bottom: none; }}
|
| 99 |
+
.optimax-rec-list li::before {{ content: "✓"; position: absolute; left: 0; color: #10b981; font-weight: 700; font-size: 1.2em; }}
|
| 100 |
+
.optimax-no-findings {{ text-align: center; padding: 40px; color: #4b5563 !important; font-style: italic; }}
|
| 101 |
+
.optimax-category-icon {{ font-size: 1.3em; }}
|
| 102 |
+
.optimax-metric-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin: 10px 0; }}
|
| 103 |
+
.optimax-metric-item {{ background: #f9fafb !important; padding: 10px 15px; border-radius: 6px; border-left: 3px solid #667eea; }}
|
| 104 |
+
.optimax-metric-label {{ font-size: 0.75em; color: #4b5563 !important; text-transform: uppercase; font-weight: 700; }}
|
| 105 |
+
.optimax-metric-value {{ font-size: 1.1em; font-weight: 700; color: #000000 !important; margin-top: 3px; }}
|
| 106 |
+
.optimax-recommendation {{ background: #dcfce7; border-left: 4px solid #10b981; padding: 12px 15px; border-radius: 6px; margin: 10px 0; }}
|
| 107 |
+
.optimax-recommendation-title {{ font-weight: 700; color: #166534; font-size: 0.95em; }}
|
| 108 |
+
.optimax-recommendation-text {{ color: #166534; margin-top: 5px; font-size: 0.9em; line-height: 1.5; }}
|
| 109 |
+
.optimax-affected-users {{ background: #fef2f2; padding: 10px 15px; border-radius: 6px; margin: 10px 0; border-left: 3px solid #dc2626; }}
|
| 110 |
+
.optimax-affected-users-title {{ font-weight: 700; color: #991b1b; font-size: 0.9em; }}
|
| 111 |
+
.optimax-affected-users-list {{ color: #991b1b; font-size: 0.85em; margin-top: 5px; }}
|
| 112 |
+
.optimax-category-summary {{ background: #f9fafb !important; padding: 15px 20px; border-radius: 8px; margin: 15px 0 25px 0; border-left: 4px solid #667eea; }}
|
| 113 |
+
.optimax-category-summary-text {{ color: #000000 !important; font-size: 0.95em; line-height: 1.6; }}
|
| 114 |
+
.optimax-download-btn {{ display: inline-block; padding: 12px 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 700; font-size: 1em; cursor: pointer; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s ease; margin: 10px 0; }}
|
| 115 |
+
.optimax-download-btn:hover {{ transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.15); }}
|
| 116 |
+
.optimax-download-btn:active {{ transform: translateY(0); }}
|
| 117 |
+
</style>
|
| 118 |
+
|
| 119 |
+
<script>
|
| 120 |
+
function downloadReport() {{
|
| 121 |
+
try {{
|
| 122 |
+
// Wait a moment for Gradio to fully render
|
| 123 |
+
setTimeout(() => {{
|
| 124 |
+
// Find the report container
|
| 125 |
+
let reportContainer = document.querySelector('.optimax-report');
|
| 126 |
+
|
| 127 |
+
// If not found in current context, try looking in parent frames
|
| 128 |
+
if (!reportContainer && window.parent) {{
|
| 129 |
+
reportContainer = window.parent.document.querySelector('.optimax-report');
|
| 130 |
+
}}
|
| 131 |
+
|
| 132 |
+
if (!reportContainer) {{
|
| 133 |
+
// Last resort: get the entire HTML content
|
| 134 |
+
const htmlContent = document.documentElement.outerHTML;
|
| 135 |
+
const blob = new Blob([htmlContent], {{ type: 'text/html' }});
|
| 136 |
+
const url = URL.createObjectURL(blob);
|
| 137 |
+
const link = document.createElement('a');
|
| 138 |
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
| 139 |
+
link.href = url;
|
| 140 |
+
link.download = `Optimax_Report_${{timestamp}}.html`;
|
| 141 |
+
link.click();
|
| 142 |
+
URL.revokeObjectURL(url);
|
| 143 |
+
alert('✅ Report downloaded!');
|
| 144 |
+
return;
|
| 145 |
+
}}
|
| 146 |
+
|
| 147 |
+
// Clone and prepare report
|
| 148 |
+
const clone = reportContainer.cloneNode(true);
|
| 149 |
+
const downloadBtn = clone.querySelector('.optimax-download-btn');
|
| 150 |
+
if (downloadBtn) downloadBtn.remove();
|
| 151 |
+
|
| 152 |
+
// Get styles
|
| 153 |
+
const styles = Array.from(document.querySelectorAll('style'))
|
| 154 |
+
.map(s => s.innerHTML).join('\\n');
|
| 155 |
+
|
| 156 |
+
// Create complete HTML
|
| 157 |
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
| 158 |
+
const fullHTML = `<!DOCTYPE html>
|
| 159 |
+
<html>
|
| 160 |
+
<head>
|
| 161 |
+
<meta charset="UTF-8">
|
| 162 |
+
<title>Optimax Security Report</title>
|
| 163 |
+
<style>${{styles}}</style>
|
| 164 |
+
</head>
|
| 165 |
+
<body style="margin:0;padding:20px;background:#f9fafb">
|
| 166 |
+
${{clone.outerHTML}}
|
| 167 |
+
</body>
|
| 168 |
+
</html>`;
|
| 169 |
+
|
| 170 |
+
// Download
|
| 171 |
+
const blob = new Blob([fullHTML], {{ type: 'text/html' }});
|
| 172 |
+
const url = URL.createObjectURL(blob);
|
| 173 |
+
const link = document.createElement('a');
|
| 174 |
+
link.href = url;
|
| 175 |
+
link.download = `Optimax_Security_Report_${{timestamp}}.html`;
|
| 176 |
+
document.body.appendChild(link);
|
| 177 |
+
link.click();
|
| 178 |
+
document.body.removeChild(link);
|
| 179 |
+
URL.revokeObjectURL(url);
|
| 180 |
+
|
| 181 |
+
// Visual feedback
|
| 182 |
+
const btn = document.querySelector('.optimax-download-btn');
|
| 183 |
+
if (btn) {{
|
| 184 |
+
const orig = btn.innerHTML;
|
| 185 |
+
btn.innerHTML = '✅ Downloaded!';
|
| 186 |
+
btn.style.background = '#10b981';
|
| 187 |
+
setTimeout(() => {{
|
| 188 |
+
btn.innerHTML = orig;
|
| 189 |
+
btn.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
| 190 |
+
}}, 2000);
|
| 191 |
+
}}
|
| 192 |
+
}}, 100);
|
| 193 |
+
}} catch (error) {{
|
| 194 |
+
console.error('Download error:', error);
|
| 195 |
+
alert('Download failed: ' + error.message + '\\n\\nTry right-clicking and Save As instead.');
|
| 196 |
+
}}
|
| 197 |
+
}}
|
| 198 |
+
</script>
|
| 199 |
+
|
| 200 |
+
<div class="optimax-report">
|
| 201 |
+
<div class="optimax-header">
|
| 202 |
+
<h1>🛡️ Optimax Security Report</h1>
|
| 203 |
+
<div class="subtitle">Comprehensive Security Analysis</div>
|
| 204 |
+
<button class="optimax-download-btn" onclick="downloadReport()" type="button">
|
| 205 |
+
📥 Download Report
|
| 206 |
+
</button>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<div class="optimax-content">
|
| 210 |
+
<!-- Organization Info -->
|
| 211 |
+
<div class="optimax-section">
|
| 212 |
+
<div class="optimax-stats-grid">
|
| 213 |
+
<div class="optimax-stat-item">
|
| 214 |
+
<div class="optimax-stat-label">Organization</div>
|
| 215 |
+
<div class="optimax-stat-value" style="font-size: 1.2em;">{org_name}</div>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="optimax-stat-item">
|
| 218 |
+
<div class="optimax-stat-label">Org ID</div>
|
| 219 |
+
<div class="optimax-stat-value" style="font-size: 0.9em;">{org_id[:15]}...</div>
|
| 220 |
+
</div>
|
| 221 |
+
<div class="optimax-stat-item">
|
| 222 |
+
<div class="optimax-stat-label">Scan Date</div>
|
| 223 |
+
<div class="optimax-stat-value" style="font-size: 0.9em;">{timestamp[:10] if timestamp else 'N/A'}</div>
|
| 224 |
+
</div>
|
| 225 |
+
<div class="optimax-stat-item">
|
| 226 |
+
<div class="optimax-stat-label">Scan Time</div>
|
| 227 |
+
<div class="optimax-stat-value" style="font-size: 0.9em;">{timestamp[11:19] if timestamp else 'N/A'}</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<!-- Risk Assessment -->
|
| 233 |
+
<div class="optimax-section">
|
| 234 |
+
<h2 class="optimax-section-title">Overall Risk Assessment</h2>
|
| 235 |
+
<div class="optimax-risk-score-box">
|
| 236 |
+
<div style="color: #4b5563 !important; font-weight: 700; text-transform: uppercase; font-size: 0.9em;">Security Risk Score</div>
|
| 237 |
+
<div class="optimax-risk-score">{risk_score}/100</div>
|
| 238 |
+
<div class="optimax-risk-level">{risk_level} RISK</div>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div class="optimax-risk-cards">
|
| 242 |
+
<div class="optimax-risk-card critical">
|
| 243 |
+
<div class="optimax-risk-card-title">Critical</div>
|
| 244 |
+
<div class="optimax-risk-card-value">{len(analysis_result.get('critical_findings', []))}</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="optimax-risk-card high">
|
| 247 |
+
<div class="optimax-risk-card-title">High</div>
|
| 248 |
+
<div class="optimax-risk-card-value">{len(analysis_result.get('high_findings', []))}</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="optimax-risk-card medium">
|
| 251 |
+
<div class="optimax-risk-card-title">Medium</div>
|
| 252 |
+
<div class="optimax-risk-card-value">{len(analysis_result.get('medium_findings', []))}</div>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="optimax-risk-card low">
|
| 255 |
+
<div class="optimax-risk-card-title">Low</div>
|
| 256 |
+
<div class="optimax-risk-card-value">{len(analysis_result.get('low_findings', []))}</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<!-- Executive Summary -->
|
| 262 |
+
{_build_summary_section(ai_summary, ai_recommendations)}
|
| 263 |
+
|
| 264 |
+
<!-- Scan Statistics -->
|
| 265 |
+
<div class="optimax-section">
|
| 266 |
+
<h2 class="optimax-section-title">Scan Statistics</h2>
|
| 267 |
+
<div class="optimax-stats-grid">
|
| 268 |
+
{_build_stats(statistics)}
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<!-- CATEGORY-BASED FINDINGS -->
|
| 273 |
+
<div class="optimax-section">
|
| 274 |
+
<h2 class="optimax-section-title">Security Findings by Category</h2>
|
| 275 |
+
|
| 276 |
+
{_build_category_section('Profiles & Permissions', categorized_findings['profiles_permissions'], '👤',
|
| 277 |
+
'User profiles, permission sets, system permissions, and access controls')}
|
| 278 |
+
|
| 279 |
+
{_build_category_section('Sharing Model', categorized_findings['sharing_model'], '🔓',
|
| 280 |
+
'Organization-wide defaults, sharing rules, role hierarchy, and record-level access')}
|
| 281 |
+
|
| 282 |
+
{_build_category_section('API & Integration', categorized_findings['api_integration'], '🔌',
|
| 283 |
+
'API usage, connected apps, OAuth tokens, and external integrations')}
|
| 284 |
+
|
| 285 |
+
{_build_category_section('Guest/Public Exposure', categorized_findings['guest_public'], '🌐',
|
| 286 |
+
'Guest user access, public pages, unauthenticated access, and site security')}
|
| 287 |
+
|
| 288 |
+
{_build_category_section('Custom Code', categorized_findings['custom_code'], '💻',
|
| 289 |
+
'Apex classes, triggers, SOQL queries, and code security vulnerabilities')}
|
| 290 |
+
|
| 291 |
+
{_build_category_section('Identity & Sessions', categorized_findings['identity_sessions'], '🔐',
|
| 292 |
+
'Login activity, MFA compliance, session security, and user authentication')}
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
"""
|
| 298 |
+
|
| 299 |
+
return html
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def _categorize_findings(all_findings: list, ai_detailed: dict) -> dict:
|
| 303 |
+
"""Categorize findings into 6 security categories"""
|
| 304 |
+
|
| 305 |
+
categories = {
|
| 306 |
+
'profiles_permissions': [],
|
| 307 |
+
'sharing_model': [],
|
| 308 |
+
'api_integration': [],
|
| 309 |
+
'guest_public': [],
|
| 310 |
+
'custom_code': [],
|
| 311 |
+
'identity_sessions': []
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
# Categorize rule-based findings
|
| 315 |
+
for finding in all_findings:
|
| 316 |
+
finding_type = finding.get('type', '').lower()
|
| 317 |
+
|
| 318 |
+
# Profiles & Permissions
|
| 319 |
+
if any(keyword in finding_type for keyword in ['permission', 'profile', 'admin', 'privilege', 'escalation']):
|
| 320 |
+
categories['profiles_permissions'].append(finding)
|
| 321 |
+
|
| 322 |
+
# Sharing Model
|
| 323 |
+
elif any(keyword in finding_type for keyword in ['sharing', 'owd', 'role', 'hierarchy']):
|
| 324 |
+
categories['sharing_model'].append(finding)
|
| 325 |
+
|
| 326 |
+
# API & Integration
|
| 327 |
+
elif any(keyword in finding_type for keyword in ['api', 'integration', 'oauth', 'connected']):
|
| 328 |
+
categories['api_integration'].append(finding)
|
| 329 |
+
|
| 330 |
+
# Guest/Public Exposure
|
| 331 |
+
elif any(keyword in finding_type for keyword in ['guest', 'public', 'unauthenticated', 'site']):
|
| 332 |
+
categories['guest_public'].append(finding)
|
| 333 |
+
|
| 334 |
+
# Custom Code
|
| 335 |
+
elif any(keyword in finding_type for keyword in ['code', 'apex', 'trigger', 'soql', 'class']):
|
| 336 |
+
categories['custom_code'].append(finding)
|
| 337 |
+
|
| 338 |
+
# Identity & Sessions
|
| 339 |
+
elif any(keyword in finding_type for keyword in ['login', 'mfa', 'dormant', 'session', 'password', 'authentication']):
|
| 340 |
+
categories['identity_sessions'].append(finding)
|
| 341 |
+
|
| 342 |
+
# Default to Profiles & Permissions if unclear
|
| 343 |
+
else:
|
| 344 |
+
categories['profiles_permissions'].append(finding)
|
| 345 |
+
|
| 346 |
+
# Add AI-detected findings if available
|
| 347 |
+
if ai_detailed and ai_detailed.get('available'):
|
| 348 |
+
|
| 349 |
+
# Code analysis findings -> Custom Code
|
| 350 |
+
for code_item in ai_detailed.get('code_analysis', []):
|
| 351 |
+
for vuln in code_item.get('vulnerabilities', []):
|
| 352 |
+
categories['custom_code'].append({
|
| 353 |
+
'type': vuln.get('type', 'Code Vulnerability'),
|
| 354 |
+
'severity': vuln.get('severity', 'Medium'),
|
| 355 |
+
'description': vuln.get('description', 'Code security issue detected'),
|
| 356 |
+
'class_name': code_item.get('class_name'),
|
| 357 |
+
'confidence': vuln.get('confidence', 0),
|
| 358 |
+
'recommendation': _get_code_recommendation(vuln.get('type', ''))
|
| 359 |
+
})
|
| 360 |
+
|
| 361 |
+
# Permission analysis -> Profiles & Permissions
|
| 362 |
+
for perm in ai_detailed.get('permission_analysis', []):
|
| 363 |
+
if perm.get('risk_level') in ['CRITICAL', 'HIGH']:
|
| 364 |
+
categories['profiles_permissions'].append({
|
| 365 |
+
'type': f"{perm.get('type', 'Permission Set')} - {perm.get('name', 'Unknown')}",
|
| 366 |
+
'severity': perm.get('risk_level', 'Medium'),
|
| 367 |
+
'description': f"Risk level: {perm.get('risk_level')}",
|
| 368 |
+
'dangerous_permissions': perm.get('dangerous_permissions', []),
|
| 369 |
+
'confidence': perm.get('confidence', 0),
|
| 370 |
+
'recommendation': 'Review and restrict dangerous permissions'
|
| 371 |
+
})
|
| 372 |
+
|
| 373 |
+
# Security analysis -> Identity & Sessions
|
| 374 |
+
for sec in ai_detailed.get('security_analysis', []):
|
| 375 |
+
categories['identity_sessions'].append({
|
| 376 |
+
'type': sec.get('type', 'Security Policy Issue'),
|
| 377 |
+
'severity': sec.get('severity', 'Medium'),
|
| 378 |
+
'description': sec.get('description', 'Security policy violation detected'),
|
| 379 |
+
'confidence': sec.get('confidence', 0),
|
| 380 |
+
'recommendation': sec.get('recommendation', 'Review and remediate')
|
| 381 |
+
})
|
| 382 |
+
|
| 383 |
+
# Anomaly detection -> Identity & Sessions
|
| 384 |
+
for anomaly in ai_detailed.get('anomaly_analysis', []):
|
| 385 |
+
categories['identity_sessions'].append({
|
| 386 |
+
'type': anomaly.get('type', 'Behavioral Anomaly'),
|
| 387 |
+
'severity': anomaly.get('severity', 'Medium'),
|
| 388 |
+
'description': anomaly.get('description', 'Unusual behavior detected'),
|
| 389 |
+
'impact': anomaly.get('impact', ''),
|
| 390 |
+
'recommendation': anomaly.get('recommendation', 'Investigate immediately'),
|
| 391 |
+
'anomaly_score': anomaly.get('anomaly_score'),
|
| 392 |
+
'username': anomaly.get('username'),
|
| 393 |
+
'login_time': anomaly.get('login_time'),
|
| 394 |
+
'source_ip': anomaly.get('source_ip')
|
| 395 |
+
})
|
| 396 |
+
|
| 397 |
+
return categories
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _get_code_recommendation(vuln_type: str) -> str:
|
| 401 |
+
"""Get recommendation for code vulnerability"""
|
| 402 |
+
recommendations = {
|
| 403 |
+
'SOQL_INJECTION': 'Use bind variables instead of string concatenation',
|
| 404 |
+
'POTENTIAL_SOQL_INJECTION': 'Replace dynamic query with parameterized query',
|
| 405 |
+
'MISSING_SHARING': 'Add with sharing keyword to class declaration',
|
| 406 |
+
'POTENTIAL_HARDCODED_CREDENTIALS': 'Move credentials to Custom Metadata or Named Credentials',
|
| 407 |
+
'UNSAFE_DML': 'Wrap DML operations in try-catch with proper error handling'
|
| 408 |
+
}
|
| 409 |
+
return recommendations.get(vuln_type, 'Review and remediate this security issue')
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def _build_summary_section(ai_summary: str, ai_recommendations: list) -> str:
|
| 413 |
+
"""Build executive summary section"""
|
| 414 |
+
if not ai_summary and not ai_recommendations:
|
| 415 |
+
return ''
|
| 416 |
+
|
| 417 |
+
html = '<div class="optimax-section">'
|
| 418 |
+
|
| 419 |
+
if ai_summary:
|
| 420 |
+
# Remove AI model mentions from summary
|
| 421 |
+
clean_summary = ai_summary
|
| 422 |
+
for model in ['CodeBERT', 'RoBERTa', 'SecBERT', 'Isolation Forest', 'AI-powered', 'AI analysis', 'AI models']:
|
| 423 |
+
clean_summary = clean_summary.replace(model, 'Security analysis')
|
| 424 |
+
|
| 425 |
+
html += f'''
|
| 426 |
+
<div class="optimax-summary-box">
|
| 427 |
+
<div class="optimax-summary-title" style="color: #0c4a6e !important;">📋 Executive Summary</div>
|
| 428 |
+
<p style="color: #000000 !important;">{clean_summary}</p>
|
| 429 |
+
</div>
|
| 430 |
+
'''
|
| 431 |
+
|
| 432 |
+
if ai_recommendations:
|
| 433 |
+
# Clean recommendations from AI model mentions
|
| 434 |
+
clean_recs = []
|
| 435 |
+
for rec in ai_recommendations:
|
| 436 |
+
clean_rec = rec
|
| 437 |
+
for model in ['CodeBERT', 'RoBERTa', 'SecBERT', 'Isolation Forest', 'AI-detected', 'AI analysis']:
|
| 438 |
+
clean_rec = clean_rec.replace(model, 'Analysis')
|
| 439 |
+
clean_recs.append(clean_rec)
|
| 440 |
+
|
| 441 |
+
html += '<div class="optimax-rec-box"><div class="optimax-rec-title" style="color: #065f46 !important;">💡 Key Recommendations</div><ul class="optimax-rec-list">'
|
| 442 |
+
for rec in clean_recs:
|
| 443 |
+
html += f'<li style="color: #000000 !important;">{rec}</li>'
|
| 444 |
+
html += '</ul></div>'
|
| 445 |
+
|
| 446 |
+
html += '</div>'
|
| 447 |
+
return html
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def _build_stats(statistics: dict) -> str:
|
| 451 |
+
"""Build statistics section"""
|
| 452 |
+
stats_items = [
|
| 453 |
+
('Total Findings', statistics.get('total_findings', 0)),
|
| 454 |
+
('Users Analyzed', statistics.get('users_analyzed', 0)),
|
| 455 |
+
('Profiles Analyzed', statistics.get('profiles_analyzed', 0)),
|
| 456 |
+
('Permission Sets', statistics.get('permission_sets_analyzed', 0)),
|
| 457 |
+
('Login Records', statistics.get('login_records_analyzed', 0)),
|
| 458 |
+
]
|
| 459 |
+
|
| 460 |
+
html = ''
|
| 461 |
+
for label, value in stats_items:
|
| 462 |
+
display_value = f'{value:,}' if isinstance(value, int) else str(value)
|
| 463 |
+
html += f'''
|
| 464 |
+
<div class="optimax-stat-item">
|
| 465 |
+
<div class="optimax-stat-label" style="color: #4b5563 !important;">{label}</div>
|
| 466 |
+
<div class="optimax-stat-value" style="color: #000000 !important;">{display_value}</div>
|
| 467 |
+
</div>
|
| 468 |
+
'''
|
| 469 |
+
|
| 470 |
+
return html
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def _build_category_section(category_name: str, findings: list, icon: str, description: str) -> str:
|
| 474 |
+
"""Build a category section with all findings"""
|
| 475 |
+
|
| 476 |
+
if not findings:
|
| 477 |
+
return f'''
|
| 478 |
+
<div class="optimax-category-title">
|
| 479 |
+
<span class="optimax-category-icon">{icon}</span>
|
| 480 |
+
<span style="color: #000000 !important;">{category_name}</span>
|
| 481 |
+
</div>
|
| 482 |
+
<div class="optimax-category-summary">
|
| 483 |
+
<div class="optimax-category-summary-text" style="color: #000000 !important;">
|
| 484 |
+
<em style="color: #000000 !important;">{description}</em>
|
| 485 |
+
</div>
|
| 486 |
+
</div>
|
| 487 |
+
<div class="optimax-no-findings" style="color: #4b5563 !important;">✅ No issues detected in this category</div>
|
| 488 |
+
'''
|
| 489 |
+
|
| 490 |
+
# Count by severity
|
| 491 |
+
critical = [f for f in findings if f.get('severity', '').upper() in ['CRITICAL']]
|
| 492 |
+
high = [f for f in findings if f.get('severity', '').upper() in ['HIGH']]
|
| 493 |
+
medium = [f for f in findings if f.get('severity', '').upper() in ['MEDIUM']]
|
| 494 |
+
low = [f for f in findings if f.get('severity', '').upper() in ['LOW']]
|
| 495 |
+
|
| 496 |
+
html = f'''
|
| 497 |
+
<div class="optimax-category-title">
|
| 498 |
+
<span class="optimax-category-icon">{icon}</span>
|
| 499 |
+
<span style="color: #000000 !important;">{category_name}</span>
|
| 500 |
+
</div>
|
| 501 |
+
<div class="optimax-category-summary">
|
| 502 |
+
<div class="optimax-category-summary-text" style="color: #000000 !important;">
|
| 503 |
+
<em style="color: #000000 !important;">{description}</em>
|
| 504 |
+
</div>
|
| 505 |
+
<div class="optimax-metric-grid" style="margin-top: 15px;">
|
| 506 |
+
<div class="optimax-metric-item">
|
| 507 |
+
<div class="optimax-metric-label" style="color: #4b5563 !important;">Critical</div>
|
| 508 |
+
<div class="optimax-metric-value" style="color: #dc2626 !important;">{len(critical)}</div>
|
| 509 |
+
</div>
|
| 510 |
+
<div class="optimax-metric-item">
|
| 511 |
+
<div class="optimax-metric-label" style="color: #4b5563 !important;">High</div>
|
| 512 |
+
<div class="optimax-metric-value" style="color: #ea580c !important;">{len(high)}</div>
|
| 513 |
+
</div>
|
| 514 |
+
<div class="optimax-metric-item">
|
| 515 |
+
<div class="optimax-metric-label" style="color: #4b5563 !important;">Medium</div>
|
| 516 |
+
<div class="optimax-metric-value" style="color: #f59e0b !important;">{len(medium)}</div>
|
| 517 |
+
</div>
|
| 518 |
+
<div class="optimax-metric-item">
|
| 519 |
+
<div class="optimax-metric-label" style="color: #4b5563 !important;">Low</div>
|
| 520 |
+
<div class="optimax-metric-value" style="color: #10b981 !important;">{len(low)}</div>
|
| 521 |
+
</div>
|
| 522 |
+
<div class="optimax-metric-item">
|
| 523 |
+
<div class="optimax-metric-label" style="color: #4b5563 !important;">Total</div>
|
| 524 |
+
<div class="optimax-metric-value" style="color: #000000 !important;">{len(findings)}</div>
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
</div>
|
| 528 |
+
'''
|
| 529 |
+
|
| 530 |
+
# Display findings
|
| 531 |
+
for finding in findings:
|
| 532 |
+
finding_type = finding.get('type', 'Unknown Issue')
|
| 533 |
+
severity = finding.get('severity', 'Medium').upper()
|
| 534 |
+
description = finding.get('description', 'No description available')
|
| 535 |
+
recommendation = finding.get('recommendation', 'Review and remediate')
|
| 536 |
+
impact = finding.get('impact', '')
|
| 537 |
+
|
| 538 |
+
# Build additional details
|
| 539 |
+
details_html = ''
|
| 540 |
+
|
| 541 |
+
if finding.get('class_name'):
|
| 542 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">Apex Class:</strong> <span style="color: #000000 !important;">{finding["class_name"]}</span></div>'
|
| 543 |
+
|
| 544 |
+
if finding.get('dangerous_permissions'):
|
| 545 |
+
perms = finding['dangerous_permissions']
|
| 546 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">Dangerous Permissions:</strong> <span style="color: #000000 !important;">{", ".join(perms)}</span></div>'
|
| 547 |
+
|
| 548 |
+
if finding.get('confidence'):
|
| 549 |
+
confidence_pct = int(finding['confidence'] * 100)
|
| 550 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">Detection Confidence:</strong> <span style="color: #000000 !important;">{confidence_pct}%</span></div>'
|
| 551 |
+
|
| 552 |
+
if finding.get('username'):
|
| 553 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">User:</strong> <span style="color: #000000 !important;">{finding["username"]}</span></div>'
|
| 554 |
+
|
| 555 |
+
if finding.get('login_time'):
|
| 556 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">Time:</strong> <span style="color: #000000 !important;">{finding["login_time"]}</span></div>'
|
| 557 |
+
|
| 558 |
+
if finding.get('source_ip'):
|
| 559 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">IP Address:</strong> <span style="color: #000000 !important;">{finding["source_ip"]}</span></div>'
|
| 560 |
+
|
| 561 |
+
if finding.get('days_inactive'):
|
| 562 |
+
details_html += f'<div style="color: #000000 !important;"><strong style="color: #000000 !important;">Inactive Days:</strong> <span style="color: #000000 !important;">{finding["days_inactive"]}</span></div>'
|
| 563 |
+
|
| 564 |
+
# Affected users with detailed information
|
| 565 |
+
affected_html = ''
|
| 566 |
+
|
| 567 |
+
# Check for detailed user information first (priority)
|
| 568 |
+
if finding.get('user_details'):
|
| 569 |
+
user_details = finding['user_details']
|
| 570 |
+
user_count = len(user_details)
|
| 571 |
+
|
| 572 |
+
# Build table for detailed user info
|
| 573 |
+
affected_html = f'''
|
| 574 |
+
<div class="optimax-affected-users">
|
| 575 |
+
<div class="optimax-affected-users-title" style="color: #991b1b !important;">👥 Affected Users ({user_count})</div>
|
| 576 |
+
<table class="optimax-user-table" style="width: 100%; margin-top: 10px; border-collapse: collapse;">
|
| 577 |
+
<thead>
|
| 578 |
+
<tr style="background: #fee2e2; border-bottom: 2px solid #991b1b;">
|
| 579 |
+
<th style="padding: 8px; text-align: left; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Name</th>
|
| 580 |
+
<th style="padding: 8px; text-align: left; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Email</th>
|
| 581 |
+
<th style="padding: 8px; text-align: left; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Username</th>
|
| 582 |
+
<th style="padding: 8px; text-align: center; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Status</th>
|
| 583 |
+
</tr>
|
| 584 |
+
</thead>
|
| 585 |
+
<tbody>
|
| 586 |
+
'''
|
| 587 |
+
|
| 588 |
+
# Show first 10 users to avoid redundancy
|
| 589 |
+
for i, user in enumerate(user_details[:10]):
|
| 590 |
+
row_bg = '#fef2f2' if i % 2 == 0 else '#ffffff'
|
| 591 |
+
status_color = '#10b981' if user.get('status') == 'Active' else '#dc2626'
|
| 592 |
+
|
| 593 |
+
affected_html += f'''
|
| 594 |
+
<tr style="background: {row_bg}; border-bottom: 1px solid #fca5a5;">
|
| 595 |
+
<td style="padding: 8px; color: #991b1b !important; font-size: 0.85em;">{user.get('name', 'Unknown')}</td>
|
| 596 |
+
<td style="padding: 8px; color: #991b1b !important; font-size: 0.85em;">{user.get('email', 'N/A')}</td>
|
| 597 |
+
<td style="padding: 8px; color: #991b1b !important; font-size: 0.85em; font-family: monospace;">{user.get('username', 'Unknown')}</td>
|
| 598 |
+
<td style="padding: 8px; text-align: center;">
|
| 599 |
+
<span style="display: inline-block; padding: 3px 8px; background: {row_bg}; color: {status_color} !important; font-weight: 700; font-size: 0.75em; border: 1px solid {status_color}; border-radius: 4px;">
|
| 600 |
+
{user.get('status', 'Unknown')}
|
| 601 |
+
</span>
|
| 602 |
+
</td>
|
| 603 |
+
</tr>
|
| 604 |
+
'''
|
| 605 |
+
|
| 606 |
+
affected_html += '</tbody></table>'
|
| 607 |
+
|
| 608 |
+
if user_count > 10:
|
| 609 |
+
affected_html += f'''
|
| 610 |
+
<div style="margin-top: 8px; color: #991b1b !important; font-size: 0.85em; font-style: italic;">
|
| 611 |
+
+ {user_count - 10} more users not shown
|
| 612 |
+
</div>
|
| 613 |
+
'''
|
| 614 |
+
|
| 615 |
+
affected_html += '</div>'
|
| 616 |
+
|
| 617 |
+
# Fallback to simple username list if no detailed info
|
| 618 |
+
elif finding.get('affected_users'):
|
| 619 |
+
affected_users = finding['affected_users']
|
| 620 |
+
if len(affected_users) > 0:
|
| 621 |
+
user_count = len(affected_users)
|
| 622 |
+
# Remove duplicates while preserving order
|
| 623 |
+
unique_users = list(dict.fromkeys(affected_users))
|
| 624 |
+
users_display = ', '.join(unique_users[:5])
|
| 625 |
+
if len(unique_users) > 5:
|
| 626 |
+
users_display += f' (+{len(unique_users) - 5} more)'
|
| 627 |
+
|
| 628 |
+
affected_html = f'''
|
| 629 |
+
<div class="optimax-affected-users">
|
| 630 |
+
<div class="optimax-affected-users-title" style="color: #991b1b !important;">👥 Affected Users ({len(unique_users)})</div>
|
| 631 |
+
<div class="optimax-affected-users-list" style="color: #991b1b !important;">{users_display}</div>
|
| 632 |
+
</div>
|
| 633 |
+
'''
|
| 634 |
+
|
| 635 |
+
# Single user details (for individual findings)
|
| 636 |
+
elif finding.get('username'):
|
| 637 |
+
affected_html = f'''
|
| 638 |
+
<div class="optimax-affected-users" style="background: #fef2f2; padding: 12px 15px; border-radius: 6px; margin: 10px 0; border-left: 3px solid #dc2626;">
|
| 639 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 640 |
+
<tr>
|
| 641 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-weight: 700; font-size: 0.85em; width: 100px;">Name:</td>
|
| 642 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-size: 0.85em;">{finding.get('name', 'Unknown')}</td>
|
| 643 |
+
</tr>
|
| 644 |
+
<tr>
|
| 645 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Email:</td>
|
| 646 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-size: 0.85em;">{finding.get('email', 'N/A')}</td>
|
| 647 |
+
</tr>
|
| 648 |
+
<tr>
|
| 649 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Username:</td>
|
| 650 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-size: 0.85em; font-family: monospace;">{finding.get('username', 'Unknown')}</td>
|
| 651 |
+
</tr>
|
| 652 |
+
<tr>
|
| 653 |
+
<td style="padding: 4px 8px; color: #991b1b !important; font-weight: 700; font-size: 0.85em;">Status:</td>
|
| 654 |
+
<td style="padding: 4px 8px;">
|
| 655 |
+
<span style="display: inline-block; padding: 2px 8px; background: #fee2e2; color: {'#10b981' if finding.get('status') == 'Active' else '#dc2626'} !important; font-weight: 700; font-size: 0.75em; border: 1px solid {'#10b981' if finding.get('status') == 'Active' else '#dc2626'}; border-radius: 4px;">
|
| 656 |
+
{finding.get('status', 'Unknown')}
|
| 657 |
+
</span>
|
| 658 |
+
</td>
|
| 659 |
+
</tr>
|
| 660 |
+
</table>
|
| 661 |
+
</div>
|
| 662 |
+
'''
|
| 663 |
+
|
| 664 |
+
html += f'''
|
| 665 |
+
<div class="optimax-finding-card">
|
| 666 |
+
<div class="optimax-finding-header">
|
| 667 |
+
<div class="optimax-finding-title">{finding_type}</div>
|
| 668 |
+
<span class="optimax-severity-badge optimax-severity-{severity.lower()}">{severity}</span>
|
| 669 |
+
</div>
|
| 670 |
+
<div class="optimax-finding-body">
|
| 671 |
+
<div style="margin-bottom: 8px; color: #000000 !important;"><strong style="color: #000000 !important;">Description:</strong> <span style="color: #000000 !important;">{description}</span></div>
|
| 672 |
+
{f'<div style="margin-bottom: 8px; color: #000000 !important;"><strong style="color: #000000 !important;">Impact:</strong> <span style="color: #000000 !important;">{impact}</span></div>' if impact else ''}
|
| 673 |
+
{details_html}
|
| 674 |
+
</div>
|
| 675 |
+
{affected_html}
|
| 676 |
+
<div class="optimax-recommendation">
|
| 677 |
+
<div class="optimax-recommendation-title" style="color: #166534 !important;">💡 Recommendation</div>
|
| 678 |
+
<div class="optimax-recommendation-text" style="color: #166534 !important;">{recommendation}</div>
|
| 679 |
+
</div>
|
| 680 |
+
</div>
|
| 681 |
+
'''
|
| 682 |
+
|
| 683 |
+
return html
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
def _generate_error_html(error_msg: str) -> str:
|
| 687 |
+
"""Generate HTML for error display"""
|
| 688 |
+
return f"""
|
| 689 |
+
<div style="padding: 30px; background: #fee2e2; border-radius: 10px; border-left: 5px solid #dc2626;">
|
| 690 |
+
<h2 style="color: #991b1b; margin-bottom: 10px; font-size: 1.5em;">❌ Error</h2>
|
| 691 |
+
<p style="color: #991b1b; font-size: 1.1em;">{error_msg}</p>
|
| 692 |
+
</div>
|
| 693 |
+
"""
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
# Save report function
|
| 697 |
+
def save_report(analysis_result: dict, filename: str = None) -> str:
|
| 698 |
+
"""Save complete HTML report to file"""
|
| 699 |
+
from datetime import datetime
|
| 700 |
+
|
| 701 |
+
html_content = generate_report(analysis_result)
|
| 702 |
+
|
| 703 |
+
full_html = f"""<!DOCTYPE html>
|
| 704 |
+
<html lang="en">
|
| 705 |
+
<head>
|
| 706 |
+
<meta charset="UTF-8">
|
| 707 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 708 |
+
<title>Optimax Security Report - Category-Based Analysis</title>
|
| 709 |
+
</head>
|
| 710 |
+
<body style="margin: 0; padding: 20px; background: #f9fafb !important;">
|
| 711 |
+
{html_content}
|
| 712 |
+
</body>
|
| 713 |
+
</html>"""
|
| 714 |
+
|
| 715 |
+
if not filename:
|
| 716 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 717 |
+
org_name = analysis_result.get('org_name', 'Unknown').replace(' ', '_')
|
| 718 |
+
filename = f"optimax_report_{org_name}_{timestamp}.html"
|
| 719 |
+
|
| 720 |
+
with open(filename, 'w', encoding='utf-8') as f:
|
| 721 |
+
f.write(full_html)
|
| 722 |
+
|
| 723 |
+
return filename
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
if __name__ == '__main__':
|
| 727 |
+
print("✅ Category-Based Optimax Report Generator loaded!")
|
| 728 |
+
print(" 📁 Categories:")
|
| 729 |
+
print(" 1. 👤 Profiles & Permissions")
|
| 730 |
+
print(" 2. 🔓 Sharing Model")
|
| 731 |
+
print(" 3. 🔌 API & Integration")
|
| 732 |
+
print(" 4. 🌐 Guest/Public Exposure")
|
| 733 |
+
print(" 5. 💻 Custom Code")
|
| 734 |
+
print(" 6. 🔐 Identity & Sessions")
|
permission_analyzer.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional
|
| 2 |
+
|
| 3 |
+
class PermissionAnalyzer:
|
| 4 |
+
"""
|
| 5 |
+
Analyzes Salesforce permissions for security vulnerabilities
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# Dangerous permissions that grant broad access
|
| 9 |
+
DANGEROUS_PERMISSIONS = {
|
| 10 |
+
'PermissionsModifyAllData': {
|
| 11 |
+
'severity': 'Critical',
|
| 12 |
+
'description': 'Modify All Data - Full read/write access to all records',
|
| 13 |
+
'impact': 'User can view, edit, and delete ALL records in the org'
|
| 14 |
+
},
|
| 15 |
+
'PermissionsViewAllData': {
|
| 16 |
+
'severity': 'Critical',
|
| 17 |
+
'description': 'View All Data - Read access to all records',
|
| 18 |
+
'impact': 'User can view ALL records, bypassing sharing rules'
|
| 19 |
+
},
|
| 20 |
+
'PermissionsManageUsers': {
|
| 21 |
+
'severity': 'Critical',
|
| 22 |
+
'description': 'Manage Users - Can create/modify users',
|
| 23 |
+
'impact': 'Can create admin users, escalate privileges'
|
| 24 |
+
},
|
| 25 |
+
'PermissionsCustomizeApplication': {
|
| 26 |
+
'severity': 'High',
|
| 27 |
+
'description': 'Customize Application - Modify org configuration',
|
| 28 |
+
'impact': 'Can change org settings, create fields, objects'
|
| 29 |
+
},
|
| 30 |
+
'PermissionsAuthorApex': {
|
| 31 |
+
'severity': 'High',
|
| 32 |
+
'description': 'Author Apex - Write and deploy code',
|
| 33 |
+
'impact': 'Can deploy malicious code, bypass security'
|
| 34 |
+
},
|
| 35 |
+
'PermissionsExportReport': {
|
| 36 |
+
'severity': 'Medium',
|
| 37 |
+
'description': 'Export Reports - Export data to external files',
|
| 38 |
+
'impact': 'Can export sensitive data in bulk'
|
| 39 |
+
},
|
| 40 |
+
'PermissionsManageRoles': {
|
| 41 |
+
'severity': 'High',
|
| 42 |
+
'description': 'Manage Roles - Modify role hierarchy',
|
| 43 |
+
'impact': 'Can manipulate data access through role changes'
|
| 44 |
+
},
|
| 45 |
+
'PermissionsModifyMetadata': {
|
| 46 |
+
'severity': 'High',
|
| 47 |
+
'description': 'Modify Metadata - Change org configuration',
|
| 48 |
+
'impact': 'Can alter security settings and configurations'
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def analyze_all_permissions(
|
| 53 |
+
self,
|
| 54 |
+
permission_sets: List[Dict],
|
| 55 |
+
profiles: List[Dict],
|
| 56 |
+
users: List[Dict] = None # ADDED: Pass users to get details
|
| 57 |
+
) -> Dict:
|
| 58 |
+
"""
|
| 59 |
+
Analyze all permission sets and profiles
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Analysis results with findings
|
| 63 |
+
"""
|
| 64 |
+
findings = []
|
| 65 |
+
|
| 66 |
+
# Create user lookup dictionary for faster access
|
| 67 |
+
user_lookup = {}
|
| 68 |
+
if users:
|
| 69 |
+
user_lookup = {u.get('Id'): u for u in users}
|
| 70 |
+
|
| 71 |
+
# Analyze permission sets
|
| 72 |
+
for ps in permission_sets:
|
| 73 |
+
ps_findings = self.analyze_permission_set(ps, user_lookup)
|
| 74 |
+
findings.extend(ps_findings)
|
| 75 |
+
|
| 76 |
+
# Analyze profiles
|
| 77 |
+
for profile in profiles:
|
| 78 |
+
profile_findings = self._analyze_profile_permissions(profile)
|
| 79 |
+
findings.extend(profile_findings)
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
'findings': findings,
|
| 83 |
+
'permission_sets_analyzed': len(permission_sets),
|
| 84 |
+
'profiles_analyzed': len(profiles),
|
| 85 |
+
'dangerous_assignments': self._count_dangerous_assignments(findings)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
def analyze_permission_set(self, permission_set: Dict, user_lookup: Dict = None) -> List[Dict]:
|
| 89 |
+
"""
|
| 90 |
+
Analyze a single permission set for security issues
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
permission_set: Permission set data from Salesforce
|
| 94 |
+
user_lookup: Dictionary mapping user IDs to user records
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
List of security findings
|
| 98 |
+
"""
|
| 99 |
+
findings = []
|
| 100 |
+
ps_name = permission_set.get('Name', 'Unknown')
|
| 101 |
+
ps_id = permission_set.get('Id', 'Unknown')
|
| 102 |
+
assignments = permission_set.get('Assignments', [])
|
| 103 |
+
|
| 104 |
+
# Build detailed user list with name, email, status
|
| 105 |
+
user_details = []
|
| 106 |
+
affected_usernames = []
|
| 107 |
+
|
| 108 |
+
if user_lookup:
|
| 109 |
+
for assignment in assignments:
|
| 110 |
+
assignee = assignment.get('Assignee', {})
|
| 111 |
+
user_id = assignee.get('Id') or assignment.get('AssigneeId')
|
| 112 |
+
|
| 113 |
+
if user_id and user_id in user_lookup:
|
| 114 |
+
user = user_lookup[user_id]
|
| 115 |
+
user_details.append({
|
| 116 |
+
'username': user.get('Username'),
|
| 117 |
+
'name': user.get('Name', 'Unknown'),
|
| 118 |
+
'email': user.get('Email', 'N/A'),
|
| 119 |
+
'status': 'Active' if user.get('IsActive') else 'Inactive'
|
| 120 |
+
})
|
| 121 |
+
affected_usernames.append(user.get('Username', 'Unknown'))
|
| 122 |
+
else:
|
| 123 |
+
# Fallback to assignee data if available
|
| 124 |
+
user_details.append({
|
| 125 |
+
'username': assignee.get('Username', 'Unknown'),
|
| 126 |
+
'name': assignee.get('Name', 'Unknown'),
|
| 127 |
+
'email': assignee.get('Email', 'N/A'),
|
| 128 |
+
'status': 'Unknown'
|
| 129 |
+
})
|
| 130 |
+
affected_usernames.append(assignee.get('Username', 'Unknown'))
|
| 131 |
+
else:
|
| 132 |
+
# Fallback when user_lookup is not available
|
| 133 |
+
for assignment in assignments:
|
| 134 |
+
assignee = assignment.get('Assignee', {})
|
| 135 |
+
user_details.append({
|
| 136 |
+
'username': assignee.get('Username', 'Unknown'),
|
| 137 |
+
'name': assignee.get('Name', 'Unknown'),
|
| 138 |
+
'email': assignee.get('Email', 'N/A'),
|
| 139 |
+
'status': 'Unknown'
|
| 140 |
+
})
|
| 141 |
+
affected_usernames.append(assignee.get('Username', 'Unknown'))
|
| 142 |
+
|
| 143 |
+
# Check for dangerous permissions
|
| 144 |
+
dangerous_perms = []
|
| 145 |
+
for perm_field, perm_info in self.DANGEROUS_PERMISSIONS.items():
|
| 146 |
+
if permission_set.get(perm_field) == True:
|
| 147 |
+
dangerous_perms.append(perm_info)
|
| 148 |
+
|
| 149 |
+
# Create finding for each dangerous permission
|
| 150 |
+
findings.append({
|
| 151 |
+
'type': 'Dangerous Permission in Permission Set',
|
| 152 |
+
'severity': perm_info['severity'],
|
| 153 |
+
'permission_set': ps_name,
|
| 154 |
+
'permission_set_id': ps_id,
|
| 155 |
+
'permission': perm_info['description'],
|
| 156 |
+
'impact': perm_info['impact'],
|
| 157 |
+
'assigned_users': len(assignments),
|
| 158 |
+
'description': f"Permission Set '{ps_name}' grants {perm_info['description']} to {len(assignments)} user(s)",
|
| 159 |
+
'recommendation': f"Review necessity of this permission. Consider removing or restricting to fewer users.",
|
| 160 |
+
'affected_users': affected_usernames, # List of usernames
|
| 161 |
+
'user_details': user_details # ADDED: Detailed user info with name, email, status
|
| 162 |
+
})
|
| 163 |
+
|
| 164 |
+
# Check for permission escalation risk
|
| 165 |
+
if len(dangerous_perms) >= 2:
|
| 166 |
+
findings.append({
|
| 167 |
+
'type': 'Permission Escalation Risk',
|
| 168 |
+
'severity': 'Critical',
|
| 169 |
+
'permission_set': ps_name,
|
| 170 |
+
'description': f"Permission Set '{ps_name}' combines multiple dangerous permissions",
|
| 171 |
+
'impact': 'High risk of privilege escalation and data breach',
|
| 172 |
+
'recommendation': 'Split into separate permission sets with single responsibilities',
|
| 173 |
+
'dangerous_permissions': [p['description'] for p in dangerous_perms],
|
| 174 |
+
'affected_users': affected_usernames,
|
| 175 |
+
'user_details': user_details # ADDED
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
return findings
|
| 179 |
+
|
| 180 |
+
def analyze_profile(self, profile_data: Dict) -> Dict:
|
| 181 |
+
"""
|
| 182 |
+
Analyze a specific profile for security issues
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
profile_data: Profile information from Salesforce
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Profile analysis with risk assessment
|
| 189 |
+
"""
|
| 190 |
+
profile_name = profile_data.get('Name', 'Unknown')
|
| 191 |
+
assigned_users = profile_data.get('AssignedUsers', [])
|
| 192 |
+
|
| 193 |
+
analysis = {
|
| 194 |
+
'profile_name': profile_name,
|
| 195 |
+
'profile_id': profile_data.get('Id'),
|
| 196 |
+
'assigned_users_count': len(assigned_users),
|
| 197 |
+
'dangerous_permissions': [],
|
| 198 |
+
'critical_issues': [],
|
| 199 |
+
'warnings': [],
|
| 200 |
+
'recommendations': [],
|
| 201 |
+
'risk_score': 0
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
# Check for dangerous permissions
|
| 205 |
+
for perm_field, perm_info in self.DANGEROUS_PERMISSIONS.items():
|
| 206 |
+
if profile_data.get(perm_field) == True:
|
| 207 |
+
analysis['dangerous_permissions'].append(perm_info['description'])
|
| 208 |
+
|
| 209 |
+
if perm_info['severity'] == 'Critical':
|
| 210 |
+
analysis['critical_issues'].append({
|
| 211 |
+
'permission': perm_info['description'],
|
| 212 |
+
'impact': perm_info['impact'],
|
| 213 |
+
'users_affected': len(assigned_users)
|
| 214 |
+
})
|
| 215 |
+
elif perm_info['severity'] == 'High':
|
| 216 |
+
analysis['warnings'].append({
|
| 217 |
+
'permission': perm_info['description'],
|
| 218 |
+
'impact': perm_info['impact']
|
| 219 |
+
})
|
| 220 |
+
|
| 221 |
+
# Calculate risk score
|
| 222 |
+
analysis['risk_score'] = self._calculate_profile_risk_score(analysis)
|
| 223 |
+
|
| 224 |
+
# Generate recommendations
|
| 225 |
+
analysis['recommendations'] = self._generate_profile_recommendations(analysis, profile_data)
|
| 226 |
+
|
| 227 |
+
return analysis
|
| 228 |
+
|
| 229 |
+
def _analyze_profile_permissions(self, profile: Dict) -> List[Dict]:
|
| 230 |
+
"""
|
| 231 |
+
Internal method to analyze profile permissions
|
| 232 |
+
"""
|
| 233 |
+
findings = []
|
| 234 |
+
profile_name = profile.get('Name', 'Unknown')
|
| 235 |
+
|
| 236 |
+
# Check if it's a standard profile
|
| 237 |
+
is_standard = not profile.get('IsCustom', False)
|
| 238 |
+
|
| 239 |
+
for perm_field, perm_info in self.DANGEROUS_PERMISSIONS.items():
|
| 240 |
+
if profile.get(perm_field) == True:
|
| 241 |
+
severity = perm_info['severity']
|
| 242 |
+
|
| 243 |
+
# Elevate severity if standard profile is modified
|
| 244 |
+
if is_standard and severity == 'High':
|
| 245 |
+
severity = 'Critical'
|
| 246 |
+
|
| 247 |
+
findings.append({
|
| 248 |
+
'type': 'Dangerous Permission in Profile',
|
| 249 |
+
'severity': severity,
|
| 250 |
+
'profile': profile_name,
|
| 251 |
+
'profile_id': profile.get('Id'),
|
| 252 |
+
'permission': perm_info['description'],
|
| 253 |
+
'impact': perm_info['impact'],
|
| 254 |
+
'description': f"Profile '{profile_name}' has {perm_info['description']}",
|
| 255 |
+
'recommendation': 'Review and restrict this permission or move to Permission Set'
|
| 256 |
+
})
|
| 257 |
+
|
| 258 |
+
return findings
|
| 259 |
+
|
| 260 |
+
def _calculate_profile_risk_score(self, analysis: Dict) -> int:
|
| 261 |
+
"""
|
| 262 |
+
Calculate risk score for a profile (0-100)
|
| 263 |
+
"""
|
| 264 |
+
score = 0
|
| 265 |
+
|
| 266 |
+
# Critical issues add 30 points each
|
| 267 |
+
score += len(analysis['critical_issues']) * 30
|
| 268 |
+
|
| 269 |
+
# High risk warnings add 15 points each
|
| 270 |
+
score += len(analysis['warnings']) * 15
|
| 271 |
+
|
| 272 |
+
# More users = higher risk
|
| 273 |
+
user_count = analysis['assigned_users_count']
|
| 274 |
+
if user_count > 50:
|
| 275 |
+
score += 20
|
| 276 |
+
elif user_count > 20:
|
| 277 |
+
score += 10
|
| 278 |
+
|
| 279 |
+
return min(100, score)
|
| 280 |
+
|
| 281 |
+
def _generate_profile_recommendations(self, analysis: Dict, profile_data: Dict) -> List[str]:
|
| 282 |
+
"""
|
| 283 |
+
Generate specific recommendations for profile security
|
| 284 |
+
"""
|
| 285 |
+
recommendations = []
|
| 286 |
+
|
| 287 |
+
if analysis['critical_issues']:
|
| 288 |
+
recommendations.append(
|
| 289 |
+
f"🚨 URGENT: Remove {len(analysis['critical_issues'])} critical permission(s) "
|
| 290 |
+
f"or migrate to Permission Sets for granular control"
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
if analysis['assigned_users_count'] > 20:
|
| 294 |
+
recommendations.append(
|
| 295 |
+
f"⚠️ {analysis['assigned_users_count']} users assigned to this profile. "
|
| 296 |
+
"Consider splitting into multiple profiles based on job functions"
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
if len(analysis['dangerous_permissions']) >= 3:
|
| 300 |
+
recommendations.append(
|
| 301 |
+
"⚠️ Profile has multiple dangerous permissions. "
|
| 302 |
+
"Implement least privilege principle"
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
if not recommendations:
|
| 306 |
+
recommendations.append("✅ Profile follows security best practices")
|
| 307 |
+
|
| 308 |
+
return recommendations
|
| 309 |
+
|
| 310 |
+
def _count_dangerous_assignments(self, findings: List[Dict]) -> int:
|
| 311 |
+
"""
|
| 312 |
+
Count total dangerous permission assignments
|
| 313 |
+
"""
|
| 314 |
+
count = 0
|
| 315 |
+
for finding in findings:
|
| 316 |
+
if finding.get('type') == 'Dangerous Permission in Permission Set':
|
| 317 |
+
count += finding.get('assigned_users', 0)
|
| 318 |
+
return count
|
| 319 |
+
|
| 320 |
+
def check_permission_escalation_risk(
|
| 321 |
+
self,
|
| 322 |
+
user_permissions: List[str]
|
| 323 |
+
) -> Dict:
|
| 324 |
+
"""
|
| 325 |
+
Check if combination of permissions creates escalation risk
|
| 326 |
+
|
| 327 |
+
Args:
|
| 328 |
+
user_permissions: List of permission names user has
|
| 329 |
+
|
| 330 |
+
Returns:
|
| 331 |
+
Risk assessment
|
| 332 |
+
"""
|
| 333 |
+
risky_combinations = [
|
| 334 |
+
(['PermissionsAuthorApex', 'PermissionsModifyAllData'], 'Can deploy malicious code with full data access'),
|
| 335 |
+
(['PermissionsManageUsers', 'PermissionsViewAllData'], 'Can create admin users and access all data'),
|
| 336 |
+
(['PermissionsCustomizeApplication', 'PermissionsManageRoles'], 'Can manipulate security architecture')
|
| 337 |
+
]
|
| 338 |
+
|
| 339 |
+
risks_found = []
|
| 340 |
+
|
| 341 |
+
for combo, impact in risky_combinations:
|
| 342 |
+
if all(perm in user_permissions for perm in combo):
|
| 343 |
+
risks_found.append({
|
| 344 |
+
'combination': combo,
|
| 345 |
+
'impact': impact,
|
| 346 |
+
'severity': 'Critical'
|
| 347 |
+
})
|
| 348 |
+
|
| 349 |
+
return {
|
| 350 |
+
'has_escalation_risk': len(risks_found) > 0,
|
| 351 |
+
'risks': risks_found
|
| 352 |
+
}
|
prompts.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FULL_SCAN_PROMPT = """You are a Salesforce security expert analyzing an organization for security vulnerabilities.
|
| 2 |
+
|
| 3 |
+
Organization ID: {org_id}
|
| 4 |
+
|
| 5 |
+
Analysis Data:
|
| 6 |
+
{analysis_data}
|
| 7 |
+
|
| 8 |
+
Perform a comprehensive security analysis covering:
|
| 9 |
+
|
| 10 |
+
1. **Identity & Access Management**
|
| 11 |
+
- Dormant admin accounts
|
| 12 |
+
- Excessive admin privileges
|
| 13 |
+
- Login anomalies
|
| 14 |
+
|
| 15 |
+
2. **Authorization & Permissions**
|
| 16 |
+
- Dangerous permission sets (Modify All Data, View All Data)
|
| 17 |
+
- Permission escalation risks
|
| 18 |
+
- Least privilege violations
|
| 19 |
+
|
| 20 |
+
3. **Record-Level Security**
|
| 21 |
+
- Public Read/Write OWD settings
|
| 22 |
+
- Overly permissive sharing rules
|
| 23 |
+
- Data exposure risks
|
| 24 |
+
|
| 25 |
+
For each vulnerability found:
|
| 26 |
+
- **Severity**: Critical, High, Medium, or Low
|
| 27 |
+
- **Category**: Identity, Permissions, or Sharing
|
| 28 |
+
- **Description**: Clear explanation of the issue
|
| 29 |
+
- **Impact**: Business and security implications
|
| 30 |
+
- **Affected Items**: Users, permission sets, or objects
|
| 31 |
+
- **Recommendation**: Specific remediation steps
|
| 32 |
+
- **Priority**: Immediate, Short-term, or Long-term
|
| 33 |
+
|
| 34 |
+
Structure your response as follows:
|
| 35 |
+
|
| 36 |
+
## EXECUTIVE SUMMARY
|
| 37 |
+
[Brief overview of security posture, total vulnerabilities by severity]
|
| 38 |
+
|
| 39 |
+
## CRITICAL FINDINGS
|
| 40 |
+
[List all critical vulnerabilities]
|
| 41 |
+
|
| 42 |
+
## HIGH PRIORITY FINDINGS
|
| 43 |
+
[List all high priority vulnerabilities]
|
| 44 |
+
|
| 45 |
+
## MEDIUM PRIORITY FINDINGS
|
| 46 |
+
[List all medium priority vulnerabilities]
|
| 47 |
+
|
| 48 |
+
## LOW PRIORITY FINDINGS
|
| 49 |
+
[List all low priority vulnerabilities]
|
| 50 |
+
|
| 51 |
+
## RECOMMENDED ACTIONS
|
| 52 |
+
[Prioritized list of remediation steps]
|
| 53 |
+
|
| 54 |
+
## COMPLIANCE NOTES
|
| 55 |
+
[Any compliance implications - SOC 2, GDPR, etc.]
|
| 56 |
+
|
| 57 |
+
Be specific, actionable, and use clear, non-technical language where possible."""
|
| 58 |
+
|
| 59 |
+
PROFILE_SCAN_PROMPT = """You are a Salesforce security expert analyzing a specific profile for security vulnerabilities.
|
| 60 |
+
|
| 61 |
+
Profile ID: {profile_id}
|
| 62 |
+
Profile Name: {profile_name}
|
| 63 |
+
|
| 64 |
+
Analysis Data:
|
| 65 |
+
{analysis_data}
|
| 66 |
+
|
| 67 |
+
Analyze this profile for:
|
| 68 |
+
|
| 69 |
+
1. **System Permissions**
|
| 70 |
+
- Administrative privileges
|
| 71 |
+
- Dangerous permissions (View All, Modify All)
|
| 72 |
+
- Setup access
|
| 73 |
+
|
| 74 |
+
2. **Object Permissions**
|
| 75 |
+
- Excessive CRUD permissions
|
| 76 |
+
- ViewAllRecords and ModifyAllRecords
|
| 77 |
+
- Unnecessary object access
|
| 78 |
+
|
| 79 |
+
3. **Field Permissions**
|
| 80 |
+
- Access to sensitive fields
|
| 81 |
+
- Edit permissions on critical data
|
| 82 |
+
|
| 83 |
+
For each vulnerability:
|
| 84 |
+
- **Severity**: Critical, High, Medium, or Low
|
| 85 |
+
- **Type**: System Permission, Object Permission, or Field Permission
|
| 86 |
+
- **Description**: What is the issue
|
| 87 |
+
- **Risk**: What could go wrong
|
| 88 |
+
- **Recommendation**: How to fix it
|
| 89 |
+
|
| 90 |
+
Structure your response as:
|
| 91 |
+
|
| 92 |
+
## PROFILE OVERVIEW
|
| 93 |
+
[Summary of profile and its purpose]
|
| 94 |
+
|
| 95 |
+
## SECURITY ASSESSMENT
|
| 96 |
+
[Overall security rating and key concerns]
|
| 97 |
+
|
| 98 |
+
## CRITICAL ISSUES
|
| 99 |
+
[Critical vulnerabilities requiring immediate attention]
|
| 100 |
+
|
| 101 |
+
## HIGH PRIORITY ISSUES
|
| 102 |
+
[High priority issues]
|
| 103 |
+
|
| 104 |
+
## MEDIUM PRIORITY ISSUES
|
| 105 |
+
[Medium priority issues]
|
| 106 |
+
|
| 107 |
+
## RECOMMENDATIONS
|
| 108 |
+
[Specific steps to improve security]
|
| 109 |
+
|
| 110 |
+
## LEAST PRIVILEGE BASELINE
|
| 111 |
+
[What permissions this profile should have based on its apparent purpose]
|
| 112 |
+
|
| 113 |
+
Be specific and actionable in your recommendations."""
|
requirements.txt
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# OPTIMAX SECURITY AGENT - REQUIREMENTS
|
| 3 |
+
# Version: 3.1.0 (Multi-Model AI + Anomaly Detection)
|
| 4 |
+
# ============================================================================
|
| 5 |
+
|
| 6 |
+
# Core Framework - Gradio 6.3.0 Compatible
|
| 7 |
+
gradio==6.3.0
|
| 8 |
+
fastapi==0.115.5
|
| 9 |
+
uvicorn[standard]==0.32.1
|
| 10 |
+
pydantic==2.10.5
|
| 11 |
+
|
| 12 |
+
# AI/ML Dependencies - Transformers & PyTorch
|
| 13 |
+
transformers==4.46.0
|
| 14 |
+
torch==2.5.1
|
| 15 |
+
numpy<2.0.0
|
| 16 |
+
sentencepiece==0.1.99
|
| 17 |
+
accelerate==0.34.2
|
| 18 |
+
|
| 19 |
+
# Machine Learning Libraries - Includes Isolation Forest for Anomaly Detection
|
| 20 |
+
scikit-learn==1.5.2 # ✅ Provides IsolationForest (no additional install needed!)
|
| 21 |
+
scipy==1.14.1
|
| 22 |
+
|
| 23 |
+
# Salesforce Integration
|
| 24 |
+
simple-salesforce==1.12.4
|
| 25 |
+
requests==2.31.0
|
| 26 |
+
|
| 27 |
+
# Utilities
|
| 28 |
+
python-dotenv==1.0.0
|
| 29 |
+
|
| 30 |
+
# ============================================================================
|
| 31 |
+
# NOTES:
|
| 32 |
+
# ============================================================================
|
| 33 |
+
#
|
| 34 |
+
# ✅ No changes needed from previous version!
|
| 35 |
+
#
|
| 36 |
+
# scikit-learn==1.5.2 already includes:
|
| 37 |
+
# - IsolationForest (anomaly detection)
|
| 38 |
+
# - cosine_similarity (used by AI models)
|
| 39 |
+
# - All necessary ML utilities
|
| 40 |
+
#
|
| 41 |
+
# Python Version: 3.10, 3.11, or 3.13
|
| 42 |
+
# Tested on: Hugging Face Spaces (Python 3.11)
|
| 43 |
+
#
|
| 44 |
+
# Total Models:
|
| 45 |
+
# 1. CodeBERT (125M) - from transformers
|
| 46 |
+
# 2. RoBERTa (125M) - from transformers
|
| 47 |
+
# 3. SecBERT (110M) - from transformers
|
| 48 |
+
# 4. Isolation Forest - from scikit-learn ✅ NEW!
|
| 49 |
+
#
|
| 50 |
+
# ============================================================================
|
runtime.txt
ADDED
|
Binary file (32 Bytes). View file
|
|
|
sharing_analyzer.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# analyzers/sharing_analyzer.py
|
| 3 |
+
from typing import Dict, List, Optional
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
class SharingAnalyzer:
|
| 7 |
+
"""
|
| 8 |
+
Analyzes Salesforce sharing model for security vulnerabilities
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
RISKY_OWD_SETTINGS = {
|
| 12 |
+
'Public Read/Write': 'Critical',
|
| 13 |
+
'Public Read/Write/Transfer': 'Critical',
|
| 14 |
+
'Public Full Access': 'Critical',
|
| 15 |
+
'Public Read Only': 'High'
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
def analyze_sharing_settings(self, sharing_settings: Dict) -> Dict:
|
| 19 |
+
"""
|
| 20 |
+
Analyze organization-wide sharing settings
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
sharing_settings: OWD settings, sharing rules, role hierarchy
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Analysis results with findings
|
| 27 |
+
"""
|
| 28 |
+
findings = []
|
| 29 |
+
|
| 30 |
+
# Analyze OWD settings
|
| 31 |
+
owd_findings = self._analyze_owd(sharing_settings.get('organization_wide_defaults', []))
|
| 32 |
+
findings.extend(owd_findings)
|
| 33 |
+
|
| 34 |
+
# Analyze role hierarchy
|
| 35 |
+
role_findings = self._analyze_role_hierarchy(sharing_settings.get('role_hierarchy', []))
|
| 36 |
+
findings.extend(role_findings)
|
| 37 |
+
|
| 38 |
+
# Analyze sharing rules
|
| 39 |
+
rule_findings = self._analyze_sharing_rules(sharing_settings.get('sharing_rules', []))
|
| 40 |
+
findings.extend(rule_findings)
|
| 41 |
+
|
| 42 |
+
return {
|
| 43 |
+
'findings': findings,
|
| 44 |
+
'owd_objects_analyzed': len(sharing_settings.get('organization_wide_defaults', [])),
|
| 45 |
+
'roles_analyzed': len(sharing_settings.get('role_hierarchy', [])),
|
| 46 |
+
'sharing_rules_analyzed': len(sharing_settings.get('sharing_rules', []))
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
def _analyze_owd(self, owd_settings: List[Dict]) -> List[Dict]:
|
| 50 |
+
"""
|
| 51 |
+
Analyze Organization-Wide Default settings
|
| 52 |
+
"""
|
| 53 |
+
findings = []
|
| 54 |
+
|
| 55 |
+
sensitive_objects = ['Account', 'Contact', 'Opportunity', 'Contract', 'Case']
|
| 56 |
+
|
| 57 |
+
for setting in owd_settings:
|
| 58 |
+
obj_name = setting.get('ObjectName', 'Unknown')
|
| 59 |
+
default_access = setting.get('DefaultAccess', 'Private')
|
| 60 |
+
|
| 61 |
+
# Check if sensitive object has public access
|
| 62 |
+
if obj_name in sensitive_objects and default_access in self.RISKY_OWD_SETTINGS:
|
| 63 |
+
severity = self.RISKY_OWD_SETTINGS[default_access]
|
| 64 |
+
|
| 65 |
+
findings.append({
|
| 66 |
+
'type': 'Insecure Organization-Wide Default',
|
| 67 |
+
'severity': severity,
|
| 68 |
+
'object': obj_name,
|
| 69 |
+
'current_setting': default_access,
|
| 70 |
+
'description': f"{obj_name} has {default_access} OWD setting",
|
| 71 |
+
'impact': f"All users can access {obj_name} records by default",
|
| 72 |
+
'recommendation': f"Change {obj_name} OWD to Private and use sharing rules for controlled access"
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
return findings
|
| 76 |
+
|
| 77 |
+
def _analyze_role_hierarchy(self, roles: List[Dict]) -> List[Dict]:
|
| 78 |
+
"""
|
| 79 |
+
Analyze role hierarchy for security issues
|
| 80 |
+
"""
|
| 81 |
+
findings = []
|
| 82 |
+
|
| 83 |
+
# Check for flat hierarchy (security issue)
|
| 84 |
+
roles_without_parent = [r for r in roles if not r.get('ParentRoleId')]
|
| 85 |
+
|
| 86 |
+
if len(roles_without_parent) > len(roles) * 0.5:
|
| 87 |
+
findings.append({
|
| 88 |
+
'type': 'Flat Role Hierarchy',
|
| 89 |
+
'severity': 'Medium',
|
| 90 |
+
'description': 'Role hierarchy is too flat',
|
| 91 |
+
'impact': 'Users may have unintended access through role hierarchy',
|
| 92 |
+
'recommendation': 'Design role hierarchy based on data access needs, not org chart',
|
| 93 |
+
'roles_without_parent': len(roles_without_parent)
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
return findings
|
| 97 |
+
|
| 98 |
+
def _analyze_sharing_rules(self, sharing_rules: List[Dict]) -> List[Dict]:
|
| 99 |
+
"""
|
| 100 |
+
Analyze sharing rules for over-broad access
|
| 101 |
+
"""
|
| 102 |
+
findings = []
|
| 103 |
+
|
| 104 |
+
for rule in sharing_rules:
|
| 105 |
+
access_level = rule.get('AccessLevel', 'Read')
|
| 106 |
+
|
| 107 |
+
if access_level in ['Edit', 'All']:
|
| 108 |
+
findings.append({
|
| 109 |
+
'type': 'Broad Sharing Rule',
|
| 110 |
+
'severity': 'Medium',
|
| 111 |
+
'rule_name': rule.get('DeveloperName', 'Unknown'),
|
| 112 |
+
'object': rule.get('SobjectType', 'Unknown'),
|
| 113 |
+
'access_level': access_level,
|
| 114 |
+
'description': f"Sharing rule grants {access_level} access",
|
| 115 |
+
'recommendation': 'Review necessity of Edit/All access in sharing rules'
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
return findings
|
| 119 |
+
|
| 120 |
+
def check_sharing_bypass_permissions(self, user_permissions: List[str]) -> bool:
|
| 121 |
+
"""
|
| 122 |
+
Check if user has permissions that bypass sharing rules
|
| 123 |
+
"""
|
| 124 |
+
bypass_permissions = [
|
| 125 |
+
'PermissionsViewAllData',
|
| 126 |
+
'PermissionsModifyAllData'
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
return any(perm in user_permissions for perm in bypass_permissions)
|
vulenerabilities_db.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vulnerability Database
|
| 3 |
+
Knowledge base of known Salesforce security vulnerabilities and patterns
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
class VulnerabilityDatabase:
|
| 7 |
+
"""Database of known Salesforce security vulnerabilities"""
|
| 8 |
+
|
| 9 |
+
# Dangerous system permissions
|
| 10 |
+
DANGEROUS_PERMISSIONS = {
|
| 11 |
+
'ModifyAllData': {
|
| 12 |
+
'severity': 'critical',
|
| 13 |
+
'category': 'Data Access',
|
| 14 |
+
'description': 'User can create, edit, and delete all records regardless of sharing settings',
|
| 15 |
+
'risk': 'Complete data manipulation capability - can alter financial records, delete data, bypass all security',
|
| 16 |
+
'recommendation': 'Remove immediately. Grant object-specific permissions instead.',
|
| 17 |
+
'compliance_impact': 'SOC 2, GDPR, HIPAA violation risk',
|
| 18 |
+
'cve_reference': None,
|
| 19 |
+
'owasp_category': 'A01:2021 - Broken Access Control'
|
| 20 |
+
},
|
| 21 |
+
'ViewAllData': {
|
| 22 |
+
'severity': 'critical',
|
| 23 |
+
'category': 'Data Access',
|
| 24 |
+
'description': 'User can view all records regardless of sharing settings',
|
| 25 |
+
'risk': 'Complete visibility to sensitive data including PII, financial data, trade secrets',
|
| 26 |
+
'recommendation': 'Remove and grant object-specific read permissions only where needed.',
|
| 27 |
+
'compliance_impact': 'GDPR, HIPAA, PCI-DSS violation risk',
|
| 28 |
+
'cve_reference': None,
|
| 29 |
+
'owasp_category': 'A01:2021 - Broken Access Control'
|
| 30 |
+
},
|
| 31 |
+
'ManageUsers': {
|
| 32 |
+
'severity': 'high',
|
| 33 |
+
'category': 'User Management',
|
| 34 |
+
'description': 'User can create, edit, and deactivate users',
|
| 35 |
+
'risk': 'Can create admin accounts, lock out legitimate users, disable security controls',
|
| 36 |
+
'recommendation': 'Restrict to designated HR and IT administrators only.',
|
| 37 |
+
'compliance_impact': 'SOC 2 - Access control violation',
|
| 38 |
+
'cve_reference': None,
|
| 39 |
+
'owasp_category': 'A01:2021 - Broken Access Control'
|
| 40 |
+
},
|
| 41 |
+
'CustomizeApplication': {
|
| 42 |
+
'severity': 'high',
|
| 43 |
+
'category': 'Configuration',
|
| 44 |
+
'description': 'User can modify org configuration including security settings',
|
| 45 |
+
'risk': 'Can weaken security controls, modify validation rules, change workflows',
|
| 46 |
+
'recommendation': 'Restrict to administrators and approved developers only.',
|
| 47 |
+
'compliance_impact': 'SOC 2 - Change management violation',
|
| 48 |
+
'cve_reference': None,
|
| 49 |
+
'owasp_category': 'A05:2021 - Security Misconfiguration'
|
| 50 |
+
},
|
| 51 |
+
'AuthorApex': {
|
| 52 |
+
'severity': 'high',
|
| 53 |
+
'category': 'Development',
|
| 54 |
+
'description': 'User can write and deploy Apex code',
|
| 55 |
+
'risk': 'Can introduce backdoors, data exfiltration code, malicious logic',
|
| 56 |
+
'recommendation': 'Restrict to vetted developers only. Implement code review process.',
|
| 57 |
+
'compliance_impact': 'SOC 2 - Development security violation',
|
| 58 |
+
'cve_reference': None,
|
| 59 |
+
'owasp_category': 'A04:2021 - Insecure Design'
|
| 60 |
+
},
|
| 61 |
+
'ViewSetup': {
|
| 62 |
+
'severity': 'medium',
|
| 63 |
+
'category': 'Information Disclosure',
|
| 64 |
+
'description': 'User can view setup and configuration information',
|
| 65 |
+
'risk': 'Can map out security architecture for targeted attacks',
|
| 66 |
+
'recommendation': 'Limit to administrators and necessary support staff.',
|
| 67 |
+
'compliance_impact': 'Information disclosure',
|
| 68 |
+
'cve_reference': None,
|
| 69 |
+
'owasp_category': 'A01:2021 - Broken Access Control'
|
| 70 |
+
},
|
| 71 |
+
'ManageInternalUsers': {
|
| 72 |
+
'severity': 'high',
|
| 73 |
+
'category': 'User Management',
|
| 74 |
+
'description': 'Can manage internal users including password resets',
|
| 75 |
+
'risk': 'Account takeover, unauthorized access escalation',
|
| 76 |
+
'recommendation': 'Restrict to IT security team only.',
|
| 77 |
+
'compliance_impact': 'SOC 2 - Access control',
|
| 78 |
+
'cve_reference': None,
|
| 79 |
+
'owasp_category': 'A01:2021 - Broken Access Control'
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# Sharing model vulnerabilities
|
| 84 |
+
SHARING_VULNERABILITIES = {
|
| 85 |
+
'PublicReadWrite': {
|
| 86 |
+
'severity': 'critical',
|
| 87 |
+
'objects': ['Account', 'Contact', 'Opportunity', 'Lead', 'Case', 'Quote', 'Contract'],
|
| 88 |
+
'description': 'All users can read and write all records',
|
| 89 |
+
'risk': 'Data integrity compromise, unauthorized modifications, compliance violations',
|
| 90 |
+
'recommendation': 'Change to Private. Use sharing rules for controlled access.',
|
| 91 |
+
'compliance_impact': 'GDPR, SOC 2, PCI-DSS violations'
|
| 92 |
+
},
|
| 93 |
+
'PublicRead': {
|
| 94 |
+
'severity': 'high',
|
| 95 |
+
'objects': ['Account', 'Contact', 'Lead', 'Case', 'Opportunity'],
|
| 96 |
+
'description': 'All users can view all records',
|
| 97 |
+
'risk': 'Sensitive data exposure, privacy violations',
|
| 98 |
+
'recommendation': 'Change to Private. Implement role hierarchy or sharing rules.',
|
| 99 |
+
'compliance_impact': 'GDPR, HIPAA privacy violations'
|
| 100 |
+
},
|
| 101 |
+
'ControlledByParent': {
|
| 102 |
+
'severity': 'medium',
|
| 103 |
+
'objects': ['Contact', 'Opportunity', 'Case'],
|
| 104 |
+
'description': 'Inherits sharing from parent record',
|
| 105 |
+
'risk': 'Unintended access if parent sharing is too permissive',
|
| 106 |
+
'recommendation': 'Verify parent object sharing is appropriate.',
|
| 107 |
+
'compliance_impact': 'Potential data leakage'
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
# Identity and access vulnerabilities
|
| 112 |
+
IDENTITY_VULNERABILITIES = {
|
| 113 |
+
'DormantAdminAccount': {
|
| 114 |
+
'severity': 'high',
|
| 115 |
+
'threshold_days': 90,
|
| 116 |
+
'description': 'Administrator account inactive for 90+ days',
|
| 117 |
+
'risk': 'Orphaned privileged accounts are prime targets for attackers',
|
| 118 |
+
'recommendation': 'Deactivate or remove admin privileges immediately.',
|
| 119 |
+
'compliance_impact': 'SOC 2, PCI-DSS - Access review violation'
|
| 120 |
+
},
|
| 121 |
+
'NoMFA': {
|
| 122 |
+
'severity': 'high',
|
| 123 |
+
'description': 'User does not have MFA enabled',
|
| 124 |
+
'risk': 'Account vulnerable to credential theft and phishing',
|
| 125 |
+
'recommendation': 'Enable MFA for all users, especially administrators.',
|
| 126 |
+
'compliance_impact': 'SOC 2, PCI-DSS - Authentication requirement'
|
| 127 |
+
},
|
| 128 |
+
'FailedLoginAttempts': {
|
| 129 |
+
'severity': 'medium',
|
| 130 |
+
'threshold': 5,
|
| 131 |
+
'description': 'Multiple failed login attempts detected',
|
| 132 |
+
'risk': 'Possible brute force attack or credential stuffing',
|
| 133 |
+
'recommendation': 'Investigate source IPs, consider account lockout policies.',
|
| 134 |
+
'compliance_impact': 'Security monitoring requirement'
|
| 135 |
+
},
|
| 136 |
+
'UnusualLoginTime': {
|
| 137 |
+
'severity': 'medium',
|
| 138 |
+
'description': 'Login outside normal business hours',
|
| 139 |
+
'risk': 'Compromised credentials or unauthorized access',
|
| 140 |
+
'recommendation': 'Implement time-based login restrictions, investigate anomalies.',
|
| 141 |
+
'compliance_impact': 'Anomaly detection'
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Known attack patterns
|
| 146 |
+
ATTACK_PATTERNS = {
|
| 147 |
+
'PrivilegeEscalation': {
|
| 148 |
+
'indicators': [
|
| 149 |
+
'Multiple permission sets assigned to non-admin user',
|
| 150 |
+
'Permission set with admin-level permissions',
|
| 151 |
+
'Profile modification to add dangerous permissions'
|
| 152 |
+
],
|
| 153 |
+
'severity': 'critical',
|
| 154 |
+
'description': 'User gaining elevated privileges through permission accumulation',
|
| 155 |
+
'mitigation': 'Regular permission audits, least privilege enforcement'
|
| 156 |
+
},
|
| 157 |
+
'DataExfiltration': {
|
| 158 |
+
'indicators': [
|
| 159 |
+
'Excessive report exports',
|
| 160 |
+
'API usage spikes',
|
| 161 |
+
'Large data exports',
|
| 162 |
+
'Access to View All Data'
|
| 163 |
+
],
|
| 164 |
+
'severity': 'critical',
|
| 165 |
+
'description': 'Suspicious data access patterns indicating potential theft',
|
| 166 |
+
'mitigation': 'Monitor API usage, implement data loss prevention'
|
| 167 |
+
},
|
| 168 |
+
'AccountTakeover': {
|
| 169 |
+
'indicators': [
|
| 170 |
+
'Login from new geographic location',
|
| 171 |
+
'Multiple failed logins followed by success',
|
| 172 |
+
'Password reset without MFA',
|
| 173 |
+
'Unusual activity after login'
|
| 174 |
+
],
|
| 175 |
+
'severity': 'critical',
|
| 176 |
+
'description': 'Compromised user account being accessed by attacker',
|
| 177 |
+
'mitigation': 'Enforce MFA, monitor login anomalies, implement IP restrictions'
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Compliance requirements
|
| 182 |
+
COMPLIANCE_MAPPINGS = {
|
| 183 |
+
'SOC2': {
|
| 184 |
+
'CC6.1': 'Logical and physical access controls',
|
| 185 |
+
'CC6.2': 'Prior to issuing system credentials',
|
| 186 |
+
'CC6.3': 'Removes access when terminated',
|
| 187 |
+
'CC6.6': 'Manages system-to-system communications',
|
| 188 |
+
'CC7.2': 'Monitors system components'
|
| 189 |
+
},
|
| 190 |
+
'GDPR': {
|
| 191 |
+
'Article_5': 'Principles relating to processing',
|
| 192 |
+
'Article_25': 'Data protection by design and default',
|
| 193 |
+
'Article_32': 'Security of processing'
|
| 194 |
+
},
|
| 195 |
+
'HIPAA': {
|
| 196 |
+
'164.308': 'Administrative safeguards',
|
| 197 |
+
'164.312': 'Technical safeguards'
|
| 198 |
+
},
|
| 199 |
+
'PCI_DSS': {
|
| 200 |
+
'Req_7': 'Restrict access to cardholder data',
|
| 201 |
+
'Req_8': 'Identify and authenticate access',
|
| 202 |
+
'Req_10': 'Track and monitor all access'
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
@classmethod
|
| 207 |
+
def get_permission_info(cls, permission_name: str) -> dict:
|
| 208 |
+
"""Get information about a specific permission"""
|
| 209 |
+
return cls.DANGEROUS_PERMISSIONS.get(permission_name, {
|
| 210 |
+
'severity': 'low',
|
| 211 |
+
'description': f'Permission: {permission_name}',
|
| 212 |
+
'recommendation': 'Review if this permission is necessary.'
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
@classmethod
|
| 216 |
+
def get_sharing_vulnerability(cls, sharing_model: str) -> dict:
|
| 217 |
+
"""Get vulnerability info for sharing model"""
|
| 218 |
+
for vuln_type, info in cls.SHARING_VULNERABILITIES.items():
|
| 219 |
+
if vuln_type in sharing_model:
|
| 220 |
+
return info
|
| 221 |
+
return {}
|
| 222 |
+
|
| 223 |
+
@classmethod
|
| 224 |
+
def get_identity_vulnerability(cls, vuln_type: str) -> dict:
|
| 225 |
+
"""Get identity vulnerability information"""
|
| 226 |
+
return cls.IDENTITY_VULNERABILITIES.get(vuln_type, {})
|
| 227 |
+
|
| 228 |
+
@classmethod
|
| 229 |
+
def check_compliance_impact(cls, finding: dict) -> list:
|
| 230 |
+
"""Check which compliance frameworks are impacted"""
|
| 231 |
+
impacts = []
|
| 232 |
+
|
| 233 |
+
if finding.get('severity') in ['critical', 'high']:
|
| 234 |
+
impacts.extend(['SOC2', 'GDPR'])
|
| 235 |
+
|
| 236 |
+
if 'data' in finding.get('category', '').lower():
|
| 237 |
+
impacts.extend(['GDPR', 'HIPAA'])
|
| 238 |
+
|
| 239 |
+
if 'access' in finding.get('type', '').lower():
|
| 240 |
+
impacts.extend(['SOC2', 'PCI_DSS'])
|
| 241 |
+
|
| 242 |
+
return list(set(impacts))
|
| 243 |
+
|
| 244 |
+
@classmethod
|
| 245 |
+
def get_remediation_priority(cls, severity: str, compliance_impact: list) -> int:
|
| 246 |
+
"""Calculate remediation priority (1=highest, 5=lowest)"""
|
| 247 |
+
base_priority = {
|
| 248 |
+
'critical': 1,
|
| 249 |
+
'high': 2,
|
| 250 |
+
'medium': 3,
|
| 251 |
+
'low': 4,
|
| 252 |
+
'info': 5
|
| 253 |
+
}.get(severity, 5)
|
| 254 |
+
|
| 255 |
+
# Increase priority if compliance is impacted
|
| 256 |
+
if compliance_impact and base_priority > 1:
|
| 257 |
+
base_priority -= 1
|
| 258 |
+
|
| 259 |
+
return base_priority
|
| 260 |
+
|
| 261 |
+
@classmethod
|
| 262 |
+
def get_similar_vulnerabilities(cls, vulnerability_type: str) -> list:
|
| 263 |
+
"""Get similar vulnerabilities to watch for"""
|
| 264 |
+
|
| 265 |
+
similarity_map = {
|
| 266 |
+
'ModifyAllData': ['ViewAllData', 'ManageUsers'],
|
| 267 |
+
'ViewAllData': ['ModifyAllData', 'ViewSetup'],
|
| 268 |
+
'PublicReadWrite': ['PublicRead', 'ControlledByParent'],
|
| 269 |
+
'DormantAdminAccount': ['NoMFA', 'UnusualLoginTime']
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
return similarity_map.get(vulnerability_type, [])
|