Or press ESC to close.

Building an Appium 3 Security Dashboard (Part II): Real-Time UI & Production Deployment

Oct 6th 2025 24 min read
hard
javascriptES6
appium3.0.2
mobile
security
reporting
architecture
webdriverio9.20.0

In Part I, we built the backend infrastructure—plugin architecture, command interception, session tracking, and the Express server. We can now capture security events, but teams can't see them yet.

In this part, we'll build the real-time dashboard UI with Socket.IO, implement intelligent security recommendations, test the complete system, and discuss production deployment strategies. Let's make this monitoring data visible and actionable.

Real-Time Updates with Socket.IO

A security dashboard that only updates when you refresh the page defeats the purpose. Security events need to be visible the moment they happen—especially high-risk operations like shell access or permission changes. Socket.IO provides the bidirectional communication channel we need to push updates from the Appium plugin to the dashboard instantly.

Broadcasting Events from the Plugin

Whenever a security event occurs, we broadcast it through Socket.IO immediately after logging it:

                
logSecurityEvent(event) {
  // Store in shared state
  this.securityEvents.push(event);
  
  // Memory management
  if (this.securityEvents.length > 1000) {
    this.securityEvents = this.securityEvents.slice(-1000);
  }
  
  // Broadcast to all connected dashboard clients
  if (this.io) {
    this.io.emit('security-event', event);
  }
  
  // Also broadcast via server-level IO (if available)
  if (SecurityDashboardPlugin.dashboardIO) {
    SecurityDashboardPlugin.dashboardIO.emit('security-event', event);
  }
}
                

We broadcast through both instance-level and static Socket.IO connections because we can't guarantee which plugin instance will be active when an event fires. This dual-broadcast approach ensures events always reach connected clients.

Client-Side Event Handling

The dashboard JavaScript connects to Socket.IO and listens for events:

                
class SecurityDashboard {
  initSocket() {
    this.socket = io();
    
    this.socket.on('connect', () => {
      console.log('Connected to security dashboard');
      this.updateConnectionStatus(true);
    });
    
    this.socket.on('disconnect', () => {
      console.log('Disconnected from security dashboard');
      this.updateConnectionStatus(false);
    });
    
    this.socket.on('security-status', (data) => {
      this.updateDashboard(data);
    });
    
    this.socket.on('security-event', (event) => {
      this.handleNewEvent(event);
    });
  }
  
  handleNewEvent(event) {
    // Add to local event list
    this.events.unshift(event);
    
    // Update UI components
    this.updateOverviewMetrics();
    this.updateEventsTimeline();
    this.updateRiskChart();
    
    // Show notification for high-risk events
    if (event.riskLevel >= 3) {
      this.showNotification(event);
    }
  }
}
                

When a new event arrives, we immediately update all affected UI sections—the metrics counters, event timeline, and risk distribution chart.

The Polling Fallback Strategy

While Socket.IO handles real-time updates beautifully, we can't rely on it exclusively. WebSocket connections can drop, clients might connect before the server is ready, or network conditions might prevent WebSocket establishment. We implement polling as a reliable fallback:

                
init() {
  this.initSocket();
  this.loadInitialData();
  
  // Poll every 2 seconds as a fallback
  setInterval(() => this.loadInitialData(), 2000);
}

async loadInitialData() {
  try {
    const response = await fetch('/api/security-status');
    const data = await response.json();
    this.updateDashboard(data);
    
    const complianceResponse = await fetch('/api/compliance-report');
    const complianceData = await complianceResponse.json();
    this.updateRecommendations(complianceData.recommendations);
  } catch (error) {
    console.error('Failed to load data:', error);
  }
}
                

This hybrid approach means:

Connection Status Indicator

Users should know whether they're seeing live data or stale information. We add a connection status indicator that changes based on Socket.IO state:

                
updateConnectionStatus(connected) {
  const statusDot = document.getElementById('connection-status');
  const statusText = document.getElementById('connection-text');
  
  if (connected) {
    statusDot.className = 'status-dot online';
    statusText.textContent = 'Connected';
  } else {
    statusDot.className = 'status-dot offline';
    statusText.textContent = 'Disconnected';
  }
}
                

A green pulsing dot indicates real-time updates are working; red means the dashboard is relying on polling.

High-Risk Event Notifications

For critical security events, passive updates in a timeline aren't enough. We create temporary on-screen notifications:

                
showNotification(event) {
  const notification = document.createElement('div');
  notification.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    background: #f56565;
    color: white;
    padding: 1rem;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    z-index: 1001;
  `;
  
  notification.innerHTML = `
    <strong>High Risk Event!</strong><br>
    ${event.command} - ${event.riskReason}
  `;
  
  document.body.appendChild(notification);
  
  setTimeout(() => notification.remove(), 5000);
}
                

This ensures critical security operations are impossible to miss, even if someone isn't actively watching the dashboard.

With real-time communication established, we can now build the frontend components that visualize this security data in an intuitive, actionable way.

Frontend Implementation

The dashboard frontend is built with vanilla JavaScript, HTML, and CSS—no heavy frameworks needed. The goal is a clean, responsive interface that displays security data at a glance.

Dashboard Structure

The UI consists of five main sections arranged in a responsive grid:

                
class SecurityDashboard {
  constructor() {
    this.socket = null;
    this.riskChart = null;
    this.events = [];
    this.sessions = new Map();
    this.init();
  }
  
  init() {
    this.initSocket();
    this.initEventListeners();
    this.initChart();
    this.loadInitialData();
    setInterval(() => this.loadInitialData(), 2000);
  }
}
                
Chart.js Integration

The risk distribution chart provides visual insight into security posture:

                
initChart() {
  const ctx = document.getElementById('riskChart').getContext('2d');
  
  this.riskChart = new Chart(ctx, {
    type: 'doughnut',
    data: {
      labels: ['Low Risk', 'Medium Risk', 'High Risk', 'Critical Risk'],
      datasets: [{
        data: [1, 1, 1, 1],
        backgroundColor: ['#48bb78', '#ed8936', '#f56565', '#e53e3e']
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false
    }
  });
}
                

We update the chart whenever events change:

                
updateRiskChart() {
  const riskCounts = [0, 0, 0, 0];
  
  this.events.forEach(event => {
    const risk = event.riskLevel || 0;
    if (risk <= 1) riskCounts[0]++;
    else if (risk === 2) riskCounts[1]++;
    else if (risk === 3) riskCounts[2]++;
    else if (risk >= 4) riskCounts[3]++;
  });
  
  this.riskChart.data.datasets[0].data = riskCounts;
  this.riskChart.update();
}
                
Event Filtering

Users can filter events by risk level using a dropdown:

                
filterEvents(filter) {
  const items = document.querySelectorAll('.event-item');
  
  items.forEach(item => {
    const eventData = JSON.parse(item.dataset.event);
    const riskLevel = eventData.riskLevel || 0;
    
    let show = false;
    if (filter === 'all') show = true;
    else if (filter === 'high' && riskLevel >= 3) show = true;
    else if (filter === 'medium' && riskLevel === 2) show = true;
    
    item.style.display = show ? 'block' : 'none';
  });
}
                
Dynamic Session Display

Sessions are rendered with full session IDs and visual risk indicators:

                
updateActiveSessions() {
  const container = document.getElementById('sessions-list');
  
  if (this.sessions.size === 0) {
    container.innerHTML = '

No active sessions

'; return; } const sessionsHtml = Array.from(this.sessions.entries()) .map(([sessionId, sessionInfo]) => { const riskLevel = this.calculateSessionRisk(sessionInfo); const riskLabel = this.getRiskLabel(riskLevel); return ` <div class="session-item"> <div class="session-header"> <span class="session-id">${sessionId}</span> <span class="session-risk risk-${riskLevel}">${riskLabel}</span> </div> <div class="session-driver">${sessionInfo.driverName}</div> </div> `; }).join(''); container.innerHTML = sessionsHtml; }

With the frontend complete, we have a fully functional real-time security monitoring system. Next, we'll explore how to make this data actionable through intelligent recommendations.

Generating Security Recommendations

Raw data is valuable, but actionable insights are what make a monitoring system truly useful. The dashboard needs to analyze security events and provide concrete recommendations that help teams reduce risk. This is where the compliance report endpoint earns its place.

Analysis-Based Recommendations

Our recommendation engine examines the last 24 hours of security events and generates tailored advice based on detected patterns:

                
generateComplianceReport() {
  const now = Date.now();
  const last24h = now - (24 * 60 * 60 * 1000);
  
  const recentEvents = this.securityEvents.filter(
    e => e.timestamp > last24h
  );
  
  const recommendations = [];
  
  // Check for high-risk event frequency
  const highRiskCount = recentEvents.filter(e => e.riskLevel >= 3).length;
  if (highRiskCount > 0) {
    recommendations.push({
      level: 'critical',
      message: `${highRiskCount} high-risk security events detected in the last 24 hours`,
      action: 'Review shell commands, file operations, and permission changes. Consider restricting these capabilities.'
    });
  }
  
  // Detect excessive shell command usage
  const shellCommands = recentEvents.filter(e => 
    e.command === 'mobile: shell' || e.command === 'execute'
  );
  if (shellCommands.length > 5) {
    recommendations.push({
      level: 'warning',
      message: `Frequent use of execute/shell commands detected (${shellCommands.length} times)`,
      action: 'Use native WebDriver commands when possible. If shell access is needed, ensure proper sandboxing.'
    });
  }
  
  return {
    timestamp: now,
    period: '24h',
    totalEvents: recentEvents.length,
    activeSessions: this.sessions.size,
    highRiskEvents: highRiskCount,
    recommendations
  };
}
                
Pattern Detection Examples

The engine looks for several suspicious patterns:

File System Operations: Multiple file push/pull operations might indicate data exfiltration attempts or misconfigured test cleanup:

                
const fileOps = recentEvents.filter(e => 
  e.command === 'pushFile' || e.command === 'pullFile'
);
if (fileOps.length > 0) {
  recommendations.push({
    level: 'warning',
    message: `File system operations detected (${fileOps.length} operations)`,
    action: 'Verify that file operations are necessary and paths are validated to prevent unauthorized access.'
  });
}
                

Clipboard Access: Clipboard operations can leak sensitive data between test sessions:

                
const clipboardOps = recentEvents.filter(e => 
  e.command === 'setClipboard' || e.command === 'getClipboard'
);
if (clipboardOps.length > 0) {
  recommendations.push({
    level: 'warning',
    message: `Clipboard access detected (${clipboardOps.length} operations)`,
    action: 'Ensure clipboard data does not contain sensitive information like passwords or tokens.'
  });
}
                

Dangerous Capabilities: Sessions using risky capabilities need review:

                
const riskySessions = Array.from(this.sessions.values()).filter(session =>
  session.securityFeatures.some(f => f.riskLevel >= 3)
);
if (riskySessions.length > 0) {
  recommendations.push({
    level: 'warning',
    message: `${riskySessions.length} session(s) with high-risk capabilities active`,
    action: 'Review capabilities like autoGrantPermissions and allowTestPackages. Disable if not needed.'
  });
}
                
Recommendation Levels

Recommendations use three severity levels that map to visual styling:

                
// Default recommendation when nothing suspicious is found
if (recommendations.length === 0) {
  recommendations.push({
    level: 'info',
    message: 'No significant security concerns detected',
    action: 'Continue monitoring security events and maintain current security practices.'
  });
}
                
Frontend Display

The dashboard renders recommendations with color-coded cards:

                
updateRecommendations(recommendations) {
  const container = document.getElementById('recommendations-list');
  
  const html = recommendations
    .map(rec => `
      <div class="recommendation-item ${rec.level}">
        <div class="recommendation-message">${rec.message}</div>
        <div class="recommendation-action">${rec.action}</div>
      </div>
    `)
    .join('');
  
  container.innerHTML = html;
}
                
Customization for Your Context

The patterns we detect are starting points. Your organization might need different rules:

Modify the generateComplianceReport() method to match your security policies and risk tolerance. The architecture is designed for easy extension—just add new pattern checks and push additional recommendations.

With recommendations in place, teams have a complete feedback loop: they see what's happening, understand the risks, and know what actions to take. Now we need to test that this entire system works reliably.

Testing the Plugin

Building a plugin is one thing—verifying it actually works is another. We need a test script that exercises all the security monitoring features: creates sessions, executes risky commands, and generates diverse security events.

Creating a Test Script

The test script uses WebdriverIO to simulate real automation scenarios with security implications:

                
const { remote } = require('webdriverio');

async function testSecurityDashboard() {
  const capabilities = {
    platformName: 'Android',
    'appium:deviceName': 'Android Emulator',
    'appium:automationName': 'UiAutomator2',
    'appium:autoGrantPermissions': true,  // Triggers security tracking
    'appium:allowTestPackages': true
  };

  const driver = await remote({ hostname: 'localhost', port: 4723, capabilities });
  
  // Generate security events
  await driver.pushFile('/sdcard/test.txt', Buffer.from('data').toString('base64'));
  await driver.executeScript('mobile: shell', [{ command: 'echo', args: ['test'] }]);
  await driver.getPageSource();
  
  // Keep session alive to observe dashboard
  await driver.pause(30000);
  await driver.deleteSession();
}
                
What to Verify

Run the test and check the dashboard for these indicators:

Common Issues

If the dashboard shows zero active sessions, increase the wait time in your test—the session might end before you look. If events aren't appearing, check Appium's console for "Security event logged" messages to confirm the plugin is intercepting commands. Shell commands require starting Appium with --allow-insecure uiautomator2:adb_shell.

Prerequisites

You need an Android emulator running, UiAutomator2 driver installed (appium driver install uiautomator2), and Appium started with the plugin enabled (appium --use-plugins security-dashboard).

A successful test shows active sessions, accumulates 10+ events across different risk levels, displays recommendations, and maintains real-time connectivity throughout. With validation complete, you're ready to consider production deployment strategies.

Production Considerations

Moving from a working prototype to a production-ready plugin requires thinking about performance, security, and operational integration. Here's what you need to consider before deploying this to your team's CI/CD pipeline.

Performance Impact

Every command flowing through your test suite now triggers interception logic. For large test suites running hundreds of sessions daily, this adds overhead. The impact is minimal for individual operations (microseconds per command), but it accumulates.

Memory management is the primary concern. Storing unlimited events will eventually crash the Appium server. Our 1000-event limit prevents this, but adjust based on your needs:

                
if (this.securityEvents.length > 1000) {
  this.securityEvents = this.securityEvents.slice(-1000);
}
                

For high-volume environments, consider persisting events to a database instead of keeping them in memory. PostgreSQL or MongoDB would handle this well, with the plugin writing events asynchronously to avoid blocking test execution.

Socket.IO connections scale reasonably well, but if you have teams across multiple locations all watching the same dashboard, connection counts can climb. Consider implementing connection limits or using Socket.IO's built-in rooms to segment traffic by team or project.

Security of the Dashboard Itself

The irony of a security monitoring tool being insecure isn't lost on anyone. The dashboard currently accepts connections from any origin (cors: { origin: "*" }), which is fine for local development but dangerous in production.

Lock down CORS to specific domains:

                
const io = new Server(dashboardServer, {
  cors: {
    origin: process.env.DASHBOARD_ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
    methods: ['GET', 'POST']
  }
});
                

Consider adding authentication if the dashboard will be exposed beyond your local network. JWT tokens or basic auth would prevent unauthorized access to your test security data.

Customizing Risk Thresholds

Every organization has different risk tolerance. A fintech company might consider any clipboard access critical, while a gaming company might not care. Make risk levels configurable:

                
const riskConfig = {
  commands: {
    'mobile: shell': process.env.SHELL_RISK_LEVEL || 4,
    pushFile: process.env.FILE_WRITE_RISK_LEVEL || 2,
    setClipboard: process.env.CLIPBOARD_RISK_LEVEL || 2
  },
  capabilities: {
    autoGrantPermissions: process.env.AUTO_GRANT_RISK_LEVEL || 3
  }
};
                

This allows teams to tune the dashboard to their specific compliance requirements without modifying code.

CI/CD Integration

The real value comes from integrating security monitoring into your continuous integration pipeline. After each test run, export a compliance report:

                
dashboardApp.get('/api/export-report', (req, res) => {
  const report = generateComplianceReport();
  res.setHeader('Content-Disposition', 'attachment; filename=security-report.json');
  res.json(report);
});
                

Your CI pipeline can then fail builds if high-risk events exceed a threshold, or generate security reports as build artifacts for audit trails.

Logging and Alerting

Console logging is useful during development, but production needs structured logs. Integrate with your existing logging infrastructure:

                
logSecurityEvent(event) {
  this.securityEvents.push(event);
  
  if (event.riskLevel >= 3) {
    logger.warn('High-risk security event', {
      sessionId: event.sessionId,
      command: event.command,
      riskLevel: event.riskLevel,
      timestamp: event.timestamp
    });
  }
}
                

For critical events, consider webhooks to Slack, PagerDuty, or your incident management system. Immediate notification of shell access attempts in production tests can catch configuration errors before they cause problems.

Multi-Environment Strategy

You probably don't need the same monitoring level in development, staging, and production. Use environment variables to adjust behavior:

                
const monitoringLevel = process.env.SECURITY_MONITORING_LEVEL || 'full';

if (monitoringLevel === 'minimal') {
  // Only track high-risk events
  if (securityRisk.level < 3) return result;
}
                

Development environments might log everything for debugging, while production focuses only on anomalies.

Documentation and Team Onboarding

Finally, document what triggers each risk level and why. Your team needs to understand that a "high-risk" event isn't necessarily a security breach—it's a signal to review. Create a runbook explaining:

The plugin is a tool, not a security solution by itself. Its value comes from how your team interprets and acts on the data it provides.

Conclusion & Next Steps

We've built a complete security monitoring solution for Appium 3, from intercepting commands in the plugin layer to displaying real-time insights in a polished dashboard. You now understand how to leverage Appium's plugin architecture to add custom functionality that goes beyond standard test automation—tracking security events, assessing risks, and generating actionable recommendations.

The techniques you've learned here extend beyond security monitoring. The same patterns apply to building performance profilers, custom reporters, test analytics dashboards, or any other cross-cutting concern in your automation infrastructure. Appium's plugin system is surprisingly powerful once you understand the lifecycle methods and shared state patterns.

Consider extending the dashboard with features like historical trend analysis, exportable PDF reports for compliance audits, or integration with your organization's SIEM tools. Add email alerts for critical security events, or implement role-based access control for multi-team environments.

The complete source code for this security dashboard plugin, including all the files discussed in this article, is available on our GitHub repository. Clone it, experiment with it, and adapt it to your needs.

Additional Resources: