@profullstack/threatcrush 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { generateDefaultConfig, generateModuleConfig } from '../core/config.js';
6
+ import { banner, logger } from '../core/logger.js';
7
+
8
+ interface DetectedService {
9
+ name: string;
10
+ binary: string;
11
+ running: boolean;
12
+ logPath?: string;
13
+ }
14
+
15
+ const SERVICES_TO_DETECT: Array<{
16
+ name: string;
17
+ binaries: string[];
18
+ logPaths: string[];
19
+ moduleConfig: Record<string, unknown>;
20
+ }> = [
21
+ {
22
+ name: 'sshd',
23
+ binaries: ['sshd'],
24
+ logPaths: ['/var/log/auth.log', '/var/log/secure'],
25
+ moduleConfig: { log_path: '/var/log/auth.log', max_failed_attempts: 5, ban_duration_minutes: 30 },
26
+ },
27
+ {
28
+ name: 'nginx',
29
+ binaries: ['nginx'],
30
+ logPaths: ['/var/log/nginx/access.log', '/var/log/nginx/error.log'],
31
+ moduleConfig: { access_log: '/var/log/nginx/access.log', error_log: '/var/log/nginx/error.log' },
32
+ },
33
+ {
34
+ name: 'apache',
35
+ binaries: ['apache2', 'httpd'],
36
+ logPaths: ['/var/log/apache2/access.log', '/var/log/httpd/access_log'],
37
+ moduleConfig: { access_log: '/var/log/apache2/access.log' },
38
+ },
39
+ {
40
+ name: 'postgresql',
41
+ binaries: ['postgres', 'postgresql'],
42
+ logPaths: ['/var/log/postgresql/postgresql-main.log'],
43
+ moduleConfig: { log_path: '/var/log/postgresql/' },
44
+ },
45
+ {
46
+ name: 'mysql',
47
+ binaries: ['mysqld', 'mariadbd'],
48
+ logPaths: ['/var/log/mysql/error.log', '/var/log/mariadb/mariadb.log'],
49
+ moduleConfig: { log_path: '/var/log/mysql/' },
50
+ },
51
+ {
52
+ name: 'redis',
53
+ binaries: ['redis-server'],
54
+ logPaths: ['/var/log/redis/redis-server.log'],
55
+ moduleConfig: { log_path: '/var/log/redis/' },
56
+ },
57
+ {
58
+ name: 'bind',
59
+ binaries: ['named', 'bind9'],
60
+ logPaths: ['/var/log/named/', '/var/log/bind/'],
61
+ moduleConfig: { log_path: '/var/log/named/' },
62
+ },
63
+ ];
64
+
65
+ function isProcessRunning(name: string): boolean {
66
+ try {
67
+ execSync(`pgrep -x ${name}`, { stdio: 'pipe' });
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ function binaryExists(name: string): boolean {
75
+ try {
76
+ execSync(`which ${name}`, { stdio: 'pipe' });
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function findLogPath(paths: string[]): string | undefined {
84
+ return paths.find((p) => existsSync(p));
85
+ }
86
+
87
+ export async function initCommand(): Promise<void> {
88
+ banner();
89
+ logger.info('Initializing ThreatCrush — scanning system...\n');
90
+
91
+ const spinner = ora({ text: 'Detecting services...', color: 'green' }).start();
92
+ const detected: DetectedService[] = [];
93
+
94
+ for (const svc of SERVICES_TO_DETECT) {
95
+ const foundBinary = svc.binaries.find((b) => binaryExists(b));
96
+ if (foundBinary) {
97
+ const running = svc.binaries.some((b) => isProcessRunning(b));
98
+ const logPath = findLogPath(svc.logPaths);
99
+ detected.push({
100
+ name: svc.name,
101
+ binary: foundBinary,
102
+ running,
103
+ logPath,
104
+ });
105
+ }
106
+ }
107
+
108
+ spinner.succeed(`Found ${detected.length} service(s)\n`);
109
+
110
+ // Print findings
111
+ if (detected.length === 0) {
112
+ console.log(chalk.yellow(' No monitored services detected on this system.'));
113
+ console.log(chalk.gray(' ThreatCrush will still work with manual configuration.\n'));
114
+ } else {
115
+ console.log(chalk.green.bold(' Detected Services:'));
116
+ console.log(chalk.gray(' ' + '─'.repeat(60)));
117
+ for (const svc of detected) {
118
+ const status = svc.running
119
+ ? chalk.green('● running')
120
+ : chalk.yellow('○ installed');
121
+ const log = svc.logPath
122
+ ? chalk.gray(` → ${svc.logPath}`)
123
+ : chalk.gray(' → no log found');
124
+ console.log(` ${status} ${chalk.white.bold(svc.name.padEnd(14))}${log}`);
125
+ }
126
+ console.log();
127
+ }
128
+
129
+ // Generate config files
130
+ const configDir = '/etc/threatcrush';
131
+ const confDDir = `${configDir}/threatcrushd.conf.d`;
132
+
133
+ const canWrite = checkWriteAccess(configDir);
134
+
135
+ if (!canWrite) {
136
+ console.log(chalk.yellow(' ⚠ Cannot write to /etc/threatcrush/ (run with sudo for full setup)'));
137
+ console.log(chalk.gray(' Printing generated configs instead:\n'));
138
+
139
+ // Print main config
140
+ console.log(chalk.green.bold(' Main config (/etc/threatcrush/threatcrushd.conf):'));
141
+ console.log(chalk.gray(' ' + '─'.repeat(60)));
142
+ const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
143
+ for (const line of mainConfig.split('\n')) {
144
+ console.log(chalk.gray(' ') + chalk.white(line));
145
+ }
146
+ console.log();
147
+
148
+ // Print module configs
149
+ for (const svc of detected) {
150
+ const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
151
+ if (!svcDef) continue;
152
+ const modName = svc.name === 'sshd' ? 'ssh-guard' : 'log-watcher';
153
+ console.log(chalk.green.bold(` Module config (${confDDir}/${modName}.conf):`));
154
+ const modConfig = generateModuleConfig(modName, {
155
+ ...svcDef.moduleConfig,
156
+ log_path: svc.logPath || svcDef.logPaths[0],
157
+ });
158
+ for (const line of modConfig.split('\n')) {
159
+ console.log(chalk.gray(' ') + chalk.white(line));
160
+ }
161
+ console.log();
162
+ }
163
+ } else {
164
+ const spinner2 = ora({ text: 'Writing configuration files...', color: 'green' }).start();
165
+
166
+ mkdirSync(confDDir, { recursive: true });
167
+ mkdirSync('/var/log/threatcrush', { recursive: true });
168
+ mkdirSync('/var/lib/threatcrush', { recursive: true });
169
+
170
+ // Write main config
171
+ const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
172
+ writeFileSync(`${configDir}/threatcrushd.conf`, mainConfig);
173
+
174
+ // Write module configs
175
+ for (const svc of detected) {
176
+ const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
177
+ if (!svcDef) continue;
178
+ const modName = svc.name === 'sshd' ? 'ssh-guard' : 'log-watcher';
179
+ const modConfig = generateModuleConfig(modName, {
180
+ ...svcDef.moduleConfig,
181
+ log_path: svc.logPath || svcDef.logPaths[0],
182
+ });
183
+ writeFileSync(`${confDDir}/${modName}.conf`, modConfig);
184
+ }
185
+
186
+ spinner2.succeed('Configuration written successfully');
187
+ console.log();
188
+ console.log(chalk.green(' ✓ Main config: ') + chalk.white(`${configDir}/threatcrushd.conf`));
189
+ console.log(chalk.green(' ✓ Module dir: ') + chalk.white(confDDir));
190
+ console.log(chalk.green(' ✓ Log dir: ') + chalk.white('/var/log/threatcrush/'));
191
+ console.log(chalk.green(' ✓ State dir: ') + chalk.white('/var/lib/threatcrush/'));
192
+ }
193
+
194
+ console.log();
195
+ console.log(chalk.green(' Next steps:'));
196
+ console.log(chalk.gray(' 1. Review and edit the config files'));
197
+ console.log(chalk.gray(' 2. Run ') + chalk.white('threatcrush monitor') + chalk.gray(' to start monitoring'));
198
+ console.log(chalk.gray(' 3. Run ') + chalk.white('threatcrush monitor --tui') + chalk.gray(' for the dashboard'));
199
+ console.log();
200
+ }
201
+
202
+ function checkWriteAccess(dir: string): boolean {
203
+ try {
204
+ if (!existsSync(dir)) {
205
+ mkdirSync(dir, { recursive: true });
206
+ }
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { banner } from '../core/logger.js';
3
+ import { discoverModules } from '../core/module-loader.js';
4
+
5
+ export async function modulesListCommand(): Promise<void> {
6
+ banner();
7
+
8
+ const modules = discoverModules();
9
+
10
+ console.log(chalk.green.bold(' Installed Modules'));
11
+ console.log(chalk.gray(' ' + '─'.repeat(60)));
12
+
13
+ if (modules.length === 0) {
14
+ console.log(chalk.gray(' No modules installed.'));
15
+ console.log();
16
+ console.log(chalk.gray(' Built-in modules will be available after running:'));
17
+ console.log(chalk.white(' threatcrush init'));
18
+ console.log();
19
+ console.log(chalk.gray(' Or install from the marketplace:'));
20
+ console.log(chalk.white(' threatcrush modules install <name>'));
21
+ } else {
22
+ console.log();
23
+ console.log(
24
+ chalk.gray(' ') +
25
+ chalk.white.bold('Name'.padEnd(20)) +
26
+ chalk.white.bold('Version'.padEnd(12)) +
27
+ chalk.white.bold('Status'.padEnd(12)) +
28
+ chalk.white.bold('Description'),
29
+ );
30
+ console.log(chalk.gray(' ' + '─'.repeat(60)));
31
+
32
+ for (const mod of modules) {
33
+ const enabled = mod.config.enabled !== false;
34
+ const status = enabled ? chalk.green('enabled') : chalk.gray('disabled');
35
+ console.log(
36
+ chalk.gray(' ') +
37
+ chalk.white(mod.manifest.name.padEnd(20)) +
38
+ chalk.gray(mod.manifest.version.padEnd(12)) +
39
+ status.padEnd(21) +
40
+ chalk.gray(mod.manifest.description || '—'),
41
+ );
42
+ }
43
+ }
44
+
45
+ console.log();
46
+ }
47
+
48
+ export async function modulesInstallCommand(name: string): Promise<void> {
49
+ banner();
50
+
51
+ console.log(chalk.green.bold(' Module Installation'));
52
+ console.log(chalk.gray(' ' + '─'.repeat(50)));
53
+ console.log();
54
+
55
+ if (name.startsWith('./') || name.startsWith('/')) {
56
+ console.log(chalk.yellow(` Local module installation from: ${name}`));
57
+ console.log(chalk.gray(' → This feature is coming soon.'));
58
+ } else {
59
+ console.log(chalk.yellow(` Installing module: ${chalk.white.bold(name)}`));
60
+ console.log();
61
+ console.log(chalk.gray(' Module marketplace is not yet available.'));
62
+ console.log(chalk.gray(' Stay tuned — module store launching soon at:'));
63
+ console.log(chalk.cyan(' https://threatcrush.com/modules'));
64
+ }
65
+
66
+ console.log();
67
+ }
@@ -0,0 +1,208 @@
1
+ import { existsSync, createReadStream, statSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { watch } from 'node:fs';
4
+ import chalk from 'chalk';
5
+ import { formatEvent, logger, banner } from '../core/logger.js';
6
+ import { autoDetectParser, detectAttackPattern, parseAuthLog, parseNginxLog } from '../core/log-parser.js';
7
+ import { initStateDB, insertEvent } from '../core/state.js';
8
+ import type { ThreatEvent, EventSeverity, EventCategory } from '../types/events.js';
9
+
10
+ interface MonitorOptions {
11
+ module?: string;
12
+ tui?: boolean;
13
+ }
14
+
15
+ interface LogWatcher {
16
+ path: string;
17
+ name: string;
18
+ category: EventCategory;
19
+ }
20
+
21
+ const LOG_SOURCES: LogWatcher[] = [
22
+ { path: '/var/log/auth.log', name: 'ssh-guard', category: 'auth' },
23
+ { path: '/var/log/secure', name: 'ssh-guard', category: 'auth' },
24
+ { path: '/var/log/nginx/access.log', name: 'log-watcher', category: 'web' },
25
+ { path: '/var/log/syslog', name: 'log-watcher', category: 'system' },
26
+ ];
27
+
28
+ export async function monitorCommand(options: MonitorOptions): Promise<void> {
29
+ if (options.tui) {
30
+ // Dynamic import to avoid loading blessed when not needed
31
+ const { startDashboard } = await import('../tui/dashboard.js');
32
+ await startDashboard();
33
+ return;
34
+ }
35
+
36
+ banner();
37
+ logger.info('Starting foreground monitor...');
38
+
39
+ const moduleFilter = options.module?.split(',').map((m) => m.trim());
40
+
41
+ // Find available log files
42
+ const availableSources = LOG_SOURCES.filter((s) => {
43
+ if (moduleFilter && !moduleFilter.includes(s.name)) return false;
44
+ return existsSync(s.path);
45
+ });
46
+
47
+ if (availableSources.length === 0) {
48
+ logger.warn('No log files found to monitor.');
49
+ logger.info('Available log paths checked:');
50
+ for (const s of LOG_SOURCES) {
51
+ const exists = existsSync(s.path);
52
+ console.log(` ${exists ? chalk.green('✓') : chalk.red('✗')} ${s.path}`);
53
+ }
54
+ console.log();
55
+ logger.info('Starting demo mode with synthetic events...\n');
56
+ await runDemoMode();
57
+ return;
58
+ }
59
+
60
+ try {
61
+ initStateDB();
62
+ } catch {
63
+ // State DB optional for monitoring
64
+ }
65
+
66
+ logger.info(`Monitoring ${availableSources.length} log source(s):`);
67
+ for (const src of availableSources) {
68
+ console.log(` ${chalk.green('●')} ${chalk.white(src.name.padEnd(14))} → ${chalk.gray(src.path)}`);
69
+ }
70
+ console.log();
71
+ console.log(chalk.gray(' Press Ctrl+C to stop\n'));
72
+
73
+ // Tail each log file
74
+ for (const src of availableSources) {
75
+ tailLog(src);
76
+ }
77
+
78
+ // Keep process alive
79
+ await new Promise(() => {});
80
+ }
81
+
82
+ function tailLog(source: LogWatcher): void {
83
+ const { path, name, category } = source;
84
+
85
+ // Start reading from end of file
86
+ const stat = statSync(path);
87
+ let position = stat.size;
88
+
89
+ const checkForNewData = () => {
90
+ try {
91
+ const currentStat = statSync(path);
92
+ if (currentStat.size <= position) {
93
+ if (currentStat.size < position) position = 0; // file rotated
94
+ return;
95
+ }
96
+
97
+ const stream = createReadStream(path, { start: position, encoding: 'utf-8' });
98
+ const rl = createInterface({ input: stream });
99
+
100
+ rl.on('line', (line) => {
101
+ if (!line.trim()) return;
102
+ processLine(line, name, category);
103
+ });
104
+
105
+ rl.on('close', () => {
106
+ position = currentStat.size;
107
+ });
108
+ } catch {
109
+ // file may not be readable
110
+ }
111
+ };
112
+
113
+ // Poll for changes (more reliable than fs.watch for log files)
114
+ setInterval(checkForNewData, 1000);
115
+ }
116
+
117
+ function processLine(line: string, moduleName: string, category: EventCategory): void {
118
+ const parsed = autoDetectParser(line);
119
+ if (!parsed) return;
120
+
121
+ let severity: EventSeverity = 'info';
122
+ let message = line;
123
+
124
+ if (parsed.source === 'auth') {
125
+ const authEntry = parseAuthLog(line);
126
+ if (!authEntry) return;
127
+
128
+ if (/failed password/i.test(authEntry.fields.message)) {
129
+ severity = 'high';
130
+ message = `Failed SSH login for ${authEntry.fields.user || 'unknown'} from ${authEntry.fields.ip || 'unknown'}`;
131
+ } else if (/accepted/i.test(authEntry.fields.message)) {
132
+ severity = 'info';
133
+ message = `SSH login accepted for ${authEntry.fields.user || 'unknown'}`;
134
+ } else if (/invalid user/i.test(authEntry.fields.message)) {
135
+ severity = 'high';
136
+ message = `Invalid SSH user attempt: ${authEntry.fields.user || 'unknown'} from ${authEntry.fields.ip || 'unknown'}`;
137
+ }
138
+ } else if (parsed.source === 'nginx') {
139
+ const nginxEntry = parseNginxLog(line);
140
+ if (!nginxEntry) return;
141
+
142
+ const status = parseInt(nginxEntry.fields.status);
143
+ const attack = detectAttackPattern(nginxEntry.fields.path);
144
+
145
+ if (attack) {
146
+ severity = 'critical';
147
+ message = `Attack detected [${attack.toUpperCase()}]: ${nginxEntry.fields.method} ${nginxEntry.fields.path}`;
148
+ } else if (status >= 500) {
149
+ severity = 'medium';
150
+ message = `Server error ${status}: ${nginxEntry.fields.method} ${nginxEntry.fields.path}`;
151
+ } else if (status >= 400) {
152
+ severity = 'low';
153
+ message = `Client error ${status}: ${nginxEntry.fields.method} ${nginxEntry.fields.path}`;
154
+ } else {
155
+ severity = 'info';
156
+ message = `${nginxEntry.fields.method} ${nginxEntry.fields.path} → ${status}`;
157
+ }
158
+ }
159
+
160
+ const event: ThreatEvent = {
161
+ timestamp: new Date(),
162
+ module: moduleName,
163
+ category,
164
+ severity,
165
+ message,
166
+ source_ip: parsed.fields.ip as string | undefined,
167
+ };
168
+
169
+ console.log(formatEvent(event.module, event.severity, event.message, event.source_ip));
170
+
171
+ try {
172
+ insertEvent(event);
173
+ } catch {
174
+ // State DB may not be available
175
+ }
176
+ }
177
+
178
+ async function runDemoMode(): Promise<void> {
179
+ const demoEvents: Array<Omit<ThreatEvent, 'timestamp'>> = [
180
+ { module: 'ssh-guard', category: 'auth', severity: 'info', message: 'SSH login accepted for ubuntu', source_ip: '10.0.0.1' },
181
+ { module: 'log-watcher', category: 'web', severity: 'info', message: 'GET /api/health → 200', source_ip: '172.16.0.50' },
182
+ { module: 'ssh-guard', category: 'auth', severity: 'high', message: 'Failed SSH login for root', source_ip: '45.33.22.11' },
183
+ { module: 'log-watcher', category: 'web', severity: 'low', message: 'GET /admin → 404', source_ip: '91.121.44.55' },
184
+ { module: 'ssh-guard', category: 'auth', severity: 'high', message: 'Failed SSH login for admin', source_ip: '45.33.22.11' },
185
+ { module: 'log-watcher', category: 'web', severity: 'critical', message: 'Attack detected [SQLI]: GET /search?q=1%27+OR+1%3D1', source_ip: '185.220.101.44' },
186
+ { module: 'log-watcher', category: 'web', severity: 'medium', message: 'Server error 502: GET /api/users', source_ip: '10.0.0.2' },
187
+ { module: 'ssh-guard', category: 'auth', severity: 'high', message: 'Invalid SSH user attempt: admin123', source_ip: '103.77.88.99' },
188
+ { module: 'log-watcher', category: 'web', severity: 'critical', message: 'Attack detected [PATH_TRAVERSAL]: GET /../../etc/passwd', source_ip: '185.220.101.44' },
189
+ { module: 'ssh-guard', category: 'auth', severity: 'high', message: 'Brute force detected — 6 failures in 45s', source_ip: '45.33.22.11' },
190
+ { module: 'log-watcher', category: 'system', severity: 'medium', message: 'Unusual outbound connection to 198.51.100.1:4444', source_ip: '198.51.100.1' },
191
+ { module: 'log-watcher', category: 'web', severity: 'info', message: 'GET /index.html → 200', source_ip: '172.16.0.51' },
192
+ ];
193
+
194
+ let i = 0;
195
+ const interval = setInterval(() => {
196
+ const event = demoEvents[i % demoEvents.length];
197
+ console.log(formatEvent(event.module, event.severity, event.message, event.source_ip));
198
+ i++;
199
+ }, 1500 + Math.random() * 2000);
200
+
201
+ process.on('SIGINT', () => {
202
+ clearInterval(interval);
203
+ console.log(chalk.gray('\n Monitor stopped.'));
204
+ process.exit(0);
205
+ });
206
+
207
+ await new Promise(() => {});
208
+ }