/**
* Command Summary and Pre-Execution Analysis System
* Provides detailed summaries of what a command will do before execution
* Uses tree-based parsing for command structure analysis
*/
import { assessRiskLevel, RiskLevel } from './safety.js';
import { redactCommand } from './redaction.js';
export interface CommandSummary {
/** Human-readable description of what the command does */
description: string;
/** Azure service being operated on */
service: string;
/** Operation/action being performed */
operation: string;
/** Key parameters extracted from command */
parameters: Map<string, string>;
/** Risk level assessment */
riskLevel: RiskLevel;
/** Warnings about the command */
warnings: string[];
/** Expected resources affected */
affectedResources: string[];
/** Estimated cost impact (if destructive) */
costImpact?: 'none' | 'low' | 'medium' | 'high';
/** Whether user confirmation is recommended */
requiresConfirmation: boolean;
/** Redacted version of command for logging */
redactedCommand: string;
}
/**
* Tree node for command parsing (DSA: Tree structure)
* Represents the hierarchical structure of Azure CLI commands
*/
interface CommandNode {
value: string;
type: 'base' | 'service' | 'resource' | 'operation' | 'parameter' | 'value';
children: CommandNode[];
metadata?: Record<string, any>;
}
/**
* Parses command string into a tree structure
* Time Complexity: O(n) where n is number of tokens
*/
function parseCommandTree(command: string): CommandNode {
const tokens = command.trim().split(/\s+/);
const root: CommandNode = {
value: 'root',
type: 'base',
children: []
};
let current = root;
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
if (token.toLowerCase() === 'az' || token.toLowerCase().startsWith('az.')) {
// Base command
const node: CommandNode = { value: token, type: 'base', children: [] };
root.children.push(node);
current = node;
} else if (token.startsWith('--') || token.startsWith('-')) {
// Parameter
const paramName = token.replace(/^--?/, '');
const paramValue = tokens[i + 1] || '';
const paramNode: CommandNode = {
value: paramName,
type: 'parameter',
children: [],
metadata: { value: paramValue }
};
current.children.push(paramNode);
i++; // Skip next token (parameter value)
} else if (i === 1) {
// Service (first token after az)
const node: CommandNode = { value: token, type: 'service', children: [] };
current.children.push(node);
current = node;
} else if (i === 2) {
// Resource type (second token after az)
const node: CommandNode = { value: token, type: 'resource', children: [] };
current.children.push(node);
current = node;
} else if (i === 3) {
// Operation (third token after az)
const node: CommandNode = { value: token, type: 'operation', children: [] };
current.children.push(node);
current = node;
}
i++;
}
return root;
}
/**
* Extracts parameters from command tree using DFS
* Time Complexity: O(n) where n is number of nodes
*/
function extractParameters(node: CommandNode): Map<string, string> {
const params = new Map<string, string>();
function traverse(n: CommandNode): void {
if (n.type === 'parameter' && n.metadata?.value) {
params.set(n.value, n.metadata.value);
}
for (const child of n.children) {
traverse(child);
}
}
traverse(node);
return params;
}
/**
* Finds node in tree by type (BFS search)
*/
function findNodeByType(root: CommandNode, type: CommandNode['type']): CommandNode | null {
const queue: CommandNode[] = [root];
while (queue.length > 0) {
const node = queue.shift()!;
if (node.type === type) {
return node;
}
queue.push(...node.children);
}
return null;
}
/**
* Hashmap of Azure service descriptions for better summaries
*/
const SERVICE_DESCRIPTIONS = new Map<string, string>([
['storage', 'Azure Storage'],
['vm', 'Virtual Machine'],
['network', 'Network'],
['keyvault', 'Key Vault'],
['cosmos', 'Cosmos DB'],
['webapp', 'Web App'],
['functionapp', 'Function App'],
['sql', 'SQL Database'],
['monitor', 'Azure Monitor'],
['aks', 'Kubernetes Service'],
]);
/**
* Hashmap of operation descriptions
*/
const OPERATION_DESCRIPTIONS = new Map<string, { verb: string; impact: string }>([
['create', { verb: 'Creating', impact: 'Will create new resources' }],
['delete', { verb: 'Deleting', impact: 'Will permanently delete resources' }],
['update', { verb: 'Updating', impact: 'Will modify existing resources' }],
['list', { verb: 'Listing', impact: 'Read-only operation' }],
['show', { verb: 'Showing', impact: 'Read-only operation' }],
['get', { verb: 'Getting', impact: 'Read-only operation' }],
['set', { verb: 'Setting', impact: 'Will modify configuration' }],
['remove', { verb: 'Removing', impact: 'Will delete configuration or resources' }],
['add', { verb: 'Adding', impact: 'Will create new configuration' }],
['purge', { verb: 'Purging', impact: 'Will permanently erase data' }],
['start', { verb: 'Starting', impact: 'Will start services (may incur costs)' }],
['stop', { verb: 'Stopping', impact: 'Will stop services' }],
['restart', { verb: 'Restarting', impact: 'Will restart services (brief downtime)' }],
]);
/**
* Generates a detailed summary of what a command will do
* This is shown to the user BEFORE executing the command
*/
export function generateCommandSummary(command: string): CommandSummary {
const tree = parseCommandTree(command);
const parameters = extractParameters(tree);
// Extract key components
const serviceNode = findNodeByType(tree, 'service');
const resourceNode = findNodeByType(tree, 'resource');
const operationNode = findNodeByType(tree, 'operation');
const service = serviceNode?.value || 'unknown';
const resource = resourceNode?.value || '';
const operation = operationNode?.value || 'unknown';
// Build description
const serviceName = SERVICE_DESCRIPTIONS.get(service) || service;
const opInfo = OPERATION_DESCRIPTIONS.get(operation) || { verb: operation, impact: 'Unknown impact' };
let description = `${opInfo.verb} ${serviceName}`;
if (resource) {
description += ` ${resource}`;
}
// Add specific resource names if present
const name = parameters.get('name') || parameters.get('n');
const resourceGroup = parameters.get('resource-group') || parameters.get('g');
if (name) {
description += ` "${name}"`;
}
if (resourceGroup) {
description += ` in resource group "${resourceGroup}"`;
}
// Assess risk and generate warnings
const riskLevel = assessRiskLevel(command);
const warnings: string[] = [];
const affectedResources: string[] = [];
if (riskLevel === 'high') {
warnings.push('DESTRUCTIVE OPERATION: This action cannot be undone');
warnings.push(opInfo.impact);
}
if (parameters.has('yes') || parameters.has('y')) {
warnings.push('Auto-confirmation enabled (will not prompt)');
}
if (parameters.has('no-wait')) {
warnings.push('Async operation (will not wait for completion)');
}
if (parameters.has('force')) {
warnings.push('Force flag enabled (will bypass safety checks)');
}
// Identify affected resources
if (name) {
affectedResources.push(`${resource || 'resource'}: ${name}`);
}
if (resourceGroup) {
affectedResources.push(`Resource Group: ${resourceGroup}`);
}
const subscription = parameters.get('subscription');
if (subscription) {
affectedResources.push(`Subscription: ${subscription}`);
}
// Estimate cost impact
let costImpact: CommandSummary['costImpact'] = 'none';
if (['delete', 'remove', 'purge'].includes(operation)) {
costImpact = 'medium'; // May save costs by removing resources
} else if (['create', 'start'].includes(operation)) {
costImpact = 'medium'; // May incur costs
} else if (['update', 'set'].includes(operation)) {
costImpact = 'low'; // Minimal cost impact
}
// Determine if confirmation is required
const requiresConfirmation = riskLevel === 'high' &&
!parameters.has('yes') &&
!parameters.has('y');
return {
description,
service,
operation,
parameters,
riskLevel,
warnings,
affectedResources,
costImpact,
requiresConfirmation,
redactedCommand: redactCommand(command)
};
}
/**
* Formats a command summary for display to the user
* Returns a clean, readable string with proper formatting
*/
export function formatSummaryForDisplay(summary: CommandSummary): string {
const lines: string[] = [];
lines.push('=== Command Summary ===');
lines.push('');
lines.push(`Description: ${summary.description}`);
lines.push(`Service: ${summary.service}`);
lines.push(`Operation: ${summary.operation}`);
lines.push(`Risk Level: ${summary.riskLevel.toUpperCase()}`);
if (summary.affectedResources.length > 0) {
lines.push('');
lines.push('Affected Resources:');
summary.affectedResources.forEach(r => lines.push(` - ${r}`));
}
if (summary.parameters.size > 0) {
lines.push('');
lines.push('Parameters:');
summary.parameters.forEach((value, key) => {
// Redact sensitive parameter values
const displayValue = /(password|secret|key|token)/i.test(key) ? '***' : value;
lines.push(` --${key}: ${displayValue}`);
});
}
if (summary.warnings.length > 0) {
lines.push('');
lines.push('WARNINGS:');
summary.warnings.forEach(w => lines.push(` ! ${w}`));
}
if (summary.costImpact && summary.costImpact !== 'none') {
lines.push('');
lines.push(`Cost Impact: ${summary.costImpact.toUpperCase()}`);
}
if (summary.requiresConfirmation) {
lines.push('');
lines.push('This operation requires confirmation before execution.');
}
lines.push('');
lines.push('========================');
return lines.join('\n');
}
/**
* Validates if a command should be allowed to execute
* based on its summary
*/
export function shouldAllowExecution(summary: CommandSummary, options: {
allowDestructive?: boolean;
maxRiskLevel?: RiskLevel;
} = {}): { allowed: boolean; reason?: string } {
const {
allowDestructive = false,
maxRiskLevel = 'high'
} = options;
// Check risk level
const riskLevels: RiskLevel[] = ['low', 'high'];
const summaryRiskIndex = riskLevels.indexOf(summary.riskLevel);
const maxRiskIndex = riskLevels.indexOf(maxRiskLevel);
if (summaryRiskIndex > maxRiskIndex) {
return {
allowed: false,
reason: `Command risk level (${summary.riskLevel}) exceeds maximum allowed (${maxRiskLevel})`
};
}
// Check destructive operations
if (!allowDestructive && summary.riskLevel === 'high') {
return {
allowed: false,
reason: 'Destructive operations not allowed'
};
}
return { allowed: true };
}