Skip to content

Commit 450700f

Browse files
authored
Merge pull request #307 from shivasurya/shiva/cli-analytics
secureflow-cli/feat: add analytics tracking for CLI commands and usage metrics
2 parents fba1562 + 2ae1289 commit 450700f

File tree

9 files changed

+628
-156
lines changed

9 files changed

+628
-156
lines changed

extension/secureflow/packages/secureflow-cli/bin/secureflow

Lines changed: 144 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,76 @@
88
const { Command } = require('commander');
99
const { yellow, red, cyan } = require('colorette');
1010
const pkg = require('../package.json');
11-
const { getMaskedConfig, loadConfig, CONFIG_FILE } = require('../lib/config');
11+
const { getMaskedConfig, loadConfig, setAnalyticsEnabled, CONFIG_FILE } = require('../lib/config');
1212
const { CLIProfileCommand } = require('../profiler');
1313
const { CLIFullScanCommand } = require('../scanner');
14+
const { AnalyticsService } = require('../lib/services/analytics');
1415

15-
const program = new Command();
16-
17-
program
18-
.name('secureflow')
19-
.description('AI-powered security analysis CLI tool with intelligent file discovery')
20-
.version(pkg.version);
21-
22-
program
23-
.command('scan')
24-
.description('Perform full security scan of project using AI analysis')
25-
.argument('[path]', 'Path to project directory (defaults to current directory)', '.')
26-
.option('--model <model>', 'AI model to use for analysis')
27-
.option('--format <format>', 'Output format: text|json|defectdojo', 'text')
28-
.option('--output <file>', 'Save results to file')
29-
.option('--defectdojo', 'Export results in DefectDojo import format (same as --format defectdojo)')
30-
.option('--defectdojo-url <url>', 'DefectDojo instance URL (e.g., https://defectdojo.example.com)')
31-
.option('--defectdojo-token <token>', 'DefectDojo API token for authentication')
32-
.option('--defectdojo-product-id <id>', 'DefectDojo product ID to submit findings to')
33-
.option('--defectdojo-engagement-id <id>', 'DefectDojo engagement ID (optional - will create if not provided)')
34-
.option('--defectdojo-test-title <title>', 'Title for the DefectDojo test (defaults to "SecureFlow Scan")')
35-
.action(async (projectPath, options) => {
16+
// Initialize analytics for CLI
17+
const analytics = AnalyticsService.getInstance();
18+
19+
// Main async function to ensure proper initialization order
20+
async function main() {
21+
const config = loadConfig();
22+
23+
// Initialize analytics FIRST if enabled by user (default: true)
24+
const analyticsEnabled = config.analytics?.enabled !== false;
25+
26+
if (analyticsEnabled) {
27+
try {
28+
await analytics.initializeForCLI({
29+
cli_version: pkg.version
30+
});
31+
} catch (err) {
32+
// Silently fail - analytics should not disrupt CLI
33+
}
34+
}
35+
36+
// NOW set up the program after analytics is ready
37+
const program = new Command();
38+
39+
program
40+
.name('secureflow')
41+
.description('AI-powered security analysis CLI tool with intelligent file discovery')
42+
.version(pkg.version)
43+
.option('--disable-analytics', 'Disable analytics for this session')
44+
.option('--enable-analytics', 'Enable analytics for this session');
45+
46+
// Handle analytics flags
47+
program.hook('preAction', (thisCommand, actionCommand) => {
48+
const opts = thisCommand.opts();
49+
if (opts.disableAnalytics) {
50+
analytics.initialized = false; // Disable for this session
51+
}
52+
});
53+
54+
program
55+
.command('scan')
56+
.description('Perform full security scan of project using AI analysis')
57+
.argument('[path]', 'Path to project directory (defaults to current directory)', '.')
58+
.option('--model <model>', 'AI model to use for analysis')
59+
.option('--format <format>', 'Output format: text|json|defectdojo', 'text')
60+
.option('--output <file>', 'Save results to file')
61+
.option('--defectdojo', 'Export results in DefectDojo import format (same as --format defectdojo)')
62+
.option('--defectdojo-url <url>', 'DefectDojo instance URL (e.g., https://defectdojo.example.com)')
63+
.option('--defectdojo-token <token>', 'DefectDojo API token for authentication')
64+
.option('--defectdojo-product-id <id>', 'DefectDojo product ID to submit findings to')
65+
.option('--defectdojo-engagement-id <id>', 'DefectDojo engagement ID (optional - will create if not provided)')
66+
.option('--defectdojo-test-title <title>', 'Title for the DefectDojo test (defaults to "SecureFlow Scan")')
67+
.action(async (projectPath, options) => {
3668
try {
69+
// Load config to get actual model being used
70+
const config = loadConfig();
71+
const actualModel = options.model || config.model;
72+
73+
// Track scan command usage with actual model
74+
await analytics.trackEvent('CLI Command: Scan', {
75+
ai_model: actualModel,
76+
ai_provider: config.provider,
77+
output_format: options.format || 'text',
78+
has_defectdojo: !!options.defectdojo
79+
});
80+
3781
// Handle --defectdojo flag
3882
let outputFormat = options.format;
3983
if (options.defectdojo) {
@@ -63,15 +107,26 @@ program
63107
}
64108
});
65109

66-
program
67-
.command('profile')
68-
.description('Profile project to identify application types and technologies')
69-
.argument('[path]', 'Path to project directory (defaults to current directory)', '.')
70-
.option('--model <model>', 'AI model to use for analysis')
71-
.option('--format <format>', 'Output format: text|json', 'text')
72-
.option('--output <file>', 'Save results to file')
73-
.action(async (projectPath, options) => {
110+
program
111+
.command('profile')
112+
.description('Profile project to identify application types and technologies')
113+
.argument('[path]', 'Path to project directory (defaults to current directory)', '.')
114+
.option('--model <model>', 'AI model to use for analysis')
115+
.option('--format <format>', 'Output format: text|json', 'text')
116+
.option('--output <file>', 'Save results to file')
117+
.action(async (projectPath, options) => {
74118
try {
119+
// Load config to get actual model being used
120+
const config = loadConfig();
121+
const actualModel = options.model || config.model;
122+
123+
// Track profile command usage with actual model
124+
await analytics.trackEvent('CLI Command: Profile', {
125+
ai_model: actualModel,
126+
ai_provider: config.provider,
127+
output_format: options.format || 'text'
128+
});
129+
75130
const profileCommand = new CLIProfileCommand({
76131
selectedModel: options.model
77132
});
@@ -86,12 +141,12 @@ program
86141
}
87142
});
88143

89-
program
90-
.command('config')
91-
.description('Show CLI configuration (masked by default)')
92-
.option('--show', 'Show configuration summary', false)
93-
.option('--raw', 'Do not mask secrets (use with caution)', false)
94-
.action((opts) => {
144+
program
145+
.command('config')
146+
.description('Show CLI configuration (masked by default)')
147+
.option('--show', 'Show configuration summary', false)
148+
.option('--raw', 'Do not mask secrets (use with caution)', false)
149+
.action((opts) => {
95150
if (!opts.show) {
96151
console.log('Use --show to display the configuration.');
97152
console.log(`Config file path: ${CONFIG_FILE}`);
@@ -105,14 +160,60 @@ program
105160
process.exitCode = 0;
106161
});
107162

108-
program
109-
.command('helpall')
110-
.description('Show help for all commands')
111-
.action(() => {
112-
program.commands.forEach((c) => c.outputHelp());
113-
});
163+
program
164+
.command('analytics <action>')
165+
.description('Manage analytics preferences (enable|disable|status)')
166+
.action((action) => {
167+
const cfg = loadConfig();
168+
const currentStatus = cfg.analytics?.enabled !== false ? 'enabled' : 'disabled';
169+
170+
if (action === 'status') {
171+
console.log(cyan('Analytics Status:'), currentStatus);
172+
console.log('\nAnalytics help improve SecureFlow by collecting anonymous usage metrics.');
173+
console.log('No personal information, code, or file paths are collected.');
174+
console.log('\nCommands:');
175+
console.log(' secureflow analytics enable - Enable analytics permanently');
176+
console.log(' secureflow analytics disable - Disable analytics permanently');
177+
console.log(' secureflow analytics status - Show current status');
178+
} else if (action === 'enable') {
179+
if (setAnalyticsEnabled(true)) {
180+
console.log(cyan('✓ Analytics enabled'));
181+
console.log('Thank you for helping improve SecureFlow!');
182+
} else {
183+
console.error(red('✗ Failed to update analytics preference'));
184+
}
185+
} else if (action === 'disable') {
186+
if (setAnalyticsEnabled(false)) {
187+
console.log(cyan('✓ Analytics disabled'));
188+
console.log('Analytics have been turned off.');
189+
} else {
190+
console.error(red('✗ Failed to update analytics preference'));
191+
}
192+
} else {
193+
console.error(red(`Unknown action: ${action}`));
194+
console.log('Valid actions: enable, disable, status');
195+
process.exitCode = 1;
196+
}
197+
});
198+
199+
program
200+
.command('helpall')
201+
.description('Show help for all commands')
202+
.action(async () => {
203+
// Track help command usage
204+
await analytics.trackEvent('CLI Command: Help', {});
205+
program.commands.forEach((c) => c.outputHelp());
206+
});
207+
208+
// Parse CLI arguments
209+
await program.parseAsync(process.argv);
210+
211+
// Quick shutdown for fast exit (fire and forget)
212+
analytics.shutdown(true).catch(() => {});
213+
}
114214

115-
program.parseAsync(process.argv).catch((err) => {
215+
// Run the main function
216+
main().catch((err) => {
116217
console.error(red('[secureflow] fatal error'));
117218
console.error(err?.stack || err?.message || String(err));
118219
process.exit(1);

extension/secureflow/packages/secureflow-cli/lib/config.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function loadConfig() {
4343
env.SECUREFLOW_API_KEY || env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY || fileCfg.apiKey || '',
4444
provider: env.SECUREFLOW_PROVIDER || fileCfg.provider || inferProvider(env, fileCfg),
4545
analytics: {
46-
enabled: getBool(env.SECUREFLOW_ANALYTICS_ENABLED, fileCfg?.analytics?.enabled, false)
46+
enabled: getBool(env.SECUREFLOW_ANALYTICS_ENABLED, fileCfg?.analytics?.enabled, true) // Default: enabled
4747
}
4848
};
4949

@@ -84,9 +84,31 @@ function getMaskedConfig() {
8484
};
8585
}
8686

87+
function setAnalyticsEnabled(enabled) {
88+
try {
89+
// Ensure directory exists
90+
if (!fs.existsSync(CONFIG_DIR)) {
91+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
92+
}
93+
94+
// Read existing config
95+
const existing = readJsonSafe(CONFIG_FILE);
96+
97+
// Update analytics setting
98+
existing.analytics = { ...existing.analytics, enabled };
99+
100+
// Write back to file
101+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
102+
return true;
103+
} catch (error) {
104+
return false;
105+
}
106+
}
107+
87108
module.exports = {
88109
loadConfig,
89110
getMaskedConfig,
111+
setAnalyticsEnabled,
90112
CONFIG_DIR,
91113
CONFIG_FILE
92114
};

extension/secureflow/packages/secureflow-cli/lib/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ export { getPromptPath, getAppProfilerPrompt } from './prompts';
1313
export { loadPrompt, getPromptForAppType, getApplicationProfilerPrompt, getThreatModelingPrompt } from './prompts/prompt-loader';
1414

1515
export { WorkspaceAnalyzer, ApplicationProfile, WorkspaceAnalyzerOptions } from './workspace-analyzer';
16+
17+
// Export analytics service
18+
export { AnalyticsService, StorageAdapter, FileStorageAdapter, VSCodeStorageAdapter } from './services/analytics';

extension/secureflow/packages/secureflow-cli/lib/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const { loadPrompt, getPromptForAppType, getApplicationProfilerPrompt, getThreat
1515
// Export workspace analyzer functionality
1616
const { WorkspaceAnalyzer, ApplicationProfile } = require('./workspace-analyzer');
1717

18+
// Export analytics service
19+
const { AnalyticsService, StorageAdapter, FileStorageAdapter, VSCodeStorageAdapter } = require('./services/analytics');
20+
1821
module.exports = {
1922
AIClient,
2023
AIClientFactory,
@@ -31,5 +34,9 @@ module.exports = {
3134
getApplicationProfilerPrompt,
3235
getThreatModelingPrompt,
3336
WorkspaceAnalyzer,
34-
ApplicationProfile
37+
ApplicationProfile,
38+
AnalyticsService,
39+
StorageAdapter,
40+
FileStorageAdapter,
41+
VSCodeStorageAdapter
3542
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* TypeScript declarations for shared Analytics Service
3+
*/
4+
5+
/**
6+
* Storage adapter interface for distinct ID persistence
7+
*/
8+
export declare class StorageAdapter {
9+
get(key: string): Promise<string | null>;
10+
set(key: string, value: string): Promise<void>;
11+
}
12+
13+
/**
14+
* File-based storage for CLI usage
15+
*/
16+
export declare class FileStorageAdapter extends StorageAdapter {
17+
constructor();
18+
get(key: string): Promise<string | null>;
19+
set(key: string, value: string): Promise<void>;
20+
}
21+
22+
/**
23+
* VS Code storage adapter
24+
*/
25+
export declare class VSCodeStorageAdapter extends StorageAdapter {
26+
constructor(context: any);
27+
get(key: string): Promise<string | null>;
28+
set(key: string, value: string): Promise<void>;
29+
}
30+
31+
/**
32+
* Shared Analytics Service
33+
*/
34+
export declare class AnalyticsService {
35+
static getInstance(): AnalyticsService;
36+
37+
/**
38+
* Initialize for CLI usage
39+
*/
40+
initializeForCLI(metadata?: Record<string, any>): Promise<void>;
41+
42+
/**
43+
* Initialize for VS Code extension usage
44+
*/
45+
initializeForVSCode(context: any, metadata?: Record<string, any>): Promise<void>;
46+
47+
/**
48+
* Check if analytics is enabled
49+
*/
50+
isEnabled(): boolean;
51+
52+
/**
53+
* Track an event with properties
54+
*/
55+
trackEvent(eventName: string, properties?: Record<string, any>): Promise<void>;
56+
57+
/**
58+
* Get the distinct ID for this user
59+
*/
60+
getDistinctId(): string;
61+
62+
/**
63+
* Get the current platform
64+
*/
65+
getPlatform(): string;
66+
67+
/**
68+
* Shutdown analytics service
69+
* @param quick - If true, don't wait for flush (faster exit)
70+
*/
71+
shutdown(quick?: boolean): Promise<void>;
72+
}

0 commit comments

Comments
 (0)