Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Salesforce PII Analyzer</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.loading-spinner { | |
width: 40px; | |
height: 40px; | |
border: 4px solid rgba(0, 0, 0, 0.1); | |
border-radius: 50%; | |
border-top-color: #3498db; | |
animation: spin 1s ease-in-out infinite; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
.pii-high { | |
background-color: #fee2e2; | |
border-left: 4px solid #ef4444; | |
} | |
.pii-medium { | |
background-color: #fef3c7; | |
border-left: 4px solid #f59e0b; | |
} | |
.pii-low { | |
background-color: #dbeafe; | |
border-left: 4px solid #3b82f6; | |
} | |
.slide-in { | |
animation: slideIn 0.3s ease-out forwards; | |
} | |
@keyframes slideIn { | |
from { transform: translateY(20px); opacity: 0; } | |
to { transform: translateY(0); opacity: 1; } | |
} | |
.progress-bar { | |
height: 6px; | |
background-color: #e5e7eb; | |
border-radius: 3px; | |
overflow: hidden; | |
} | |
.progress-fill { | |
height: 100%; | |
background-color: #10b981; | |
transition: width 0.3s ease; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8 max-w-6xl"> | |
<header class="mb-8"> | |
<div class="flex items-center justify-between"> | |
<div> | |
<h1 class="text-3xl font-bold text-gray-800 flex items-center"> | |
<i class="fas fa-shield-alt text-blue-500 mr-3"></i> | |
Salesforce PII Analyzer | |
</h1> | |
<p class="text-gray-600 mt-2">Identify potential Personally Identifiable Information in your Salesforce org</p> | |
</div> | |
<div id="loginButton" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg shadow-md transition-colors cursor-pointer flex items-center"> | |
<i class="fab fa-salesforce mr-2"></i> | |
Login with Salesforce | |
</div> | |
<div id="userInfo" class="hidden items-center"> | |
<img id="userPhoto" class="w-10 h-10 rounded-full mr-3" src="" alt="User photo"> | |
<div> | |
<div id="userName" class="font-medium text-gray-800"></div> | |
<div id="orgName" class="text-sm text-gray-500"></div> | |
</div> | |
<button id="logoutButton" class="ml-4 text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-sign-out-alt"></i> | |
</button> | |
</div> | |
</div> | |
</header> | |
<div id="appContent" class="hidden"> | |
<div class="bg-white rounded-xl shadow-md p-6 mb-6"> | |
<h2 class="text-xl font-semibold text-gray-800 mb-4">Org Analysis</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> | |
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
<div class="text-gray-500 text-sm mb-1">Total Objects</div> | |
<div id="totalObjects" class="text-2xl font-bold text-gray-800">0</div> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
<div class="text-gray-500 text-sm mb-1">Potential PII Fields</div> | |
<div id="piiFields" class="text-2xl font-bold text-red-600">0</div> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
<div class="text-gray-500 text-sm mb-1">Analysis Progress</div> | |
<div class="progress-bar mt-2"> | |
<div id="progressFill" class="progress-fill" style="width: 0%"></div> | |
</div> | |
<div id="progressText" class="text-sm text-gray-600 mt-1 text-right">0%</div> | |
</div> | |
</div> | |
<div class="flex items-center justify-between mb-4"> | |
<h3 class="font-medium text-gray-700">Object Analysis Results</h3> | |
<div class="relative"> | |
<select id="filterSelect" class="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
<option value="all">All Objects</option> | |
<option value="pii">Only PII Objects</option> | |
<option value="standard">Standard Objects</option> | |
<option value="custom">Custom Objects</option> | |
</select> | |
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
<i class="fas fa-chevron-down text-xs"></i> | |
</div> | |
</div> | |
</div> | |
<div id="resultsContainer" class="space-y-3"> | |
<!-- Results will be populated here --> | |
</div> | |
</div> | |
<div id="piiGuidance" class="bg-white rounded-xl shadow-md p-6"> | |
<h2 class="text-xl font-semibold text-gray-800 mb-4">PII Identification Guidance</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
<div class="p-4 rounded-lg pii-high"> | |
<div class="flex items-center mb-2"> | |
<div class="w-3 h-3 rounded-full bg-red-500 mr-2"></div> | |
<h3 class="font-medium">High Risk PII</h3> | |
</div> | |
<p class="text-sm text-gray-700">Fields that clearly contain personal data like SSN, credit card numbers, full names, etc.</p> | |
</div> | |
<div class="p-4 rounded-lg pii-medium"> | |
<div class="flex items-center mb-2"> | |
<div class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></div> | |
<h3 class="font-medium">Potential PII</h3> | |
</div> | |
<p class="text-sm text-gray-700">Fields that might contain personal data based on naming patterns or field types.</p> | |
</div> | |
<div class="p-4 rounded-lg pii-low"> | |
<div class="flex items-center mb-2"> | |
<div class="w-3 h-3 rounded-full bg-blue-500 mr-2"></div> | |
<h3 class="font-medium">Low Risk</h3> | |
</div> | |
<p class="text-sm text-gray-700">Fields that are unlikely to contain personal data but should still be reviewed.</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="loginState" class="flex flex-col items-center justify-center py-20"> | |
<div class="bg-white rounded-xl shadow-md p-8 max-w-md w-full text-center"> | |
<i class="fab fa-salesforce text-5xl text-blue-500 mb-4"></i> | |
<h2 class="text-2xl font-bold text-gray-800 mb-2">Salesforce PII Analyzer</h2> | |
<p class="text-gray-600 mb-6">Login with your Salesforce account to analyze your org for potential Personally Identifiable Information (PII) data.</p> | |
<div id="loginButtonCenter" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg shadow-md transition-colors cursor-pointer flex items-center justify-center mx-auto w-full max-w-xs"> | |
<i class="fab fa-salesforce mr-2"></i> | |
Login with Salesforce | |
</div> | |
</div> | |
</div> | |
<div id="loadingState" class="hidden flex flex-col items-center justify-center py-20"> | |
<div class="bg-white rounded-xl shadow-md p-8 max-w-md w-full text-center"> | |
<div class="loading-spinner mx-auto mb-4"></div> | |
<h2 class="text-xl font-bold text-gray-800 mb-2" id="loadingText">Connecting to Salesforce...</h2> | |
<p class="text-gray-600" id="loadingSubtext">Please wait while we authenticate your session.</p> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Configuration | |
const CLIENT_ID = '3MVG9pRzvMkjMb6lZlt3YjDQwe2tA1wWE4jHVJ.VZJj4i8T6gJQBWmwdVJZqgNrm4sY.sp0SJQCH2TfULXjG_'; | |
const CALLBACK_URL = 'https://login.salesforce.com/services/oauth2/success'; | |
const LOGIN_URL = 'https://login.salesforce.com'; | |
const API_VERSION = '56.0'; | |
// DOM elements | |
const loginButton = document.getElementById('loginButton'); | |
const loginButtonCenter = document.getElementById('loginButtonCenter'); | |
const logoutButton = document.getElementById('logoutButton'); | |
const userInfo = document.getElementById('userInfo'); | |
const userName = document.getElementById('userName'); | |
const userPhoto = document.getElementById('userPhoto'); | |
const orgName = document.getElementById('orgName'); | |
const appContent = document.getElementById('appContent'); | |
const loginState = document.getElementById('loginState'); | |
const loadingState = document.getElementById('loadingState'); | |
const loadingText = document.getElementById('loadingText'); | |
const loadingSubtext = document.getElementById('loadingSubtext'); | |
const resultsContainer = document.getElementById('resultsContainer'); | |
const totalObjects = document.getElementById('totalObjects'); | |
const piiFields = document.getElementById('piiFields'); | |
const progressFill = document.getElementById('progressFill'); | |
const progressText = document.getElementById('progressText'); | |
const filterSelect = document.getElementById('filterSelect'); | |
// State | |
let accessToken = null; | |
let instanceUrl = null; | |
let userId = null; | |
let orgId = null; | |
let objectsData = []; | |
let piiCount = 0; | |
// Initialize | |
document.addEventListener('DOMContentLoaded', () => { | |
// Check for OAuth response in URL | |
if (window.location.hash) { | |
handleOAuthResponse(); | |
} | |
// Set up event listeners | |
loginButton.addEventListener('click', initiateOAuth); | |
loginButtonCenter.addEventListener('click', initiateOAuth); | |
logoutButton.addEventListener('click', logout); | |
filterSelect.addEventListener('change', filterResults); | |
}); | |
// OAuth Functions | |
function initiateOAuth() { | |
const authUrl = `${LOGIN_URL}/services/oauth2/authorize?response_type=token` + | |
`&client_id=${CLIENT_ID}` + | |
`&redirect_uri=${encodeURIComponent(CALLBACK_URL)}` + | |
`&scope=api%20refresh_token%20web` + | |
`&prompt=login%20consent`; | |
// Show loading state | |
loginState.classList.add('hidden'); | |
loadingState.classList.remove('hidden'); | |
loadingText.textContent = 'Redirecting to Salesforce...'; | |
loadingSubtext.textContent = 'Please login with your Salesforce credentials'; | |
// Redirect to Salesforce login | |
window.location.href = authUrl; | |
} | |
function handleOAuthResponse() { | |
const hash = window.location.hash.substring(1); | |
const params = new URLSearchParams(hash); | |
if (params.has('access_token')) { | |
// Get OAuth parameters | |
accessToken = params.get('access_token'); | |
instanceUrl = params.get('instance_url'); | |
userId = params.get('id').split('/').pop(); | |
orgId = params.get('id').split('/')[4]; | |
// Clean up URL | |
history.replaceState({}, document.title, window.location.pathname); | |
// Show loading state | |
loginState.classList.add('hidden'); | |
loadingState.classList.remove('hidden'); | |
loadingText.textContent = 'Loading org data...'; | |
loadingSubtext.textContent = 'Fetching object metadata from your Salesforce org'; | |
// Fetch user info and start analysis | |
fetchUserInfo(); | |
} else if (params.has('error')) { | |
// Handle OAuth error | |
console.error('OAuth error:', params.get('error_description')); | |
showLoginState(); | |
} | |
} | |
async function fetchUserInfo() { | |
try { | |
const response = await fetch(`${instanceUrl}/services/data/v${API_VERSION}/query?q=` + | |
encodeURIComponent(`SELECT Name, Username, SmallPhotoUrl FROM User WHERE Id = '${userId}'`), { | |
headers: { | |
'Authorization': `Bearer ${accessToken}` | |
} | |
}); | |
const data = await response.json(); | |
if (data.records && data.records.length > 0) { | |
const user = data.records[0]; | |
// Update UI with user info | |
userName.textContent = user.Name; | |
userPhoto.src = user.SmallPhotoUrl; | |
orgName.textContent = `Org: ${orgId}`; | |
userInfo.classList.remove('hidden'); | |
loginButton.classList.add('hidden'); | |
// Start analysis | |
analyzeOrg(); | |
} | |
} catch (error) { | |
console.error('Error fetching user info:', error); | |
showLoginState(); | |
} | |
} | |
// Analysis Functions | |
async function analyzeOrg() { | |
try { | |
loadingText.textContent = 'Analyzing your Salesforce org...'; | |
loadingSubtext.textContent = 'This may take a few moments depending on your org size'; | |
// Get all objects in the org | |
const objectsResponse = await fetch(`${instanceUrl}/services/data/v${API_VERSION}/sobjects`, { | |
headers: { | |
'Authorization': `Bearer ${accessToken}` | |
} | |
}); | |
const objects = await objectsResponse.json(); | |
objectsData = objects.sobjects.filter(obj => obj.queryable && !obj.name.endsWith('ChangeEvent')); | |
totalObjects.textContent = objectsData.length; | |
// Update progress | |
updateProgress(0); | |
// Analyze each object | |
for (let i = 0; i < objectsData.length; i++) { | |
const obj = objectsData[i]; | |
await analyzeObject(obj); | |
updateProgress(((i + 1) / objectsData.length) * 100); | |
} | |
// Show results | |
loadingState.classList.add('hidden'); | |
appContent.classList.remove('hidden'); | |
renderResults(); | |
} catch (error) { | |
console.error('Error analyzing org:', error); | |
loadingText.textContent = 'Error analyzing org'; | |
loadingSubtext.textContent = error.message; | |
} | |
} | |
async function analyzeObject(obj) { | |
try { | |
// Get object describe | |
const describeResponse = await fetch(`${instanceUrl}/services/data/v${API_VERSION}/sobjects/${obj.name}/describe`, { | |
headers: { | |
'Authorization': `Bearer ${accessToken}` | |
} | |
}); | |
const describe = await describeResponse.json(); | |
obj.fields = describe.fields; | |
// Analyze fields for PII | |
obj.piiFields = []; | |
obj.piiScore = 0; | |
for (const field of obj.fields) { | |
const piiRisk = detectPII(field); | |
if (piiRisk.level > 0) { | |
obj.piiFields.push({ | |
...field, | |
piiRisk | |
}); | |
// Add to PII count if high or medium risk | |
if (piiRisk.level >= 2) { | |
piiCount++; | |
piiFields.textContent = piiCount; | |
} | |
} | |
} | |
// Calculate object PII score (0-100) | |
if (obj.piiFields.length > 0) { | |
const highRiskCount = obj.piiFields.filter(f => f.piiRisk.level === 3).length; | |
const medRiskCount = obj.piiFields.filter(f => f.piiRisk.level === 2).length; | |
obj.piiScore = Math.min(100, (highRiskCount * 5) + (medRiskCount * 2)); | |
} | |
} catch (error) { | |
console.error(`Error analyzing object ${obj.name}:`, error); | |
obj.error = error.message; | |
} | |
} | |
function detectPII(field) { | |
const fieldName = field.name.toLowerCase(); | |
const fieldLabel = field.label.toLowerCase(); | |
const fieldType = field.type; | |
// Common PII patterns | |
const piiPatterns = { | |
high: [ | |
'ssn', 'social', 'security', 'taxid', 'tax id', 'creditcard', 'credit card', | |
'cardnumber', 'card number', 'passport', 'drivinglicense', 'driving license', | |
'bankaccount', 'bank account', 'routingnumber', 'routing number', 'dob', | |
'dateofbirth', 'date of birth', 'mothersmaiden', 'mothers maiden' | |
], | |
medium: [ | |
'name', 'firstname', 'first name', 'lastname', 'last name', 'middlename', | |
'middle name', 'email', 'phone', 'mobile', 'address', 'street', 'city', | |
'state', 'zip', 'postalcode', 'postal code', 'country', 'gender', 'race', | |
'ethnicity', 'salary', 'income', 'health', 'medical', 'insurance' | |
] | |
}; | |
// Check for high risk PII | |
for (const pattern of piiPatterns.high) { | |
if (fieldName.includes(pattern) || fieldLabel.includes(pattern)) { | |
return { | |
level: 3, | |
reason: `Field name suggests high risk PII (${pattern})` | |
}; | |
} | |
} | |
// Check for medium risk PII | |
for (const pattern of piiPatterns.medium) { | |
if (fieldName.includes(pattern) || fieldLabel.includes(pattern)) { | |
return { | |
level: 2, | |
reason: `Field name suggests potential PII (${pattern})` | |
}; | |
} | |
} | |
// Check field types that might contain PII | |
if (fieldType === 'email') { | |
return { | |
level: 2, | |
reason: 'Email field type may contain PII' | |
}; | |
} | |
if (fieldType === 'phone' || fieldType === 'textarea') { | |
return { | |
level: 1, | |
reason: `${fieldType} field type may contain PII` | |
}; | |
} | |
// No PII detected | |
return { | |
level: 0, | |
reason: 'No obvious PII indicators' | |
}; | |
} | |
// UI Functions | |
function updateProgress(percent) { | |
progressFill.style.width = `${percent}%`; | |
progressText.textContent = `${Math.round(percent)}%`; | |
} | |
function renderResults() { | |
resultsContainer.innerHTML = ''; | |
// Sort objects by PII score (descending) | |
const sortedObjects = [...objectsData].sort((a, b) => b.piiScore - a.piiScore); | |
for (const obj of sortedObjects) { | |
if (obj.piiFields.length > 0) { | |
const objectCard = document.createElement('div'); | |
objectCard.className = 'bg-gray-50 rounded-lg border border-gray-200 overflow-hidden slide-in'; | |
// Object header | |
const header = document.createElement('div'); | |
header.className = 'flex items-center justify-between p-4 bg-white border-b border-gray-200 cursor-pointer'; | |
header.innerHTML = ` | |
<div class="flex items-center"> | |
<div class="w-3 h-3 rounded-full ${getPiiColorClass(obj.piiScore)} mr-3"></div> | |
<div> | |
<h4 class="font-medium text-gray-800">${obj.label} (${obj.name})</h4> | |
<div class="text-xs text-gray-500">${obj.piiFields.length} potential PII fields</div> | |
</div> | |
</div> | |
<div class="flex items-center"> | |
<span class="text-sm font-medium ${getPiiTextColorClass(obj.piiScore)} mr-2"> | |
PII Score: ${obj.piiScore} | |
</span> | |
<i class="fas fa-chevron-down text-gray-400 transition-transform"></i> | |
</div> | |
`; | |
// Fields container (initially hidden) | |
const fieldsContainer = document.createElement('div'); | |
fieldsContainer.className = 'hidden p-4 bg-white'; | |
// Add fields to container | |
obj.piiFields.sort((a, b) => b.piiRisk.level - a.piiRisk.level).forEach(field => { | |
const fieldDiv = document.createElement('div'); | |
fieldDiv.className = `p-3 mb-2 rounded-md ${getPiiFieldClass(field.piiRisk.level)}`; | |
fieldDiv.innerHTML = ` | |
<div class="flex justify-between items-start"> | |
<div> | |
<div class="font-medium">${field.label} (${field.name})</div> | |
<div class="text-sm text-gray-600 mt-1">${field.piiRisk.reason}</div> | |
</div> | |
<span class="text-xs font-medium px-2 py-1 rounded-full ${getPiiRiskBadgeClass(field.piiRisk.level)}"> | |
${getPiiRiskLabel(field.piiRisk.level)} | |
</span> | |
</div> | |
<div class="text-xs text-gray-500 mt-2"> | |
<span class="font-medium">Type:</span> ${field.type} | | |
<span class="font-medium">Length:</span> ${field.length || 'N/A'} | | |
<span class="font-medium">API Name:</span> ${field.name} | |
</div> | |
`; | |
fieldsContainer.appendChild(fieldDiv); | |
}); | |
// Toggle fields visibility on header click | |
header.addEventListener('click', () => { | |
const icon = header.querySelector('i'); | |
fieldsContainer.classList.toggle('hidden'); | |
icon.classList.toggle('fa-chevron-down'); | |
icon.classList.toggle('fa-chevron-up'); | |
}); | |
objectCard.appendChild(header); | |
objectCard.appendChild(fieldsContainer); | |
resultsContainer.appendChild(objectCard); | |
} | |
} | |
} | |
function filterResults() { | |
const filterValue = filterSelect.value; | |
const allCards = resultsContainer.querySelectorAll('.bg-gray-50'); | |
allCards.forEach(card => { | |
const objectName = card.querySelector('h4').textContent; | |
const isStandard = !objectName.includes('__c'); | |
const hasPII = !card.querySelector('.hidden'); | |
switch (filterValue) { | |
case 'all': | |
card.style.display = 'block'; | |
break; | |
case 'pii': | |
card.style.display = hasPII ? 'block' : 'none'; | |
break; | |
case 'standard': | |
card.style.display = isStandard ? 'block' : 'none'; | |
break; | |
case 'custom': | |
card.style.display = isStandard ? 'none' : 'block'; | |
break; | |
} | |
}); | |
} | |
function getPiiColorClass(score) { | |
if (score >= 60) return 'bg-red-500'; | |
if (score >= 30) return 'bg-yellow-500'; | |
return 'bg-blue-500'; | |
} | |
function getPiiTextColorClass(score) { | |
if (score >= 60) return 'text-red-600'; | |
if (score >= 30) return 'text-yellow-600'; | |
return 'text-blue-600'; | |
} | |
function getPiiFieldClass(level) { | |
if (level === 3) return 'pii-high'; | |
if (level === 2) return 'pii-medium'; | |
return 'pii-low'; | |
} | |
function getPiiRiskBadgeClass(level) { | |
if (level === 3) return 'bg-red-100 text-red-800'; | |
if (level === 2) return 'bg-yellow-100 text-yellow-800'; | |
return 'bg-blue-100 text-blue-800'; | |
} | |
function getPiiRiskLabel(level) { | |
if (level === 3) return 'High Risk'; | |
if (level === 2) return 'Medium Risk'; | |
return 'Low Risk'; | |
} | |
function showLoginState() { | |
loadingState.classList.add('hidden'); | |
appContent.classList.add('hidden'); | |
loginState.classList.remove('hidden'); | |
loginButton.classList.remove('hidden'); | |
userInfo.classList.add('hidden'); | |
} | |
function logout() { | |
// Clear OAuth data | |
accessToken = null; | |
instanceUrl = null; | |
userId = null; | |
orgId = null; | |
objectsData = []; | |
piiCount = 0; | |
// Reset UI | |
showLoginState(); | |
totalObjects.textContent = '0'; | |
piiFields.textContent = '0'; | |
progressFill.style.width = '0%'; | |
progressText.textContent = '0%'; | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Naresh1993/deepsitetest" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |